C#截圖功能
阿新 • • 發佈:2021-06-23
Windows 上,螢幕截圖一般是呼叫 win32 api 完成的,如果 C# 想實現截圖功能,就需要封裝相關 api。在 Windows 上,主要圖形介面有 GDI 和 DirectX。GDI 介面比較靈活,可以擷取指定視窗,哪怕視窗被遮擋或位於顯示區域外,但相容性較低,無法擷取 DX 介面輸出的畫面。DirectX 是高效能圖形介面(當然還有其他功能,與本文無關,忽略不計),主要作為遊戲圖形介面使用,靈活性較低,無法指定擷取特定視窗(或者只是我不會吧),但是相容性較高,可以擷取任何輸出到螢幕的內容,本文提供兩種,根據情況使用。
GDI+
以下程式碼使用了 C# 8.0 的新功能,只能使用 VS 2019 編譯,如果需要在老版本 VS 使用,需要自行改造。
public static class CaptureWindow { #region 類 /// <summary> /// Helper class containing User32 API functions /// </summary> private class User32 { [StructLayout(LayoutKind.Sequential)] public struct RECT { public int left; public int top; public int right; public int bottom; } [DllImport("user32.dll")] public static extern IntPtr GetDesktopWindow(); [DllImport("user32.dll")] public static extern IntPtr GetWindowDC(IntPtr hWnd); [DllImport("user32.dll")] public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC); [DllImport("user32.dll")] public static extern IntPtr GetWindowRect(IntPtr hWnd, ref RECT rect); [DllImport("user32.dll", EntryPoint = "FindWindow", CharSet = CharSet.Unicode)] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); } private class Gdi32 { public const int SRCCOPY = 0x00CC0020; // BitBlt dwRop parameter [DllImport("gdi32.dll")] public static extern bool BitBlt(IntPtr hObject, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hObjectSource, int nXSrc, int nYSrc, int dwRop); [DllImport("gdi32.dll")] public static extern IntPtr CreateCompatibleBitmap(IntPtr hDC, int nWidth, int nHeight); [DllImport("gdi32.dll")] public static extern IntPtr CreateCompatibleDC(IntPtr hDC); [DllImport("gdi32.dll")] public static extern bool DeleteDC(IntPtr hDC); [DllImport("gdi32.dll")] public static extern bool DeleteObject(IntPtr hObject); [DllImport("gdi32.dll")] public static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject); } #endregion /// <summary> /// 根據控制代碼截圖 /// </summary> /// <param name="hWnd">控制代碼</param> /// <returns></returns> public static Image ByHwnd(IntPtr hWnd) { // get te hDC of the target window IntPtr hdcSrc = User32.GetWindowDC(hWnd); // get the size User32.RECT windowRect = new User32.RECT(); User32.GetWindowRect(hWnd, ref windowRect); int width = windowRect.right - windowRect.left; int height = windowRect.bottom - windowRect.top; // create a device context we can copy to IntPtr hdcDest = Gdi32.CreateCompatibleDC(hdcSrc); // create a bitmap we can copy it to, // using GetDeviceCaps to get the width/height IntPtr hBitmap = Gdi32.CreateCompatibleBitmap(hdcSrc, width, height); // select the bitmap object IntPtr hOld = Gdi32.SelectObject(hdcDest, hBitmap); // bitblt over Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, Gdi32.SRCCOPY); // restore selection Gdi32.SelectObject(hdcDest, hOld); // clean up Gdi32.DeleteDC(hdcDest); User32.ReleaseDC(hWnd, hdcSrc); // get a .NET image object for it Image img = Image.FromHbitmap(hBitmap); // free up the Bitmap object Gdi32.DeleteObject(hBitmap); return img; } /// <summary> /// 根據視窗名稱截圖 /// </summary> /// <param name="windowName">視窗名稱</param> /// <returns></returns> public static Image ByName(string windowName) { IntPtr handle = User32.FindWindow(null, windowName); IntPtr hdcSrc = User32.GetWindowDC(handle); User32.RECT windowRect = new User32.RECT(); User32.GetWindowRect(handle, ref windowRect); int width = windowRect.right - windowRect.left; int height = windowRect.bottom - windowRect.top; IntPtr hdcDest = Gdi32.CreateCompatibleDC(hdcSrc); IntPtr hBitmap = Gdi32.CreateCompatibleBitmap(hdcSrc, width, height); IntPtr hOld = Gdi32.SelectObject(hdcDest, hBitmap); Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, Gdi32.SRCCOPY); Gdi32.SelectObject(hdcDest, hOld); Gdi32.DeleteDC(hdcDest); User32.ReleaseDC(handle, hdcSrc); Image img = Image.FromHbitmap(hBitmap); Gdi32.DeleteObject(hBitmap); return img; } }
Direct3D
安裝 nuget 包 SharpDX.Direct3D11,簡單封裝。此處使用 D3D 11 介面封裝,對多顯示卡多顯示器的情況只能擷取主顯示卡主顯示器畫面,如需擷取其他螢幕,需稍微改造建構函式。截圖可能失敗,也可能擷取到黑屏,已經在返回值中提示。由於不是主要內容,參考即可,程式碼:
public class DirectXScreenCapturer : IDisposable { private Factory1 factory; private Adapter1 adapter; private SharpDX.Direct3D11.Device device; private Output output; private Output1 output1; private Texture2DDescription textureDesc; //2D 紋理,儲存截圖資料 private Texture2D screenTexture; public DirectXScreenCapturer() { // 獲取輸出裝置(顯示卡、顯示器),這裡是主顯示卡和主顯示器 factory = new Factory1(); adapter = factory.GetAdapter1(0); device = new SharpDX.Direct3D11.Device(adapter); output = adapter.GetOutput(0); output1 = output.QueryInterface<Output1>(); //設定紋理資訊,供後續使用(截圖大小和質量) textureDesc = new Texture2DDescription { CpuAccessFlags = CpuAccessFlags.Read, BindFlags = BindFlags.None, Format = Format.B8G8R8A8_UNorm, Width = output.Description.DesktopBounds.Right, Height = output.Description.DesktopBounds.Bottom, OptionFlags = ResourceOptionFlags.None, MipLevels = 1, ArraySize = 1, SampleDescription = { Count = 1, Quality = 0 }, Usage = ResourceUsage.Staging }; screenTexture = new Texture2D(device, textureDesc); } public Result ProcessFrame(Action<DataBox, Texture2DDescription> processAction, int timeoutInMilliseconds = 5) { //截圖,可能失敗 using OutputDuplication duplicatedOutput = output1.DuplicateOutput(device); var result = duplicatedOutput.TryAcquireNextFrame(timeoutInMilliseconds, out OutputDuplicateFrameInformation duplicateFrameInformation, out SharpDX.DXGI.Resource screenResource); if (!result.Success) return result; using Texture2D screenTexture2D = screenResource.QueryInterface<Texture2D>(); //複製資料 device.ImmediateContext.CopyResource(screenTexture2D, screenTexture); DataBox mapSource = device.ImmediateContext.MapSubresource(screenTexture, 0, MapMode.Read, SharpDX.Direct3D11.MapFlags.None); processAction?.Invoke(mapSource, textureDesc); //釋放資源 device.ImmediateContext.UnmapSubresource(screenTexture, 0); screenResource.Dispose(); duplicatedOutput.ReleaseFrame(); return result; } public (Result result, bool isBlackFrame, Image image) GetFrameImage(int timeoutInMilliseconds = 5) { //生成 C# 用影象 Bitmap image = new Bitmap(textureDesc.Width, textureDesc.Height, PixelFormat.Format24bppRgb); bool isBlack = true; var result = ProcessFrame(ProcessImage); if (!result.Success) image.Dispose(); return (result, isBlack, result.Success ? image : null); void ProcessImage(DataBox dataBox, Texture2DDescription texture) { BitmapData data = image.LockBits(new Rectangle(0, 0, texture.Width, texture.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb); unsafe { byte* dataHead = (byte*)dataBox.DataPointer.ToPointer(); for (int x = 0; x < texture.Width; x++) { for (int y = 0; y < texture.Height; y++) { byte* pixPtr = (byte*)(data.Scan0 + y * data.Stride + x * 3); int pos = x + y * texture.Width; pos *= 4; byte r = dataHead[pos + 2]; byte g = dataHead[pos + 1]; byte b = dataHead[pos + 0]; if (isBlack && (r != 0 || g != 0 || b != 0)) isBlack = false; pixPtr[0] = b; pixPtr[1] = g; pixPtr[2] = r; } } } image.UnlockBits(data); } } #region IDisposable Support private bool disposedValue = false; // 要檢測冗餘呼叫 protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: 釋放託管狀態(託管物件)。 factory.Dispose(); adapter.Dispose(); device.Dispose(); output.Dispose(); output1.Dispose(); screenTexture.Dispose(); } // TODO: 釋放未託管的資源(未託管的物件)並在以下內容中替代終結器。 // TODO: 將大型欄位設定為 null。 factory = null; adapter = null; device = null; output = null; output1 = null; screenTexture = null; disposedValue = true; } } // TODO: 僅當以上 Dispose(bool disposing) 擁有用於釋放未託管資源的程式碼時才替代終結器。 // ~DirectXScreenCapturer() // { // // 請勿更改此程式碼。將清理程式碼放入以上 Dispose(bool disposing) 中。 // Dispose(false); // } // 新增此程式碼以正確實現可處置模式。 public void Dispose() { // 請勿更改此程式碼。將清理程式碼放入以上 Dispose(bool disposing) 中。 Dispose(true); // TODO: 如果在以上內容中替代了終結器,則取消註釋以下行。 // GC.SuppressFinalize(this); } #endregion }
使用示例
程式碼:
static async Task Main(string[] args)
{
Console.Write("按任意鍵開始DX截圖……");
Console.ReadKey();
string path = @"E:\截圖測試";
var cancel = new CancellationTokenSource();
await Task.Run(() =>
{
Task.Run(() =>
{
Thread.Sleep(5000);
cancel.Cancel();
Console.WriteLine("DX截圖結束!");
});
var savePath = $@"{path}\DX";
Directory.CreateDirectory(savePath);
using var dx = new DirectXScreenCapturer();
Console.WriteLine("開始DX截圖……");
while (!cancel.IsCancellationRequested)
{
var (result, isBlackFrame, image) = dx.GetFrameImage();
if (result.Success && !isBlackFrame) image.Save($@"{savePath}\{DateTime.Now.Ticks}.jpg", ImageFormat.Jpeg);
image?.Dispose();
}
}, cancel.Token);
var windows = WindowEnumerator.FindAll();
for (int i = 0; i < windows.Count; i++)
{
var window = windows[i];
Console.WriteLine($@"{i.ToString().PadLeft(3, ' ')}. {window.Title}
{window.Bounds.X}, {window.Bounds.Y}, {window.Bounds.Width}, {window.Bounds.Height}");
}
var savePath = $@"{path}\Gdi";
Directory.CreateDirectory(savePath);
Console.WriteLine("開始Gdi視窗截圖……");
foreach (var win in windows)
{
var image = CaptureWindow.ByHwnd(win.Hwnd);
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);
image.Dispose();
}
Console.WriteLine("Gdi視窗截圖結束!");
Console.ReadKey();
}