前言

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 屏幕截图相关推荐

  1. WINDOWS高级窗口的客户区域拖动技术及其应用

    WINDOWS高级窗口的客户区域拖动技术及其应用 来源:http://dev.21tx.com WINDOWS应用程序窗口一般包括两种:普通窗口和常居顶层的无标题条高级窗口.前者是由WINDOWS内部 ...

  2. 戏说 Windows GDI (3)

    继续讨论之前的问题,包括GDI对象的删除问题,窗口注册与销毁问题,最后用标尺的小例子说明问题 1.GDI对象的删除 由CGDIObject派生类创建的画笔.画刷和其他对象都要占用内存资源,因此在使用完 ...

  3. MFC和Win32之三___CGdiObject类和windows Gdi对象

    小结: 前面讲到的windows窗口对象,在windows下用句柄来代表之,并且用了一个数据结构WNDCLASS(窗口类)来描述之.同理,windows的Gdi对象也有一些句柄来代表之(比如hPen等 ...

  4. Windows GDI和GDI+编程实例剖析(1)

    Windows GDI和GDI+编程实例剖析(1) 作者:宋宝华  e-mail:21cnbao@21cn.com 1.基本概念 GDI在Windows中定义为Graphics Device Inte ...

  5. 原创]Windows Gdi入门初级应用(VC SDK)

    原创]Windows Gdi入门初级应用(VC SDK) 好久没发贴了,今天手痒痒,发一个. GDI的绘图函数基本上都是有状态的,所有的函数都要求一个HDC类型的句柄. 这个HDC的获得有几个途径Be ...

  6. Windows GDI

    Windows GDI 在单任务环境如 MS-DOS 中,运行中的应用程序随时可自由地做它想做的事情,无论是在屏幕上画一条线,重新编写适配器的调色板,还是转换到另一种图像模式.而在窗口化多任务环境如 ...

  7. w7系统计算机网络密码是什么,解答win7系统共享弹出Windows安全窗口提示输入网络密码的详细教程...

    随着电脑的使用率越来越高,我们有时候可能会遇到win7系统共享弹出Windows安全窗口提示输入网络密码问题,如果我们遇到了win7系统共享弹出Windows安全窗口提示输入网络密码的问题,要怎么处理 ...

  8. Windows - CMD窗口UTF8编码乱码问题的解决!

    Windows - CMD窗口UTF8编码乱码问题的解决! 参考文章: (1)Windows - CMD窗口UTF8编码乱码问题的解决! (2)https://www.cnblogs.com/suny ...

  9. windows cmd 窗口 显示信息慢_你玩过Windows 10新版CMD了吗?

    [PConline应用]CMD是Windows里一项经典的命令行工具,很多人认为PowerShell的出现将逐步取代CMD,成为新一代默认命令行.近日微软在Win10官方商店,发布了一款全新的Wind ...

最新文章

  1. Aveva Marine C# 二次开发入门001
  2. 用C++ 和OpenCV 实现视频目标检测(YOLOv4模型)
  3. 第十六届智能车竞赛室内视觉AI组别靶标使用说明
  4. Sql结果导出为excel文件
  5. Visual Studio 在根目录下运行文件系统站点 [ Visual Studio | 文件系统 | WebDev.WebServer.EXE ]...
  6. 微信可直接转账到 QQ;小米联合中国联通推出当前最便宜 5G 套餐;Git 2.25.0 发布 | 极客头条...
  7. MySQL主主+Keepalived高可用(一):解决单点故障
  8. mysql workbench 1064_mysql - MySQL Workbench中的正向工程输出错误1064 - 堆栈内存溢出
  9. Kafka学习之broker配置(0.8.1版)(转)
  10. 2019JS必看面试题
  11. Html5实现二维码扫描并解析-web前端教程
  12. mysql书单推荐_MySQL有什么推荐的学习书籍
  13. 现行各主流语言的特点
  14. MQTT网关连接阿里云平台案例教程
  15. C#Windows学生信息管理系统
  16. 2008服务器文件夹镜像,【玩转腾讯云】导入镜像-Windows 2008 R2 Datacenter
  17. 计算机汉字显示原理,计算机汉字显示原理
  18. 《炬丰科技-半导体工艺》单层胶体晶体的微纳米光刻技术研究进展
  19. 苹果手机应用分身_G胖串流应用上架App Store,苹果手机能玩电脑游戏了
  20. FishC《零基础学习python》笔记--第004讲:改进我们的小游戏

热门文章

  1. 微信分享解决wx not defined
  2. 【那些年,我们一起追得女孩】第十五章
  3. C++ 图书管理系统设计
  4. Python版打字练习软件源代码,键盘练习软件源代码,含娱乐模式和训练模式
  5. 【allegro 17.4软件操作保姆级教程三】布局操作基础一
  6. SaaS二月寒冬、传统IT厂商逆袭与重新定义云计算
  7. 手机号或者邮箱注册功能
  8. oracle 行级死锁_Oracle常见死锁发生的原因以及解决方法
  9. Sortablejs + css 模仿安卓桌面,实现可拖拽排序的主页图标+文件夹收纳
  10. 手把手教你使用Keras进行人脸检测和识别