windows 上,屏幕截图一般是调用 win32 api 完成的,如果 C# 想实现截图功能,就需要封装相关 api。在 Windows 上,主要图形接口有 GDI 和 DirectX。GDI 接口比较灵活,可以截取指定窗口,哪怕窗口被遮挡或位于显示区域外,但兼容性较低,无法截取 DX 接口输出的画面。DirectX 是高性能图形接口(当然还有其他功能,与本文无关,忽略不计),主要作为游戏图形接口使用,灵活性较低,无法指定截取特定窗口(或者只是我不会吧),但是兼容性较高,可以截取任何输出到屏幕的内容,根据情况使用。
以下代码使用了 C# 8.0 的新功能,只能使用 VS 2019 编译,如果需要在老版本 VS 使用,需要自行改造。
用静态类简单封装 GDI 接口并调用接口截图。
1 public static class CaptureWindow
2 {
3 #region 类
4 /// <summary>
5 /// Helper class containing User32 API functions
6 /// </summary>
7 private class User32
8 {
9 [StructLayout(LayoutKind.Sequential)]
10 public struct RECT
11 {
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 Gdi32
31 {
32
33 public const int SRCCOPY = 0x00CC0020; // BitBlt dwRop parameter
34 [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 #endregion
51
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 window
60 IntPtr hdcSrc = User32.GetWindowDC(hWnd);
61 // get the size
62 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 to
67 IntPtr hdcDest = Gdi32.CreateCompatibleDC(hdcSrc);
68 // create a bitmap we can copy it to,
69 // using GetDeviceCaps to get the width/height
70 IntPtr hBitmap = Gdi32.CreateCompatibleBitmap(hdcSrc, width, height);
71 // select the bitmap object
72 IntPtr hOld = Gdi32.SelectObject(hdcDest, hBitmap);
73 // bitblt over
74 Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, Gdi32.SRCCOPY);
75 // restore selection
76 Gdi32.SelectObject(hdcDest, hOld);
77 // clean up
78 Gdi32.DeleteDC(hdcDest);
79 User32.ReleaseDC(hWnd, hdcSrc);
80 // get a .NET image object for it
81 Image img = Image.FromHbitmap(hBitmap);
82 // free up the Bitmap object
83 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 }
安装 nuget 包 SharpDX.Direct3D11,简单封装。此处使用 D3D 11 接口封装,对多显卡多显示器的情况只能截取主显卡主显示器画面,如需截取其他屏幕,需稍微改造构造函数。截屏可能失败,也可能截取到黑屏,已经在返回值中提示。
将 DX 截屏转换成 C# 图像使用了指针操作,一方面可以提升性能,一方面也是因为都用 DX 了,基本上是很难避免底层操作了,那就一不做二不休,多利用一下。
1 public class DirectXScreenCapturer : IDisposable
2 {
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 Texture2DDescription
23 {
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.Staging
34 };
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 unsafe
79 {
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 是时候退出历史舞台了。代码参考了网上大神的文章,并根据实际情况进行改造,尽可能简化实现和使用代码,展示最简单情况下所必须的代码。如果实际需求比较复杂,可以以这个为底版进行改造。