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 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     }

Direct3D

       安装 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 是时候退出历史舞台了。代码参考了网上大神的文章,并根据实际情况进行改造,尽可能简化实现和使用代码,展示最简单情况下所必须的代码。如果实际需求比较复杂,可以以这个为底版进行改造。

 

       转载请完整保留以下内容并在显眼位置标注,未经授权删除以下内容进行转载盗用的,保留追究法律责任的权利!

  本文地址:https://www.cnblogs.com/coredx/p/12422559.html

  完整源代码:Github

  里面有各种小东西,这只是其中之一,不嫌弃的话可以Star一下。