.NET 視窗/螢幕截圖

唐宋元明清2188發表於2024-07-30

影像採集源除了顯示控制元件(上一篇《.NET 控制元件轉圖片》有介紹從介面控制元件轉圖片),更多的是視窗以及螢幕。

視窗截圖最常用的方法是GDI,直接上Demo吧:

 1         private void GdiCaptureButton_OnClick(object sender, RoutedEventArgs e)
 2         {
 3             var bitmap = CaptureScreen();
 4             CaptureImage.Source = ConvertBitmapToBitmapSource(bitmap);
 5         }
 6         /// <summary>
 7         /// 截圖螢幕
 8         /// </summary>
 9         /// <returns></returns>
10         public static Bitmap CaptureScreen()
11         {
12             IntPtr desktopWindow = GetDesktopWindow();
13             //獲取視窗位置大小
14             GetWindowRect(desktopWindow, out var lpRect);
15             return CaptureByGdi(desktopWindow, 0d, 0d, lpRect.Width, lpRect.Height);
16         }
17         private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap)
18         {
19             using MemoryStream memoryStream = new MemoryStream();
20             // 將 System.Drawing.Bitmap 儲存到記憶體流中
21             bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
22             // 重置記憶體流的指標到開頭
23             memoryStream.Seek(0, SeekOrigin.Begin);
24 
25             // 建立 BitmapImage 物件並從記憶體流中載入影像
26             BitmapImage bitmapImage = new BitmapImage();
27             bitmapImage.BeginInit();
28             bitmapImage.StreamSource = memoryStream;
29             bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
30             bitmapImage.EndInit();
31             // 確保記憶體流不會被回收
32             bitmapImage.Freeze();
33             return bitmapImage;
34         }
35         /// <summary>
36         /// 截圖視窗/螢幕
37         /// </summary>
38         /// <param name="windowIntPtr">視窗控制代碼(視窗或者桌面)</param>
39         /// <param name="left">水平座標</param>
40         /// <param name="top">豎直座標</param>
41         /// <param name="width">寬度</param>
42         /// <param name="height">高度</param>
43         /// <returns></returns>
44         private static Bitmap CaptureByGdi(IntPtr windowIntPtr, double left, double top, double width, double height)
45         {
46             IntPtr windowDc = GetWindowDC(windowIntPtr);
47             IntPtr compatibleDc = CreateCompatibleDC(windowDc);
48             IntPtr compatibleBitmap = CreateCompatibleBitmap(windowDc, (int)width, (int)height);
49             IntPtr bitmapObj = SelectObject(compatibleDc, compatibleBitmap);
50             BitBlt(compatibleDc, 0, 0, (int)width, (int)height, windowDc, (int)left, (int)top, CopyPixelOperation.SourceCopy);
51             Bitmap bitmap = System.Drawing.Image.FromHbitmap(compatibleBitmap);
52             //釋放
53             SelectObject(compatibleDc, bitmapObj);
54             DeleteObject(compatibleBitmap);
55             DeleteDC(compatibleDc);
56             ReleaseDC(windowIntPtr, windowDc);
57             return bitmap;
58         }

根據user32.dll下拿到的桌面資訊-控制代碼獲取桌面視窗的裝置上下文,再以裝置上下文分別建立記憶體裝置上下文、裝置點陣圖控制代碼

 1 BOOL BitBlt(
 2     HDC   hdcDest,  // 目標裝置上下文
 3     int   nXDest,   // 目標起始x座標
 4     int   nYDest,   // 目標起始y座標
 5     int   nWidth,   // 寬度(畫素)
 6     int   nHeight,  // 高度(畫素)
 7     HDC   hdcSrc,   // 源裝置上下文
 8     int   nXSrc,    // 源起始x座標
 9     int   nYSrc,    // 源起始y座標
10     DWORD dwRop    // 操作碼(如CopyPixelOperation.SourceCopy)
11 );

影像位塊傳輸BitBlt是最關鍵的函式,Gdi提供用於在裝置上下文之間進行點陣圖塊的傳輸,從原裝置上下文復現點陣圖到建立的裝置上下文

另外,與Bitblt差不多的還有StretchBlt,StretchBlt也是複製影像,但可以同時對影像進行拉伸或者縮小,需要縮圖可以用這個方法

