平台调用gdi32实现屏幕捕获等任务

捕获屏幕信息很多,windows上你可以用平台调用的方式去Dllimport gdi32.dll 来实现功能。你也可以使用DXGI 交互,以及WGC的方式。

​ Windows Graphics Capture (WGC) 是一个非常明智的决定。它是在现代 Windows 平台上进行高性能屏幕采集的最佳方式。而直接调用 gdi32.dll 中的 Win32 API 来进行屏幕采集,是了解底层工作原理的最佳方式。这正是 System.Drawing.Graphics.CopyFromScreen 在幕后所做的事情。

平台调用 (P/Invoke) 详解

什么是平台调用?

平台调用 (Platform Invocation Services, P/Invoke) 是 .NET Framework 和 .NET Core/.NET 5+ 提供的一种机制,允许托管代码(如 C#)调用非托管代码(如 Windows API、C/C++ 库等)。

P/Invoke 的工作原理

  1. 托管到非托管的桥接:P/Invoke 在托管代码和非托管代码之间建立桥梁
  2. 数据封送 (Marshaling):自动处理数据类型转换和内存管理
  3. 调用约定处理:处理不同的函数调用约定(如 stdcall、cdecl 等)
  4. 异常处理:将非托管异常转换为托管异常

P/Invoke 的优势和劣势

优势:

  • 直接访问 Windows API 和系统功能
  • 性能开销相对较小
  • 可以调用现有的 C/C++ 库
  • 提供对底层系统功能的完全控制

劣势:

  • 需要手动管理非托管资源
  • 容易出现内存泄漏
  • 跨平台兼容性差
  • 调试困难,错误信息不够友好

GDI (Graphics Device Interface) 详解

什么是 GDI?

​ GDI (Graphics Device Interface) 是 Windows 操作系统的核心图形子系统,负责处理所有图形输出操作。它是 Windows 图形架构的基础层。

GDI 的核心概念

1. 设备上下文 (Device Context, DC)

  • 定义:DC 是 Windows 中表示绘图表面的抽象概念
  • 作用:提供绘图操作的接口,包括画笔、画刷、字体等
  • 类型:
    • 屏幕 DC:直接绘制到屏幕
    • 内存 DC:在内存中创建虚拟绘图表面
    • 打印机 DC:用于打印输出
    • 位图 DC:用于位图操作

2. 句柄 (Handle)

  • 定义:Windows 中用于标识系统资源的唯一标识符
  • 类型:
    • HDC:设备上下文句柄
    • HBITMAP:位图句柄
    • HPEN:画笔句柄
    • HBRUSH:画刷句柄

3. 位图 (Bitmap)

  • 定义:存储像素数据的图形对象
  • 格式:支持多种颜色深度(1位、4位、8位、16位、24位、32位)
  • 操作:创建、选择、复制、删除

GDI 函数分类

设备上下文管理

  • GetDC() / ReleaseDC():获取/释放设备上下文
  • CreateCompatibleDC() / DeleteDC():创建/删除兼容 DC
  • SelectObject():选择 GDI 对象到 DC

位图操作

  • CreateCompatibleBitmap():创建兼容位图
  • BitBlt():位块传输,执行像素复制
  • GetDIBits():获取位图像素数据
  • DeleteObject():删除 GDI 对象

绘图操作

  • SetPixel() / GetPixel():设置/获取像素颜色
  • LineTo() / MoveToEx():绘制直线
  • Rectangle() / Ellipse():绘制几何图形

屏幕捕获的核心原理

我们将通过 P/Invoke (Platform Invocation Services) 来调用 gdi32.dll 中的原生函数。整个流程可以概括为:

1. 获取屏幕的设备上下文 (DC)

  • 原理:DC 是 Windows 中表示绘图表面的抽象概念,可以理解为指向一个绘图表面的"画笔"或"句柄"
  • 实现:使用 GetDC(IntPtr.Zero) 获取整个屏幕的设备上下文
  • 注意:IntPtr.Zero 表示获取整个屏幕的 DC,而不是特定窗口的 DC

2. 创建内存中的 DC 和位图

  • 原理:在内存中创建一个与屏幕兼容的"画布"(Memory DC) 和"画纸"(Bitmap)
  • 实现:
    • CreateCompatibleDC() 创建兼容的内存 DC
    • CreateCompatibleBitmap() 创建与屏幕兼容的位图
  • 优势:内存操作比直接屏幕操作更快,且不会影响屏幕显示

3. 执行位块传输 (BitBlt)

  • 原理:将屏幕 DC 的内容,通过"位块传输"的方式,完整地复制到内存 DC 上的位图中
  • 实现:BitBlt() 函数执行像素级的复制操作
  • 参数说明:
    • 目标 DC、目标坐标、尺寸
    • 源 DC、源坐标
    • 光栅操作码 (ROP)

4. 提取像素数据

  • 原理:从内存中的 GDI 位图对象中,提取出原始的像素数据到一个 C# 的字节数组 (byte[]) 中
  • 实现:使用 GetDIBits() 函数获取位图像素数据
  • 数据格式:通常使用 32 位 BGRA 格式(蓝、绿、红、透明度)

5. 释放资源

  • 重要性:(最重要的一步) 必须手动释放所有申请的 GDI 资源句柄,否则会造成严重的内存泄漏
  • 释放顺序:
    1. 恢复原始位图选择
    2. 删除位图对象
    3. 删除内存 DC
    4. 释放屏幕 DC

P/Invoke 函数声明详解

首先我们需要进行平台调用来导入 dll。以下是详细的函数声明和说明:

public static class ColorClass
{
    // ========== GDI32.dll 函数声明 ==========
    
    /// <summary>
    /// 位块传输函数 - 执行像素复制操作
    /// </summary>
    /// <param name="hdcDest">目标设备上下文句柄</param>
    /// <param name="nXDest">目标矩形左上角X坐标</param>
    /// <param name="nYDest">目标矩形左上角Y坐标</param>
    /// <param name="nWidth">复制区域的宽度</param>
    /// <param name="nHeight">复制区域的高度</param>
    /// <param name="hdcSrc">源设备上下文句柄</param>
    /// <param name="nXSrc">源矩形左上角X坐标</param>
    /// <param name="nYSrc">源矩形左上角Y坐标</param>
    /// <param name="dwRop">光栅操作码,定义如何组合源和目标像素</param>
    /// <returns>成功返回true,失败返回false</returns>
    [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);

    /// <summary>
    /// 创建与指定设备上下文兼容的位图
    /// </summary>
    /// <param name="hdc">设备上下文句柄</param>
    /// <param name="nWidth">位图宽度(像素)</param>
    /// <param name="nHeight">位图高度(像素)</param>
    /// <returns>成功返回位图句柄,失败返回IntPtr.Zero</returns>
    [DllImport("gdi32.dll")]
    public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);

    /// <summary>
    /// 创建与指定设备上下文兼容的内存设备上下文
    /// </summary>
    /// <param name="hdc">参考设备上下文句柄</param>
    /// <returns>成功返回内存DC句柄,失败返回IntPtr.Zero</returns>
    [DllImport("gdi32.dll")]
    public static extern IntPtr CreateCompatibleDC(IntPtr hdc);

    /// <summary>
    /// 删除指定的设备上下文
    /// </summary>
    /// <param name="hdc">要删除的设备上下文句柄</param>
    /// <returns>成功返回true,失败返回false</returns>
    [DllImport("gdi32.dll")]
    public static extern bool DeleteDC(IntPtr hdc);

    /// <summary>
    /// 删除GDI对象(位图、画笔、画刷等)
    /// </summary>
    /// <param name="hObject">要删除的GDI对象句柄</param>
    /// <returns>成功返回true,失败返回false</returns>
    [DllImport("gdi32.dll")]
    public static extern bool DeleteObject(IntPtr hObject);

    /// <summary>
    /// 将GDI对象选择到指定的设备上下文中
    /// </summary>
    /// <param name="hdc">设备上下文句柄</param>
    /// <param name="hgdiobj">要选择的GDI对象句柄</param>
    /// <returns>返回之前选择的同类型对象句柄</returns>
    [DllImport("gdi32.dll")]
    public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

    /// <summary>
    /// 获取位图的像素数据
    /// </summary>
    /// <param name="hdc">设备上下文句柄</param>
    /// <param name="hbmp">位图句柄</param>
    /// <param name="uStartScan">开始扫描的行号</param>
    /// <param name="cScanLines">要扫描的行数</param>
    /// <param name="lpvBits">接收像素数据的缓冲区</param>
    /// <param name="lpbi">位图信息结构</param>
    /// <param name="uUsage">颜色表使用方式</param>
    /// <returns>成功返回扫描行数,失败返回0</returns>
    [DllImport("gdi32.dll")]
    public static extern int GetDIBits(IntPtr hdc, IntPtr hbmp, uint uStartScan, uint cScanLines, [Out] byte[] lpvBits, ref BITMAPINFO lpbi, uint uUsage);

    // ========== USER32.dll 函数声明 ==========
    
    /// <summary>
    /// 获取指定窗口的设备上下文
    /// </summary>
    /// <param name="hWnd">窗口句柄,IntPtr.Zero表示整个屏幕</param>
    /// <returns>成功返回DC句柄,失败返回IntPtr.Zero</returns>
    [DllImport("user32.dll")]
    public static extern IntPtr GetDC(IntPtr hWnd);

    /// <summary>
    /// 释放设备上下文
    /// </summary>
    /// <param name="hWnd">窗口句柄</param>
    /// <param name="hDC">要释放的设备上下文句柄</param>
    /// <returns>成功返回1,失败返回0</returns>
    [DllImport("user32.dll")]
    public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);

    #region 常量和结构体定义
    
    /// <summary>
    /// 光栅操作码 - 直接复制源到目标
    /// </summary>
    public const uint SRCCOPY = 0x00CC0020;
    
    /// <summary>
    /// DIB颜色表使用方式 - 使用RGB颜色值
    /// </summary>
    public const uint DIB_RGB_COLORS = 0;

    /// <summary>
    /// 位图信息结构体
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public struct BITMAPINFO
    {
        public BITMAPINFOHEADER bmiHeader;  // 位图头信息
        public RGBQUAD bmiColors;           // 颜色表(对于调色板位图)
    }

    /// <summary>
    /// 位图头信息结构体
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public struct BITMAPINFOHEADER
    {
        public uint biSize;           // 结构体大小
        public int biWidth;           // 位图宽度(像素)
        public int biHeight;          // 位图高度(像素,负数表示从上到下)
        public ushort biPlanes;       // 颜色平面数(必须为1)
        public ushort biBitCount;     // 每像素位数(1,4,8,16,24,32)
        public uint biCompression;    // 压缩方式(0=无压缩)
        public uint biSizeImage;      // 图像数据大小(字节)
        public int biXPelsPerMeter;   // 水平分辨率(像素/米)
        public int biYPelsPerMeter;   // 垂直分辨率(像素/米)
        public uint biClrUsed;        // 使用的颜色数
        public uint biClrImportant;   // 重要颜色数
    }

    /// <summary>
    /// RGB颜色四元组结构体
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public struct RGBQUAD
    {
        public byte rgbBlue;      // 蓝色分量
        public byte rgbGreen;     // 绿色分量
        public byte rgbRed;       // 红色分量
        public byte rgbReserved;  // 保留字段
    }
    #endregion
}

