腾讯云 Windows Server 2022 RDP 截屏黑屏 + 坐标偏移完整解决方案



一、问题背景

在腾讯云 Windows Server 2022 服务器上使用传统BitBlt方式实现区域截屏功能时,遇到两个核心问题:

■ RDP 最小化或直接关闭窗口后,截屏图片全黑;
■ 实际截取的区域与鼠标选择的区域严重偏移;

本文仅记录本人实际验证通过的解决方案,所有步骤和代码均经过腾讯云 CVM 实测,可直接复用。

二、解决RDP 断开后截屏全黑

Windows 远程桌面服务的资源优化机制:当检测到 RDP 会话断开或最小化时,系统会立即切断该会话的图形渲染管道,释放 GPU 资源,导致传统 GDI 截屏 API(BitBlt/GetDC)无法获取到画面数据,只能得到全黑图像。通过配置组策略 + tscon 脚本,无需修改任何截屏代码,仅需服务器端配置即可解决黑屏问题。

配置组策略

按Win+R输入gpedit.msc打开本地组策略编辑器,启用硬件图形适配器策略和启用单用户单会话限制策略,配置完重启:
引用内容 引用内容
计算机配置 → 管理模板 → Windows 组件 → 远程桌面服务 → 远程桌面会话主机 → 远程会话环境
→ 将硬件图形适配器应用于所有远程桌面服务会话 → 已启用

计算机配置 → 管理模板 → Windows 组件 → 远程桌面服务 → 远程桌面会话主机 → 连接
→ 将远程桌面服务用户限制到单独的远程桌面服务会话 → 已启用

使用 tscon 脚本断开 RDP(核心关键)

绝对不能直接点击 RDP 窗口的关闭按钮,这会导致会话进入断开状态,图形渲染依然会被释放。每次要退出 RDP 时,右键以管理员身份运行以下 bat 脚本:
@echo off
:: 将当前RDP会话无缝切换到服务器本地控制台
:: 保持会话后台锁定但活动,图形渲染不中断
for /f "skip=1 tokens=3" %%s in ('query user %USERNAME%') do (
  %windir%\System32\tscon.exe %%s /dest:console
)

三、解决黑屏后坐标严重偏移

这是最容易被忽略的坑:RDP 会话和服务器控制台会话使用完全不同的显示设备和分辨率。RDP 会话使用虚拟显示适配器,分辨率由本地客户端指定(通常是 1366×768),控制台会话腾讯云服务器默认没有物理显示器,控制台分辨率只有1024×768。在 RDP 里选择的坐标是基于 1366×768 的,但 tscon 切换到控制台后,截屏用的是 1024×768 的坐标系,导致坐标完全错位。