然後以裝置點陣圖控制代碼輸出一個點陣圖System.Drawing.Bitmap,使用到的User32、Gdi32函式:

.NET 視窗/螢幕截圖
  1     /// <summary>
  2     /// 獲取桌面視窗
  3     /// </summary>
  4     /// <returns></returns>
  5     [DllImport("user32.dll")]
  6     public static extern IntPtr GetDesktopWindow();
  7     /// <summary>
  8     /// 獲取整個視窗的矩形區域
  9     /// </summary>
 10     /// <returns></returns>
 11     [DllImport("user32.dll", SetLastError = true)]
 12     public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
 13     /// <summary>
 14     /// 檢索整個視窗的裝置上下文
 15     /// </summary>
 16     /// <param name="hWnd">具有要檢索的裝置上下文的視窗的控制代碼</param>
 17     /// <returns></returns>
 18     [DllImport("user32.dll", SetLastError = true)]
 19     public static extern IntPtr GetWindowDC(IntPtr hWnd);
 20     /// <summary>
 21     /// 建立與指定裝置相容的記憶體裝置上下文
 22     /// </summary>
 23     /// <param name="hdc">現有 DC 的控制代碼</param>
 24     /// <returns>如果函式成功,則返回值是記憶體 DC 的控制代碼,否則返回Null</returns>
 25     [DllImport("gdi32.dll")]
 26     public static extern IntPtr CreateCompatibleDC([In] IntPtr hdc);
 27     /// <summary>
 28     /// 將物件選擇到指定的裝置上下文中
 29     /// </summary>
 30     /// <param name="hdc">DC 的控制代碼</param>
 31     /// <param name="gdiObj">要選擇的物件控制代碼</param>
 32     /// <returns>如果函式成功,則返回值是相容點陣圖 (DDB) 的控制代碼,否則返回Null</returns>
 33     [DllImport("gdi32.dll")]
 34     public static extern IntPtr SelectObject([In] IntPtr hdc, [In] IntPtr gdiObj);
 35     /// <summary>
 36     /// 建立與與指定裝置上下文關聯的裝置的點陣圖
 37     /// </summary>
 38     /// <param name="hdc">裝置上下文的控制代碼</param>
 39     /// <param name="nWidth">點陣圖寬度(以畫素為單位)</param>
 40     /// <param name="nHeight">點陣圖高度(以畫素為單位)</param>
 41     /// <returns></returns>
 42     [DllImport("gdi32.dll")]
 43     public static extern IntPtr CreateCompatibleBitmap([In] IntPtr hdc, int nWidth, int nHeight);
 44     /// <summary>
 45     /// 執行與從指定源裝置上下文到目標裝置上下文中的畫素矩形對應的顏色資料的位塊傳輸
 46     /// </summary>
 47     /// <param name="hdcDest">目標裝置上下文的控制代碼</param>
 48     /// <param name="xDest">目標矩形左上角的 x 座標(邏輯單位)</param>
 49     /// <param name="yDest">目標矩形左上角的 y 座標(邏輯單位)</param>
 50     /// <param name="wDest">源矩形和目標矩形的寬度(邏輯單位)</param>
 51     /// <param name="hDest">源矩形和目標矩形的高度(邏輯單位)</param>
 52     /// <param name="hdcSource">源裝置上下文的控制代碼</param>
 53     /// <param name="xSrc">源矩形左上角的 x 座標(邏輯單位)</param>
 54     /// <param name="ySrc">源矩形左上角的 y 座標(邏輯單位)</param>
 55     /// <param name="rop">定義如何將源矩形的顏色資料與目標矩形的顏色資料相結合</param>
 56     /// <returns></returns>
 57     [DllImport("gdi32.dll")]
 58     public static extern bool BitBlt(IntPtr hdcDest,
 59         int xDest, int yDest, int wDest, int hDest, IntPtr hdcSource,
 60         int xSrc, int ySrc, CopyPixelOperation rop);
 61     /// <summary>
 62     /// 刪除邏輯筆、畫筆、字型、點陣圖、區域或調色盤,釋放與物件關聯的所有系統資源。
 63     /// 刪除物件後,指定的控制代碼將不再有效。
 64     /// </summary>
 65     /// <param name="hObject"></param>
 66     /// <returns></returns>
 67     [DllImport("gdi32.dll")]
 68     public static extern bool DeleteObject(IntPtr hObject);
 69     /// <summary>
 70     /// 刪除指定的裝置上下文
 71     /// </summary>
 72     /// <param name="hdc">裝置上下文的句裝置上下文的句</param>
 73     /// <returns></returns>
 74     [DllImport("gdi32.dll")]
 75     public static extern bool DeleteDC([In] IntPtr hdc);
 76     /// <summary>
 77     /// 釋放裝置上下文 (DC),釋放它以供其他應用程式使用
 78     /// </summary>
 79     /// <param name="hWnd"></param>
 80     /// <param name="hdc"></param>
 81     /// <returns></returns>
 82     [DllImport("user32.dll", SetLastError = true)]
 83     public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hdc);
 84 
 85     /// <summary>
 86     /// 定義一個矩形區域。
 87     /// </summary>
 88     [StructLayout(LayoutKind.Sequential)]
 89     public struct RECT
 90     {
 91         /// <summary>
 92         /// 矩形左側的X座標。
 93         /// </summary>
 94         public int Left;
 95 
 96         /// <summary>
 97         /// 矩形頂部的Y座標。
 98         /// </summary>
 99         public int Top;
100 
101         /// <summary>
102         /// 矩形右側的X座標。
103         /// </summary>
104         public int Right;
105 
106         /// <summary>
107         /// 矩形底部的Y座標。
108         /// </summary>
109         public int Bottom;
110 
111         /// <summary>
112         /// 獲取矩形的寬度。
113         /// </summary>
114         public int Width => Right - Left;
115 
116         /// <summary>
117         /// 獲取矩形的高度。
118         /// </summary>
119         public int Height => Bottom - Top;
120 
121         /// <summary>
122         /// 初始化一個新的矩形。
123         /// </summary>
124         /// <param name="left">矩形左側的X座標。</param>
125         /// <param name="top">矩形頂部的Y座標。</param>
126         /// <param name="right">矩形右側的X座標。</param>
127         /// <param name="bottom">矩形底部的Y座標。</param>
128         public RECT(int left, int top, int right, int bottom)
129         {
130             Left = left;
131             Top = top;
132             Right = right;
133             Bottom = bottom;
134         }
135     }
View Code

