Windows 上,屏幕截圖通常是調用 win32 api 完成的,若是 C# 想實現截圖功能,就須要封裝相關 api。在 Windows 上,主要圖形接口有 GDI 和 DirectX。GDI 接口比較靈活,能夠截取指定窗口,哪怕窗口被遮擋或位於顯示區域外,但兼容性較低,沒法截取 DX 接口輸出的畫面。DirectX 是高性能圖形接口(固然還有其餘功能,與本文無關,忽略不計),主要做爲遊戲圖形接口使用,靈活性較低,沒法指定截取特定窗口(或者只是我不會吧),可是兼容性較高,能夠截取任何輸出到屏幕的內容,根據狀況使用。html
如下代碼使用了 C# 8.0 的新功能,只能使用 VS 2019 編譯,若是須要在老版本 VS 使用,須要自行改造。git
用靜態類簡單封裝 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 }
安裝 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一下。