• 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一下。

  • 相关阅读:
    SDOI2008 Sandy的卡片
    BZOJ2555 Substring
    CTSC2012 熟悉的文章
    递增
    丢失的牛
    【模板】点分治
    陌上花开(三维偏序)
    Holes(河鼠入洞)
    弹飞河鼠
    树状数组1
  • 原文地址:https://www.cnblogs.com/coredx/p/12422559.html
Copyright © 2020-2023  润新知