平台调用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 的工作原理
- 托管到非托管的桥接:P/Invoke 在托管代码和非托管代码之间建立桥梁
- 数据封送 (Marshaling):自动处理数据类型转换和内存管理
- 调用约定处理:处理不同的函数调用约定(如 stdcall、cdecl 等)
- 异常处理:将非托管异常转换为托管异常
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():创建/删除兼容 DCSelectObject():选择 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()创建兼容的内存 DCCreateCompatibleBitmap()创建与屏幕兼容的位图
- 优势:内存操作比直接屏幕操作更快,且不会影响屏幕显示
3. 执行位块传输 (BitBlt)
- 原理:将屏幕 DC 的内容,通过"位块传输"的方式,完整地复制到内存 DC 上的位图中
- 实现:
BitBlt()函数执行像素级的复制操作 - 参数说明:
- 目标 DC、目标坐标、尺寸
- 源 DC、源坐标
- 光栅操作码 (ROP)
4. 提取像素数据
- 原理:从内存中的 GDI 位图对象中,提取出原始的像素数据到一个 C# 的字节数组 (
byte[]) 中 - 实现:使用
GetDIBits()函数获取位图像素数据 - 数据格式:通常使用 32 位 BGRA 格式(蓝、绿、红、透明度)
5. 释放资源
- 重要性:(最重要的一步) 必须手动释放所有申请的 GDI 资源句柄,否则会造成严重的内存泄漏
- 释放顺序:
- 恢复原始位图选择
- 删除位图对象
- 删除内存 DC
- 释放屏幕 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; }
总结与最佳实践
关键技术要点
- P/Invoke 平台调用
- 正确声明外部函数和结构体
- 处理数据类型映射和内存布局
- 管理非托管资源的生命周期
- GDI 图形编程
- 理解设备上下文 (DC) 的概念
- 掌握位图操作和像素数据传输
- 正确处理 GDI 资源释放
- 屏幕捕获流程
- 获取屏幕设备上下文
- 创建兼容的内存 DC 和位图
- 执行位块传输操作
- 提取像素数据
性能优化建议
- 内存管理
- 及时释放 GDI 资源,避免内存泄漏
- 使用
Unsafe.As进行高效的类型转换 - 考虑使用对象池减少内存分配
- 错误处理
- 检查每个 GDI 函数的返回值
- 使用 try-finally 确保资源清理
- 提供用户友好的错误提示
- 多显示器支持
- 使用
SystemParameters.VirtualScreenWidth/Height获取虚拟屏幕尺寸 - 考虑使用
EnumDisplayMonitors枚举所有显示器
- 使用
安全注意事项
- 权限要求
- 某些系统可能需要管理员权限
- 考虑使用 UAC 提示或权限提升
- 隐私保护
- 明确告知用户屏幕捕获的目的
- 避免在后台静默捕获屏幕内容
- 跨平台兼容性
- GDI32 仅适用于 Windows 平台
- 考虑使用跨平台的替代方案(如 SkiaSharp)
替代技术方案
- Windows Graphics Capture (WGC)
- 更现代的屏幕捕获 API
- 更好的性能和功能支持
- 支持窗口级别的捕获
- DirectX/DXGI
- 硬件加速的图形处理
- 更低的 CPU 占用率
- 支持更复杂的图形操作
- System.Drawing.Graphics.CopyFromScreen
- .NET 内置的屏幕捕获方法
- 更简单的 API,但性能较低
- 适合简单的屏幕截图需求
这个完整的实现展示了如何使用 P/Invoke 调用 GDI32 进行屏幕捕获,涵盖了从底层 API 调用到高级 MVVM 模式应用的完整技术栈。