P/Invoke 声明要点解析

1. DllImport 特性

  • 作用:告诉 .NET 运行时从哪个 DLL 导入函数
  • 参数:DLL 文件名(如 "gdi32.dll")
  • 可选参数:
    • EntryPoint:指定函数名(如果 C# 方法名与 DLL 函数名不同)
    • CallingConvention:指定调用约定(默认是 Winapi)
    • CharSet:指定字符集(默认是 Ansi)

2. extern 关键字

  • 作用:声明这是一个外部函数,由非托管代码实现
  • 必须与 static 一起使用

3. 数据类型映射

  • IntPtr:对应 Windows 中的句柄类型(HDC、HBITMAP 等)
  • int:对应 Windows 中的 32 位整数
  • uint:对应 Windows 中的无符号 32 位整数
  • byte:对应 Windows 中的字节数组缓冲区

4. 结构体布局

  • StructLayout(LayoutKind.Sequential):确保结构体成员按顺序排列
  • 作用:保证与 C/C++ 结构体的内存布局一致

屏幕捕获方法封装详解

然后是封装颜色获取方法,这里我们将详细分析每个步骤:

// 方法封装
public static class ColorService
{
    public static byte[]? CaptureScreenPixels(out int width, out int height, out int stride)
    {
        // 1. 获取屏幕尺寸
        width = (int)SystemParameters.PrimaryScreenWidth;
        height = (int)SystemParameters.PrimaryScreenHeight;
        stride = 0;

        // GDI 句柄,必须在 finally 中释放
        IntPtr screenDc = IntPtr.Zero;
        IntPtr memDc = IntPtr.Zero;
        IntPtr hBitmap = IntPtr.Zero;
        IntPtr hOldBitmap = IntPtr.Zero;

        try
        {
            // 2. 获取屏幕DC
            screenDc = GetDC(IntPtr.Zero);
            if (screenDc == IntPtr.Zero) return null;

            // 3. 创建兼容的内存DC和位图
            memDc = CreateCompatibleDC(screenDc);
            if (memDc == IntPtr.Zero) return null;

            hBitmap = CreateCompatibleBitmap(screenDc, width, height);
            if (hBitmap == IntPtr.Zero) return null;

            // 4. 将位图选入内存DC
            hOldBitmap = SelectObject(memDc, hBitmap);

            // 5. 将屏幕内容复制到内存DC
            BitBlt(memDc, 0, 0, width, height, screenDc, 0, 0, SRCCOPY);

            // 6. 从GDI位图中提取像素数据
            BITMAPINFOHEADER bmi = new BITMAPINFOHEADER
            {
                biSize = (uint)Marshal.SizeOf(typeof(BITMAPINFOHEADER)),
                biWidth = width,
                biHeight = -height, // 使用负数高度,得到从上到下的“Top-Down”位图
                biPlanes = 1,
                biBitCount = 32, // 32位 BGRA
                biCompression = 0 // BI_RGB
            };

            int bytesPerPixel = 4;
            stride = width * bytesPerPixel;
            byte[] pixels = new byte[stride * height];

            var result = GetDIBits(memDc, hBitmap, 0, (uint)height, pixels,
                ref Unsafe.As<BITMAPINFOHEADER, BITMAPINFO>(ref bmi), DIB_RGB_COLORS);

            return result > 0 ? pixels : null;
        }
        finally
        {
            // 7. 关键:清理所有GDI资源,防止内存泄漏
            if (hOldBitmap != IntPtr.Zero)
            {
                SelectObject(memDc, hOldBitmap);
            }
            if (hBitmap != IntPtr.Zero)
            {
                DeleteObject(hBitmap);
            }
            if (memDc != IntPtr.Zero)
            {
                DeleteDC(memDc);
            }
            if (screenDc != IntPtr.Zero)
            {
                ReleaseDC(IntPtr.Zero, screenDc);
            }
        }
    }

ViewModel 调用实现

最后是函数调用,这里展示如何在 MVVM 模式中使用屏幕捕获功能:

using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Animation.Services;
using Wpf.Ui;
using System.Windows;
using System.Windows.Media;

namespace Animation.Pages.ColorCollecter;

/// <summary>
/// 颜色采集器 ViewModel
/// 演示如何使用 GDI32 进行屏幕捕获和颜色提取
/// </summary>
public partial class ColorCollecterViewModel : ViewModel
{
    #region 属性定义
    
    /// <summary>
    /// 采集到的颜色画刷
    /// </summary>
    [ObservableProperty]
    private SolidColorBrush _collectedColor = new SolidColorBrush(Colors.LightGray);

    /// <summary>
    /// 颜色信息文本
    /// </summary>
    [ObservableProperty]
    private string _colorInfo = "点击采集颜色";
    
    #endregion

    #region 命令实现
    
    /// <summary>
    /// 执行颜色采集命令
    /// 演示屏幕捕获和像素数据解析的完整流程
    /// </summary>
    [RelayCommand]
    public void ExcuteColorCollecter()
    {
        Console.WriteLine("开始颜色采集...");
        
        try
        {
            // ========== 第一步:调用屏幕捕获服务 ==========
            var pixels = ColorService.CaptureScreenPixels(out int width, out int height, out int stride);
            
            if (pixels != null)
            {
                // ========== 第二步:选择采样点 ==========
                // 示例:获取屏幕中央像素的颜色
                int x = width / 2;   // 屏幕中央 X 坐标
                int y = height / 2;  // 屏幕中央 Y 坐标

                // ========== 第三步:计算像素索引 ==========
                // 像素格式是 BGRA (Blue, Green, Red, Alpha)
                // 每个像素占用 4 字节,按行存储
                int index = (y * stride) + (x * 4);

                // ========== 第四步:提取颜色分量 ==========
                byte blue = pixels[index];      // 蓝色分量
                byte green = pixels[index + 1]; // 绿色分量
                byte red = pixels[index + 2];   // 红色分量
                byte alpha = pixels[index + 3]; // 透明度分量

                // ========== 第五步:创建颜色对象 ==========
                // 注意:WPF 的 Color.FromArgb 参数顺序是 (Alpha, Red, Green, Blue)
                CollectedColor = new SolidColorBrush(Color.FromArgb(alpha, red, green, blue));
                
                // ========== 第六步:更新UI显示 ==========
                ColorInfo = $"RGB({red}, {green}, {blue}) | 位置: ({x}, {y})";
                
                Console.WriteLine($"成功采集颜色: R={red}, G={green}, B={blue}, A={alpha}");
                Console.WriteLine($"屏幕尺寸: {width}x{height}, 步长: {stride}");
            }
            else
            {
                // ========== 错误处理 ==========
                MessageBox.Show("屏幕捕获失败!请检查系统权限或重试。", "错误", 
                    MessageBoxButton.OK, MessageBoxImage.Error);
                Console.WriteLine("屏幕捕获失败");
            }
        }
        catch (Exception ex)
        {
            // ========== 异常处理 ==========
            MessageBox.Show($"颜色采集过程中发生错误:{ex.Message}", "异常", 
                MessageBoxButton.OK, MessageBoxImage.Warning);
            Console.WriteLine($"颜色采集异常: {ex.Message}");
        }
    }

    /// <summary>
    /// 清空颜色命令
    /// 重置UI状态到初始状态
    /// </summary>
    [RelayCommand]
    public void ClearColor()
    {
        CollectedColor = new SolidColorBrush(Colors.LightGray);
        ColorInfo = "点击采集颜色";
        Console.WriteLine("颜色已清空,UI状态已重置");
    }
    
    #endregion
}

ViewModel 实现解析

1. MVVM 模式应用

  • ObservableProperty:使用 CommunityToolkit.Mvvm 的源生成器自动生成属性通知
  • RelayCommand:自动生成命令实现,支持异步操作
  • 数据绑定:属性变化自动通知UI更新

2. 屏幕捕获集成

  • 服务调用:调用 ColorService.CaptureScreenPixels 获取像素数据
  • 错误处理:完整的异常处理和用户友好的错误提示
  • 日志记录:详细的控制台输出便于调试

3. 像素数据解析

  • 坐标计算:计算屏幕中央像素的坐标
  • 索引计算:根据步长和坐标计算像素在字节数组中的索引
  • 颜色提取:按 BGRA 格式提取颜色分量

4. UI 更新机制

  • 颜色显示:使用 SolidColorBrush 显示采集到的颜色
  • 信息展示:显示 RGB 值和坐标信息
  • 状态管理:提供清空功能重置UI状态

扩展功能建议

1. 多采样点支持

// 可以扩展为支持多个采样点
public void SampleMultiplePoints(Point[] points)
{
    // 实现多个点的颜色采样
}

2. 颜色格式转换

// 支持不同颜色格式的转换
public string GetHexColor(Color color)
{
    return $"#{color.R:X2}{color.G:X2}{color.B:X2}";
}

3. 历史记录功能

// 保存颜色采集历史
public ObservableCollection<ColorHistoryItem> ColorHistory { get; set; }

总结与最佳实践

关键技术要点

  1. P/Invoke 平台调用
    • 正确声明外部函数和结构体
    • 处理数据类型映射和内存布局
    • 管理非托管资源的生命周期
  2. GDI 图形编程
    • 理解设备上下文 (DC) 的概念
    • 掌握位图操作和像素数据传输
    • 正确处理 GDI 资源释放
  3. 屏幕捕获流程
    • 获取屏幕设备上下文
    • 创建兼容的内存 DC 和位图
    • 执行位块传输操作
    • 提取像素数据

性能优化建议

  1. 内存管理
    • 及时释放 GDI 资源,避免内存泄漏
    • 使用 Unsafe.As 进行高效的类型转换
    • 考虑使用对象池减少内存分配
  2. 错误处理
    • 检查每个 GDI 函数的返回值
    • 使用 try-finally 确保资源清理
    • 提供用户友好的错误提示
  3. 多显示器支持
    • 使用 SystemParameters.VirtualScreenWidth/Height 获取虚拟屏幕尺寸
    • 考虑使用 EnumDisplayMonitors 枚举所有显示器

安全注意事项

  1. 权限要求
    • 某些系统可能需要管理员权限
    • 考虑使用 UAC 提示或权限提升
  2. 隐私保护
    • 明确告知用户屏幕捕获的目的
    • 避免在后台静默捕获屏幕内容
  3. 跨平台兼容性
    • GDI32 仅适用于 Windows 平台
    • 考虑使用跨平台的替代方案(如 SkiaSharp)

替代技术方案

  1. Windows Graphics Capture (WGC)
    • 更现代的屏幕捕获 API
    • 更好的性能和功能支持
    • 支持窗口级别的捕获
  2. DirectX/DXGI
    • 硬件加速的图形处理
    • 更低的 CPU 占用率
    • 支持更复杂的图形操作
  3. System.Drawing.Graphics.CopyFromScreen
    • .NET 内置的屏幕捕获方法
    • 更简单的 API,但性能较低
    • 适合简单的屏幕截图需求

这个完整的实现展示了如何使用 P/Invoke 调用 GDI32 进行屏幕捕获,涵盖了从底层 API 调用到高级 MVVM 模式应用的完整技术栈。