在代码中自动检测两个会话的分辨率差异并缩放坐标,即可一劳永逸解决所有分辨率不匹配问题,示例代码:
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    class Program
    {
        // 保存选择区域时的信息
        private static RECT _selectedRect;
        private static Size _selectTimeScreenSize;

        [STAThread] // 必须标记,否则WinForms窗口无法正常显示
        static void Main(string[] args)
        {
            Console.WriteLine("=== 腾讯云RDP区域截屏工具 ===");
            Console.WriteLine("提示:按ESC可取消区域选择");
            Console.WriteLine("按任意键开始选择区域...");
            Console.ReadKey();

            // 1. 记录选择区域时的屏幕分辨率
            _selectTimeScreenSize = Screen.PrimaryScreen.Bounds.Size;

            // 2. 打开区域选择窗口
            Console.WriteLine("\n正在打开区域选择窗口...");
            using (RegionSelectForm selectForm = new RegionSelectForm())
            {
                if (selectForm.ShowDialog() != DialogResult.OK)
                {
                    Console.WriteLine("已取消选择,程序退出。");
                    return;
                }

                _selectedRect = selectForm.SelectedRect;
                if (_selectedRect.Right <= _selectedRect.Left || _selectedRect.Bottom <= _selectedRect.Top)
                {
                    Console.WriteLine("错误:选择的区域无效");
                    return;
                }
            }

            Console.WriteLine($"\n已选择区域:({_selectedRect.Left},{_selectedRect.Top})-({_selectedRect.Right},{_selectedRect.Bottom})");
            
            // 3. 5秒倒计时
            Console.WriteLine("倒计时开始:");
            for (int i = 5; i > 0; i--)
            {
                Console.Write($"{i}... ");
                Thread.Sleep(1000);
            }
            Console.WriteLine("\n正在截屏...");

            // 4. 执行截屏
            CaptureWithAutoScale();

            Console.WriteLine("\n按任意键退出...");
            Console.ReadKey();
        }

        /// <summary>
        /// 自动适配分辨率的截屏方法(核心)
        /// </summary>
        private static void CaptureWithAutoScale()
        {
            IntPtr hScreenDC = IntPtr.Zero;
            IntPtr hMemDC = IntPtr.Zero;
            IntPtr hBitmap = IntPtr.Zero;
            IntPtr hOldBitmap = IntPtr.Zero;

            try
            {
                // 1. 获取截屏时(控制台)的实际物理分辨率
                Size captureTimeScreenSize = GetPhysicalScreenSize();
                
                // 2. 计算分辨率缩放比例
                float scaleX = (float)captureTimeScreenSize.Width / _selectTimeScreenSize.Width;
                float scaleY = (float)captureTimeScreenSize.Height / _selectTimeScreenSize.Height;

                // 3. 自动修正坐标
                int scaledLeft = (int)Math.Round(_selectedRect.Left * scaleX);
                int scaledTop = (int)Math.Round(_selectedRect.Top * scaleY);
                int scaledRight = (int)Math.Round(_selectedRect.Right * scaleX);
                int scaledBottom = (int)Math.Round(_selectedRect.Bottom * scaleY);

                int width = scaledRight - scaledLeft;
                int height = scaledBottom - scaledTop;

                // 4. 纯Win32 API截屏
                hScreenDC = NativeMethods.CreateDC("DISPLAY", null, null, IntPtr.Zero);
                hMemDC = NativeMethods.CreateCompatibleDC(hScreenDC);
                hBitmap = NativeMethods.CreateCompatibleBitmap(hScreenDC, width, height);
                hOldBitmap = NativeMethods.SelectObject(hMemDC, hBitmap);

                NativeMethods.BitBlt(hMemDC, 0, 0, width, height, hScreenDC, scaledLeft, scaledTop, 0x00CC0020);

                // 5. 保存图片
                string fileName = $"screenshot_{DateTime.Now:yyyyMMdd_HHmmss}.jpg";
                string path = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
                
                using (Bitmap bmp = Image.FromHbitmap(hBitmap))
                {
                    bmp.Save(path, ImageFormat.Jpeg);
                }

                // 6. 输出结果
                Console.WriteLine("\n========== 截屏成功 ==========");
                Console.WriteLine($"保存路径:{path}");
                Console.WriteLine($"\n【调试信息】");
                Console.WriteLine($"选择时分辨率:{_selectTimeScreenSize.Width}×{_selectTimeScreenSize.Height}");
                Console.WriteLine($"截屏时分辨率:{captureTimeScreenSize.Width}×{captureTimeScreenSize.Height}");
                Console.WriteLine($"缩放比例:X={scaleX:F2}, Y={scaleY:F2}");
                Console.WriteLine($"原始坐标:({_selectedRect.Left},{_selectedRect.Top})-({_selectedRect.Right},{_selectedRect.Bottom})");
                Console.WriteLine($"修正后坐标:({scaledLeft},{scaledTop})-({scaledRight},{scaledBottom})");
                Console.WriteLine("==============================");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"\n错误:截屏失败 - {ex.Message}");
            }
            finally
            {
                // 严格释放GDI资源
                if (hOldBitmap != IntPtr.Zero) NativeMethods.SelectObject(hMemDC, hOldBitmap);
                if (hBitmap != IntPtr.Zero) NativeMethods.DeleteObject(hBitmap);
                if (hMemDC != IntPtr.Zero) NativeMethods.DeleteDC(hMemDC);
                if (hScreenDC != IntPtr.Zero) NativeMethods.DeleteDC(hScreenDC);
            }
        }

        /// <summary>
        /// 获取物理屏幕的实际分辨率
        /// </summary>
        private static Size GetPhysicalScreenSize()
        {
            IntPtr hDC = NativeMethods.CreateDC("DISPLAY", null, null, IntPtr.Zero);
            int width = NativeMethods.GetDeviceCaps(hDC, 8); // HORZRES
            int height = NativeMethods.GetDeviceCaps(hDC, 10); // VERTRES
            NativeMethods.DeleteDC(hDC);
            return new Size(width, height);
        }

        #region 区域选择窗口(独立类,复用原逻辑)
        private class RegionSelectForm : Form
        {
            private Point _startPoint;
            private Point _endPoint;
            private bool _isSelecting;
            public RECT SelectedRect { get; private set; }

            public RegionSelectForm()
            {
                this.FormBorderStyle = FormBorderStyle.None;
                this.Bounds = GetAllScreensBounds();
                this.BackColor = Color.Black;
                this.Opacity = 0.3;
                this.TopMost = true;
                this.ShowInTaskbar = false;
                this.DoubleBuffered = true;
                this.Cursor = Cursors.Cross;
                this.Text = "拖动鼠标选择区域,ESC取消";

                this.MouseDown += RegionSelectForm_MouseDown;
                this.MouseMove += RegionSelectForm_MouseMove;
                this.MouseUp += RegionSelectForm_MouseUp;
                this.Paint += RegionSelectForm_Paint;
                this.KeyDown += RegionSelectForm_KeyDown;
            }

            private Rectangle GetAllScreensBounds()
            {
                int left = int.MaxValue, top = int.MaxValue;
                int right = int.MinValue, bottom = int.MinValue;
                foreach (Screen screen in Screen.AllScreens)
                {
                    left = Math.Min(left, screen.Bounds.Left);
                    top = Math.Min(top, screen.Bounds.Top);
                    right = Math.Max(right, screen.Bounds.Right);
                    bottom = Math.Max(bottom, screen.Bounds.Bottom);
                }
                return new Rectangle(left, top, right - left, bottom - top);
            }

            private void RegionSelectForm_MouseDown(object sender, MouseEventArgs e)
            {
                if (e.Button == MouseButtons.Left)
                {
                    _isSelecting = true;
                    _startPoint = Control.MousePosition;
                    _endPoint = _startPoint;
                }
            }

            private void RegionSelectForm_MouseMove(object sender, MouseEventArgs e)
            {
                if (_isSelecting)
                {
                    _endPoint = Control.MousePosition;
                    this.Invalidate();
                }
            }

            private void RegionSelectForm_MouseUp(object sender, MouseEventArgs e)
            {
                if (e.Button == MouseButtons.Left && _isSelecting)
                {
                    _isSelecting = false;
                    _endPoint = Control.MousePosition;

                    int left = Math.Min(_startPoint.X, _endPoint.X);
                    int top = Math.Min(_startPoint.Y, _endPoint.Y);
                    int right = Math.Max(_startPoint.X, _endPoint.X);
                    int bottom = Math.Max(_endPoint.Y, _startPoint.Y);
                    
                    SelectedRect = new RECT(left, top, right, bottom);
                    this.DialogResult = DialogResult.OK;
                    this.Close();
                }
            }

            private void RegionSelectForm_Paint(object sender, PaintEventArgs e)
            {
                if (_isSelecting)
                {
                    Point clientStart = this.PointToClient(_startPoint);
                    Point clientEnd = this.PointToClient(_endPoint);

                    int x = Math.Min(clientStart.X, clientEnd.X);
                    int y = Math.Min(clientStart.Y, clientEnd.Y);
                    int w = Math.Abs(clientEnd.X - clientStart.X);
                    int h = Math.Abs(clientEnd.Y - clientStart.Y);

                    using (Pen pen = new Pen(Color.Lime, 3))
                    {
                        e.Graphics.DrawRectangle(pen, x, y, w, h);
                    }
                }
            }

            private void RegionSelectForm_KeyDown(object sender, KeyEventArgs e)
            {
                if (e.KeyCode == Keys.Escape)
                {
                    this.DialogResult = DialogResult.Cancel;
                    this.Close();
                }
            }
        }
        #endregion

        #region Win32 API
        [StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;

            public RECT(int left, int top, int right, int bottom)
            {
                Left = left;
                Top = top;
                Right = right;
                Bottom = bottom;
            }
        }

        private static class NativeMethods
        {
            [DllImport("gdi32.dll")]
            public static extern IntPtr CreateDC(string lpszDriver, string lpszDevice, string lpszOutput, IntPtr lpInitData);
            
            [DllImport("gdi32.dll")]
            public static extern IntPtr CreateCompatibleDC(IntPtr hdc);
            
            [DllImport("gdi32.dll")]
            public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
            
            [DllImport("gdi32.dll")]
            public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
            
            [DllImport("gdi32.dll")]
            public static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, uint dwRop);
            
            [DllImport("gdi32.dll")]
            public static extern bool DeleteObject(IntPtr hObject);
            
            [DllImport("gdi32.dll")]
            public static extern bool DeleteDC(IntPtr hdc);
            
            [DllImport("gdi32.dll")]
            public static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
        }
        #endregion
    }
}

问题解决,感谢豆包大力支持!

上一篇: IIS 10 应用程序池内置帐户ApplicationPoolIdentity和NetworkService的区别
下一篇: 这是最新一篇日志
文章来自: 本站原创
引用通告: 查看所有引用 | 我要引用此文章
Tags:
最新日志:
评论: 0 | 引用: 0 | 查看次数: 22
发表评论
登录后再发表评论!