Windows GDI 窗口與 Direct3D 屏幕截圖

前言

       Windows 上,屏幕截圖通常是調用 win32 api 完成的,若是 C# 想實現截圖功能,就須要封裝相關 api。在 Windows 上,主要圖形接口有 GDI 和 DirectX。GDI 接口比較靈活,能夠截取指定窗口,哪怕窗口被遮擋或位於顯示區域外,但兼容性較低,沒法截取 DX 接口輸出的畫面。DirectX 是高性能圖形接口(固然還有其餘功能,與本文無關,忽略不計),主要做爲遊戲圖形接口使用,靈活性較低,沒法指定截取特定窗口(或者只是我不會吧),可是兼容性較高,能夠截取任何輸出到屏幕的內容,根據狀況使用。html

正文

       如下代碼使用了 C# 8.0 的新功能,只能使用 VS 2019 編譯,若是須要在老版本 VS 使用,須要自行改造。git

GDI

       用靜態類簡單封裝 GDI 接口並調用接口截圖。github

 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 接口封裝,對多顯卡多顯示器的狀況只能截取主顯卡主顯示器畫面,如需截取其餘屏幕,需稍微改造構造函數。截屏可能失敗,也可能截取到黑屏,已經在返回值中提示。windows

       將 DX 截屏轉換成 C# 圖像使用了指針操做,一方面能夠提高性能,一方面也是由於都用 DX 了,基本上是很難避免底層操做了,那就一不作二不休,多利用一下。api

 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。async

 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 是時候退出歷史舞臺了。代碼參考了網上大神的文章,並根據實際狀況進行改造,儘量簡化實現和使用代碼,展現最簡單狀況下所必須的代碼。若是實際需求比較複雜,能夠以這個爲底版進行改造。ide

 

       轉載請完整保留如下內容並在顯眼位置標註,未經受權刪除如下內容進行轉載盜用的,保留追究法律責任的權利!函數

  本文地址:http://www.javashuo.com/article/p-hegetopw-ep.html
性能

  完整源代碼:Github測試

  裏面有各類小東西,這只是其中之一,不嫌棄的話能夠Star一下。

相關文章
相關標籤/搜索