Windows GDI 窗口与 Direct3D 屏幕截图
前言
Windows 上,屏幕截图一般是调用 win32 api 完成的,如果 C# 想实现截图功能,就需要封装相关 api。在 Windows 上,主要图形接口有 GDI 和 DirectX。GDI 接口比较灵活,可以截取指定窗口,哪怕窗口被遮挡或位于显示区域外,但兼容性较低,无法截取 DX 接口输出的画面。DirectX 是高性能图形接口(当然还有其他功能,与本文无关,忽略不计),主要作为游戏图形接口使用,灵活性较低,无法指定截取特定窗口(或者只是我不会吧),但是兼容性较高,可以截取任何输出到屏幕的内容,根据情况使用。
正文
以下代码使用了 C# 8.0 的新功能,只能使用 VS 2019 编译,如果需要在老版本 VS 使用,需要自行改造。
GDI
用静态类简单封装 GDI 接口并调用接口截图。
1 public static class CaptureWindow2 {3 #region 类4 /// <summary>5 /// Helper class containing User32 API functions6 /// </summary>7 private class User328 {9 [StructLayout(LayoutKind.Sequential)]10 public struct RECT11 {12 public int left;13 public int top;14 public int right;15 public int bottom;16 }17 [DllImport("user32.dll")]18 public static extern IntPtr GetDesktopWindow();19 [DllImport("user32.dll")]20 public static extern IntPtr GetWindowDC(IntPtr hWnd);21 [DllImport("user32.dll")]22 public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);23 [DllImport("user32.dll")]24 public static extern IntPtr GetWindowRect(IntPtr hWnd, ref RECT rect);25 26 [DllImport("user32.dll", EntryPoint = "FindWindow", CharSet = CharSet.Unicode)]27 public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);28 }29 30 private class Gdi3231 {32 33 public const int SRCCOPY = 0x00CC0020; // BitBlt dwRop parameter34 [DllImport("gdi32.dll")]35 public static extern bool BitBlt(IntPtr hObject, int nXDest, int nYDest,36 int nWidth, int nHeight, IntPtr hObjectSource,37 int nXSrc, int nYSrc, int dwRop);38 [DllImport("gdi32.dll")]39 public static extern IntPtr CreateCompatibleBitmap(IntPtr hDC, int nWidth,40 int nHeight);41 [DllImport("gdi32.dll")]42 public static extern IntPtr CreateCompatibleDC(IntPtr hDC);43 [DllImport("gdi32.dll")]44 public static extern bool DeleteDC(IntPtr hDC);45 [DllImport("gdi32.dll")]46 public static extern bool DeleteObject(IntPtr hObject);47 [DllImport("gdi32.dll")]48 public static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);49 }50 #endregion51 52 /// <summary>53 /// 根据句柄截图54 /// </summary>55 /// <param name="hWnd">句柄</param>56 /// <returns></returns>57 public static Image ByHwnd(IntPtr hWnd)58 {59 // get te hDC of the target window60 IntPtr hdcSrc = User32.GetWindowDC(hWnd);61 // get the size62 User32.RECT windowRect = new User32.RECT();63 User32.GetWindowRect(hWnd, ref windowRect);64 int width = windowRect.right - windowRect.left;65 int height = windowRect.bottom - windowRect.top;66 // create a device context we can copy to67 IntPtr hdcDest = Gdi32.CreateCompatibleDC(hdcSrc);68 // create a bitmap we can copy it to,69 // using GetDeviceCaps to get the width/height70 IntPtr hBitmap = Gdi32.CreateCompatibleBitmap(hdcSrc, width, height);71 // select the bitmap object72 IntPtr hOld = Gdi32.SelectObject(hdcDest, hBitmap);73 // bitblt over74 Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, Gdi32.SRCCOPY);75 // restore selection76 Gdi32.SelectObject(hdcDest, hOld);77 // clean up78 Gdi32.DeleteDC(hdcDest);79 User32.ReleaseDC(hWnd, hdcSrc);80 // get a .NET image object for it81 Image img = Image.FromHbitmap(hBitmap);82 // free up the Bitmap object83 Gdi32.DeleteObject(hBitmap);84 return img;85 }86 87 /// <summary>88 /// 根据窗口名称截图89 /// </summary>90 /// <param name="windowName">窗口名称</param>91 /// <returns></returns>92 public static Image ByName(string windowName)93 {94 IntPtr handle = User32.FindWindow(null, windowName);95 IntPtr hdcSrc = User32.GetWindowDC(handle);96 User32.RECT windowRect = new User32.RECT();97 User32.GetWindowRect(handle, ref windowRect);98 int width = windowRect.right - windowRect.left;99 int height = windowRect.bottom - windowRect.top;
100 IntPtr hdcDest = Gdi32.CreateCompatibleDC(hdcSrc);
101 IntPtr hBitmap = Gdi32.CreateCompatibleBitmap(hdcSrc, width, height);
102 IntPtr hOld = Gdi32.SelectObject(hdcDest, hBitmap);
103 Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, Gdi32.SRCCOPY);
104 Gdi32.SelectObject(hdcDest, hOld);
105 Gdi32.DeleteDC(hdcDest);
106 User32.ReleaseDC(handle, hdcSrc);
107 Image img = Image.FromHbitmap(hBitmap);
108 Gdi32.DeleteObject(hBitmap);
109 return img;
110 }
111 }
Direct3D
安装 nuget 包 SharpDX.Direct3D11,简单封装。此处使用 D3D 11 接口封装,对多显卡多显示器的情况只能截取主显卡主显示器画面,如需截取其他屏幕,需稍微改造构造函数。截屏可能失败,也可能截取到黑屏,已经在返回值中提示。
将 DX 截屏转换成 C# 图像使用了指针操作,一方面可以提升性能,一方面也是因为都用 DX 了,基本上是很难避免底层操作了,那就一不做二不休,多利用一下。
1 public class DirectXScreenCapturer : IDisposable2 {3 private Factory1 factory;4 private Adapter1 adapter;5 private SharpDX.Direct3D11.Device device;6 private Output output;7 private Output1 output1;8 private Texture2DDescription textureDesc;9 //2D 纹理,存储截屏数据10 private Texture2D screenTexture;11 12 public DirectXScreenCapturer()13 {14 // 获取输出设备(显卡、显示器),这里是主显卡和主显示器15 factory = new Factory1();16 adapter = factory.GetAdapter1(0);17 device = new SharpDX.Direct3D11.Device(adapter);18 output = adapter.GetOutput(0);19 output1 = output.QueryInterface<Output1>();20 21 //设置纹理信息,供后续使用(截图大小和质量)22 textureDesc = new Texture2DDescription23 {24 CpuAccessFlags = CpuAccessFlags.Read,25 BindFlags = BindFlags.None,26 Format = Format.B8G8R8A8_UNorm,27 Width = output.Description.DesktopBounds.Right,28 Height = output.Description.DesktopBounds.Bottom,29 OptionFlags = ResourceOptionFlags.None,30 MipLevels = 1,31 ArraySize = 1,32 SampleDescription = { Count = 1, Quality = 0 },33 Usage = ResourceUsage.Staging34 };35 36 screenTexture = new Texture2D(device, textureDesc);37 }38 39 public Result ProcessFrame(Action<DataBox, Texture2DDescription> processAction, int timeoutInMilliseconds = 5)40 {41 //截屏,可能失败42 using OutputDuplication duplicatedOutput = output1.DuplicateOutput(device);43 var result = duplicatedOutput.TryAcquireNextFrame(timeoutInMilliseconds, out OutputDuplicateFrameInformation duplicateFrameInformation, out SharpDX.DXGI.Resource screenResource);44 45 if (!result.Success) return result;46 47 using Texture2D screenTexture2D = screenResource.QueryInterface<Texture2D>();48 49 //复制数据50 device.ImmediateContext.CopyResource(screenTexture2D, screenTexture);51 DataBox mapSource = device.ImmediateContext.MapSubresource(screenTexture, 0, MapMode.Read, SharpDX.Direct3D11.MapFlags.None);52 53 processAction?.Invoke(mapSource, textureDesc);54 55 //释放资源56 device.ImmediateContext.UnmapSubresource(screenTexture, 0);57 screenResource.Dispose();58 duplicatedOutput.ReleaseFrame();59 60 return result;61 }62 63 public (Result result, bool isBlackFrame, Image image) GetFrameImage(int timeoutInMilliseconds = 5)64 {65 //生成 C# 用图像66 Bitmap image = new Bitmap(textureDesc.Width, textureDesc.Height, PixelFormat.Format24bppRgb);67 bool isBlack = true;68 var result = ProcessFrame(ProcessImage);69 70 if (!result.Success) image.Dispose();71 72 return (result, isBlack, result.Success ? image : null);73 74 void ProcessImage(DataBox dataBox, Texture2DDescription texture)75 {76 BitmapData data = image.LockBits(new Rectangle(0, 0, texture.Width, texture.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);77 78 unsafe79 {80 byte* dataHead = (byte*)dataBox.DataPointer.ToPointer();81 82 for (int x = 0; x < texture.Width; x++)83 {84 for (int y = 0; y < texture.Height; y++)85 {86 byte* pixPtr = (byte*)(data.Scan0 + y * data.Stride + x * 3);87 88 int pos = x + y * texture.Width;89 pos *= 4;90 91 byte r = dataHead[pos + 2];92 byte g = dataHead[pos + 1];93 byte b = dataHead[pos + 0];94 95 if (isBlack && (r != 0 || g != 0 || b != 0)) isBlack = false;96 97 pixPtr[0] = b;98 pixPtr[1] = g;99 pixPtr[2] = r;
100 }
101 }
102 }
103
104 image.UnlockBits(data);
105 }
106 }
107
108 #region IDisposable Support
109 private bool disposedValue = false; // 要检测冗余调用
110
111 protected virtual void Dispose(bool disposing)
112 {
113 if (!disposedValue)
114 {
115 if (disposing)
116 {
117 // TODO: 释放托管状态(托管对象)。
118 factory.Dispose();
119 adapter.Dispose();
120 device.Dispose();
121 output.Dispose();
122 output1.Dispose();
123 screenTexture.Dispose();
124 }
125
126 // TODO: 释放未托管的资源(未托管的对象)并在以下内容中替代终结器。
127 // TODO: 将大型字段设置为 null。
128 factory = null;
129 adapter = null;
130 device = null;
131 output = null;
132 output1 = null;
133 screenTexture = null;
134
135 disposedValue = true;
136 }
137 }
138
139 // TODO: 仅当以上 Dispose(bool disposing) 拥有用于释放未托管资源的代码时才替代终结器。
140 // ~DirectXScreenCapturer()
141 // {
142 // // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
143 // Dispose(false);
144 // }
145
146 // 添加此代码以正确实现可处置模式。
147 public void Dispose()
148 {
149 // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
150 Dispose(true);
151 // TODO: 如果在以上内容中替代了终结器,则取消注释以下行。
152 // GC.SuppressFinalize(this);
153 }
154 #endregion
155 }
使用示例
其中使用了窗口枚举辅助类,详细代码请看文章末尾的 Github 项目。支持 .Net Core。
1 static async Task Main(string[] args)2 {3 Console.Write("按任意键开始DX截图……");4 Console.ReadKey();5 6 string path = @"E:\截图测试";7 8 var cancel = new CancellationTokenSource();9 await Task.Run(() =>
10 {
11 Task.Run(() =>
12 {
13 Thread.Sleep(5000);
14 cancel.Cancel();
15 Console.WriteLine("DX截图结束!");
16 });
17 var savePath = $@"{path}\DX";
18 Directory.CreateDirectory(savePath);
19
20 using var dx = new DirectXScreenCapturer();
21 Console.WriteLine("开始DX截图……");
22
23 while (!cancel.IsCancellationRequested)
24 {
25 var (result, isBlackFrame, image) = dx.GetFrameImage();
26 if (result.Success && !isBlackFrame) image.Save($@"{savePath}\{DateTime.Now.Ticks}.jpg", ImageFormat.Jpeg);
27 image?.Dispose();
28 }
29 }, cancel.Token);
30
31 var windows = WindowEnumerator.FindAll();
32 for (int i = 0; i < windows.Count; i++)
33 {
34 var window = windows[i];
35 Console.WriteLine($@"{i.ToString().PadLeft(3, ' ')}. {window.Title}
36 {window.Bounds.X}, {window.Bounds.Y}, {window.Bounds.Width}, {window.Bounds.Height}");
37 }
38
39 var savePath = $@"{path}\Gdi";
40 Directory.CreateDirectory(savePath);
41 Console.WriteLine("开始Gdi窗口截图……");
42
43 foreach (var win in windows)
44 {
45 var image = CaptureWindow.ByHwnd(win.Hwnd);
46 image.Save($@"{savePath}\{win.Title.Substring(win.Title.LastIndexOf(@"\") < 0 ? 0 : win.Title.LastIndexOf(@"\") + 1).Replace("/", "").Replace("*", "").Replace("?", "").Replace("\"", "").Replace(":", "").Replace("<", "").Replace(">", "").Replace("|", "")}.jpg", ImageFormat.Jpeg);
47 image.Dispose();
48 }
49 Console.WriteLine("Gdi窗口截图结束!");
50
51 Console.ReadKey();
52 }
结语
这个示例代码中的 DX 截图只支持 win7 以上版本,xp 是时候退出历史舞台了。代码参考了网上大神的文章,并根据实际情况进行改造,尽可能简化实现和使用代码,展示最简单情况下所必须的代码。如果实际需求比较复杂,可以以这个为底版进行改造。
Windows GDI 窗口与 Direct3D 屏幕截图相关推荐
- WINDOWS高级窗口的客户区域拖动技术及其应用
WINDOWS高级窗口的客户区域拖动技术及其应用 来源:http://dev.21tx.com WINDOWS应用程序窗口一般包括两种:普通窗口和常居顶层的无标题条高级窗口.前者是由WINDOWS内部 ...
- 戏说 Windows GDI (3)
继续讨论之前的问题,包括GDI对象的删除问题,窗口注册与销毁问题,最后用标尺的小例子说明问题 1.GDI对象的删除 由CGDIObject派生类创建的画笔.画刷和其他对象都要占用内存资源,因此在使用完 ...
- MFC和Win32之三___CGdiObject类和windows Gdi对象
小结: 前面讲到的windows窗口对象,在windows下用句柄来代表之,并且用了一个数据结构WNDCLASS(窗口类)来描述之.同理,windows的Gdi对象也有一些句柄来代表之(比如hPen等 ...
- Windows GDI和GDI+编程实例剖析(1)
Windows GDI和GDI+编程实例剖析(1) 作者:宋宝华 e-mail:21cnbao@21cn.com 1.基本概念 GDI在Windows中定义为Graphics Device Inte ...
- 原创]Windows Gdi入门初级应用(VC SDK)
原创]Windows Gdi入门初级应用(VC SDK) 好久没发贴了,今天手痒痒,发一个. GDI的绘图函数基本上都是有状态的,所有的函数都要求一个HDC类型的句柄. 这个HDC的获得有几个途径Be ...
- Windows GDI
Windows GDI 在单任务环境如 MS-DOS 中,运行中的应用程序随时可自由地做它想做的事情,无论是在屏幕上画一条线,重新编写适配器的调色板,还是转换到另一种图像模式.而在窗口化多任务环境如 ...
- w7系统计算机网络密码是什么,解答win7系统共享弹出Windows安全窗口提示输入网络密码的详细教程...
随着电脑的使用率越来越高,我们有时候可能会遇到win7系统共享弹出Windows安全窗口提示输入网络密码问题,如果我们遇到了win7系统共享弹出Windows安全窗口提示输入网络密码的问题,要怎么处理 ...
- Windows - CMD窗口UTF8编码乱码问题的解决!
Windows - CMD窗口UTF8编码乱码问题的解决! 参考文章: (1)Windows - CMD窗口UTF8编码乱码问题的解决! (2)https://www.cnblogs.com/suny ...
- windows cmd 窗口 显示信息慢_你玩过Windows 10新版CMD了吗?
[PConline应用]CMD是Windows里一项经典的命令行工具,很多人认为PowerShell的出现将逐步取代CMD,成为新一代默认命令行.近日微软在Win10官方商店,发布了一款全新的Wind ...
最新文章
- Aveva Marine C# 二次开发入门001
- 用C++ 和OpenCV 实现视频目标检测(YOLOv4模型)
- 第十六届智能车竞赛室内视觉AI组别靶标使用说明
- Sql结果导出为excel文件
- Visual Studio 在根目录下运行文件系统站点 [ Visual Studio | 文件系统 | WebDev.WebServer.EXE ]...
- 微信可直接转账到 QQ;小米联合中国联通推出当前最便宜 5G 套餐;Git 2.25.0 发布 | 极客头条...
- MySQL主主+Keepalived高可用(一):解决单点故障
- mysql workbench 1064_mysql - MySQL Workbench中的正向工程输出错误1064 - 堆栈内存溢出
- Kafka学习之broker配置(0.8.1版)(转)
- 2019JS必看面试题
- Html5实现二维码扫描并解析-web前端教程
- mysql书单推荐_MySQL有什么推荐的学习书籍
- 现行各主流语言的特点
- MQTT网关连接阿里云平台案例教程
- C#Windows学生信息管理系统
- 2008服务器文件夹镜像,【玩转腾讯云】导入镜像-Windows 2008 R2 Datacenter
- 计算机汉字显示原理,计算机汉字显示原理
- 《炬丰科技-半导体工艺》单层胶体晶体的微纳米光刻技术研究进展
- 苹果手机应用分身_G胖串流应用上架App Store,苹果手机能玩电脑游戏了
- FishC《零基础学习python》笔记--第004讲:改进我们的小游戏