還有一種比較簡單的方法Graphics.CopyFromScreen,看看呼叫DEMO:

 1         private void GraphicsCaptureButton_OnClick(object sender, RoutedEventArgs e)
 2         {
 3             var image = CaptureScreen1();
 4             CaptureImage.Source = ConvertBitmapToBitmapSource(image);
 5         }
 6         /// <summary>
 7         /// 截圖螢幕
 8         /// </summary>
 9         /// <returns></returns>
10         public static Bitmap CaptureScreen1()
11         {
12             IntPtr desktopWindow = GetDesktopWindow();
13             //獲取視窗位置大小
14             GetWindowRect(desktopWindow, out var lpRect);
15             return CaptureScreenByGraphics(0, 0, lpRect.Width, lpRect.Height);
16         }
17         /// <summary>
18         /// 截圖螢幕
19         /// </summary>
20         /// <param name="x">x座標</param>
21         /// <param name="y">y座標</param>
22         /// <param name="width">擷取的寬度</param>
23         /// <param name="height">擷取的高度</param>
24         /// <returns></returns>
25         public static Bitmap CaptureScreenByGraphics(int x, int y, int width, int height)
26         {
27             var bitmap = new Bitmap(width, height);
28             using var graphics = Graphics.FromImage(bitmap);
29             graphics.CopyFromScreen(x, y, 0, 0, new System.Drawing.Size(width, height), CopyPixelOperation.SourceCopy);
30             return bitmap;
31         }

Graphics.CopyFromScreen呼叫簡單了很多,與GDI有什麼區別?

Graphics.CopyFromScreen內部也是透過GDI.BitBlt來完成螢幕捕獲的,封裝了下提供更高階別、易勝的API。

測試了下,第一種方法Gdi32效能比Graphics.CopyFromScreen效能略微好一點,冷啟動時更明顯點,試了2次耗時大概少個10多ms。

所以對於一般應用場景,使用 Graphics.CopyFromScreen 就足夠了,但如果你需要更高的控制權和效能最佳化,建議使用 Gdi32.BitBlt

kybs00/CaptureImageDemo (github.com)

相關文章