会议白板之窗口/屏幕截图

科技   2024-09-11 07:30   北京  

 会议白板之窗口/屏幕截图

本文经原作者授权以原创方式二次分享,欢迎转载、分享。

原文作者:唐宋元明清

原文地址:https://www.cnblogs.com/kybs0/p/18330803

会议白板之窗口/屏幕截图

图像采集源除了显示控件(上一篇《.NET 控件转图片》有介绍从界面控件转图片),更多的是窗口以及屏幕。

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

private void GdiCaptureButton_OnClick(object sender, RoutedEventArgs e)
        {
            var bitmap = CaptureScreen();
            CaptureImage.Source = ConvertBitmapToBitmapSource(bitmap);
        }
        /// <summary>
        /// 截图屏幕
        /// </summary>
        /// <returns></returns>
        public static Bitmap CaptureScreen()
        {
            IntPtr desktopWindow = GetDesktopWindow();
            //获取窗口位置大小
            GetWindowRect(desktopWindow, out var lpRect);
            return CaptureByGdi(desktopWindow, 0d, 0d, lpRect.Width, lpRect.Height);
        }
        private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap)
        {
            using MemoryStream memoryStream = new MemoryStream();
            // 将 System.Drawing.Bitmap 保存到内存流中
            bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
            // 重置内存流的指针到开头
            memoryStream.Seek(0, SeekOrigin.Begin);

            // 创建 BitmapImage 对象并从内存流中加载图像
            BitmapImage bitmapImage = new BitmapImage();
            bitmapImage.BeginInit();
            bitmapImage.StreamSource = memoryStream;
            bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
            bitmapImage.EndInit();
            // 确保内存流不会被回收
            bitmapImage.Freeze();
            return bitmapImage;
        }
        /// <summary>
        /// 截图窗口/屏幕
        /// </summary>
        /// <param name="windowIntPtr">窗口句柄(窗口或者桌面)</param>
        /// <param name="left">水平坐标</param>
        /// <param name="top">竖直坐标</param>
        /// <param name="width">宽度</param>
        /// <param name="height">高度</param>
        /// <returns></returns>
        private static Bitmap CaptureByGdi(IntPtr windowIntPtr, double left, double top, double width, double height)
        {
            IntPtr windowDc = GetWindowDC(windowIntPtr);
            IntPtr compatibleDc = CreateCompatibleDC(windowDc);
            IntPtr compatibleBitmap = CreateCompatibleBitmap(windowDc, (int)width, (int)height);
            IntPtr bitmapObj = SelectObject(compatibleDc, compatibleBitmap);
            BitBlt(compatibleDc, 00, (int)width, (int)height, windowDc, (int)left, (int)top, CopyPixelOperation.SourceCopy);
            Bitmap bitmap = System.Drawing.Image.FromHbitmap(compatibleBitmap);
            //释放
            SelectObject(compatibleDc, bitmapObj);
            DeleteObject(compatibleBitmap);
            DeleteDC(compatibleDc);
            ReleaseDC(windowIntPtr, windowDc);
            return bitmap;
        }

根据 user32.dll 下拿到的桌面信息-句柄获取桌面窗口的设备上下文,再以设备上下文分别创建内存设备上下文、设备位图句柄

BOOL BitBlt(
    HDC   hdcDest,  // 目标设备上下文
    int   nXDest,   // 目标起始x坐标
    int   nYDest,   // 目标起始y坐标
    int   nWidth,   // 宽度(像素)
    int   nHeight,  // 高度(像素)
    HDC   hdcSrc,   // 源设备上下文
    int   nXSrc,    // 源起始x坐标
    int   nYSrc,    // 源起始y坐标
    DWORD dwRop    // 操作码(如CopyPixelOperation.SourceCopy)
)
;

图像位块传输 BitBlt 是最关键的函数,GDI 提供用于在设备上下文之间进行位图块的传输,从原设备上下文复现位图到创建的设备上下文

另外,与 BitBlt 差不多的还有 StretchBltStretchBl 也是复制图像,但可以同时对图像进行拉伸或者缩小,需要缩略图可以用这个方法

然后以设备位图句柄输出一个位图 System.Drawing.Bitmap,使用到的 User32Gdi32 函数:

/// <summary>
    /// 获取桌面窗口
    /// </summary>
    /// <returns></returns>
    [DllImport("user32.dll")]
    public static extern IntPtr GetDesktopWindow();
    /// <summary>
    /// 获取整个窗口的矩形区域
    /// </summary>
    /// <returns></returns>
    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
    /// <summary>
    /// 检索整个窗口的设备上下文
    /// </summary>
    /// <param name="hWnd">具有要检索的设备上下文的窗口的句柄</param>
    /// <returns></returns>
    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr GetWindowDC(IntPtr hWnd);
    /// <summary>
    /// 创建与指定设备兼容的内存设备上下文
    /// </summary>
    /// <param name="hdc">现有 DC 的句柄</param>
    /// <returns>如果函数成功,则返回值是内存 DC 的句柄,否则返回Null</returns>
    [DllImport("gdi32.dll")]
    public static extern IntPtr CreateCompatibleDC([In] IntPtr hdc);
    /// <summary>
    /// 将对象选择到指定的设备上下文中
    /// </summary>
    /// <param name="hdc">DC 的句柄</param>
    /// <param name="gdiObj">要选择的对象句柄</param>
    /// <returns>如果函数成功,则返回值是兼容位图 (DDB) 的句柄,否则返回Null</returns>
    [DllImport("gdi32.dll")]
    public static extern IntPtr SelectObject([In] IntPtr hdc, [In] IntPtr gdiObj);
    /// <summary>
    /// 创建与与指定设备上下文关联的设备的位图
    /// </summary>
    /// <param name="hdc">设备上下文的句柄</param>
    /// <param name="nWidth">位图宽度(以像素为单位)</param>
    /// <param name="nHeight">位图高度(以像素为单位)</param>
    /// <returns></returns>
    [DllImport("gdi32.dll")]
    public static extern IntPtr CreateCompatibleBitmap([In] IntPtr hdc, int nWidth, int nHeight);
    /// <summary>
    /// 执行与从指定源设备上下文到目标设备上下文中的像素矩形对应的颜色数据的位块传输
    /// </summary>
    /// <param name="hdcDest">目标设备上下文的句柄</param>
    /// <param name="xDest">目标矩形左上角的 x 坐标(逻辑单位)</param>
    /// <param name="yDest">目标矩形左上角的 y 坐标(逻辑单位)</param>
    /// <param name="wDest">源矩形和目标矩形的宽度(逻辑单位)</param>
    /// <param name="hDest">源矩形和目标矩形的高度(逻辑单位)</param>
    /// <param name="hdcSource">源设备上下文的句柄</param>
    /// <param name="xSrc">源矩形左上角的 x 坐标(逻辑单位)</param>
    /// <param name="ySrc">源矩形左上角的 y 坐标(逻辑单位)</param>
    /// <param name="rop">定义如何将源矩形的颜色数据与目标矩形的颜色数据相结合</param>
    /// <returns></returns>
    [DllImport("gdi32.dll")]
    public static extern bool BitBlt(IntPtr hdcDest,
        int xDest, int yDest, int wDest, int hDest, IntPtr hdcSource,
        int xSrc, int ySrc, CopyPixelOperation rop
)
;
    /// <summary>
    /// 删除逻辑笔、画笔、字体、位图、区域或调色板,释放与对象关联的所有系统资源。
    /// 删除对象后,指定的句柄将不再有效。
    /// </summary>
    /// <param name="hObject"></param>
    /// <returns></returns>
    [DllImport("gdi32.dll")]
    public static extern bool DeleteObject(IntPtr hObject);
    /// <summary>
    /// 删除指定的设备上下文
    /// </summary>
    /// <param name="hdc">设备上下文的句设备上下文的句</param>
    /// <returns></returns>
    [DllImport("gdi32.dll")]
    public static extern bool DeleteDC([In] IntPtr hdc);
    /// <summary>
    /// 释放设备上下文 (DC),释放它以供其他应用程序使用
    /// </summary>
    /// <param name="hWnd"></param>
    /// <param name="hdc"></param>
    /// <returns></returns>
    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hdc);

    /// <summary>
    /// 定义一个矩形区域。
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
        /// <summary>
        /// 矩形左侧的X坐标。
        /// </summary>
        public int Left;

        /// <summary>
        /// 矩形顶部的Y坐标。
        /// </summary>
        public int Top;

        /// <summary>
        /// 矩形右侧的X坐标。
        /// </summary>
        public int Right;

        /// <summary>
        /// 矩形底部的Y坐标。
        /// </summary>
        public int Bottom;

        /// <summary>
        /// 获取矩形的宽度。
        /// </summary>
        public int Width => Right - Left;

        /// <summary>
        /// 获取矩形的高度。
        /// </summary>
        public int Height => Bottom - Top;

        /// <summary>
        /// 初始化一个新的矩形。
        /// </summary>
        /// <param name="left">矩形左侧的X坐标。</param>
        /// <param name="top">矩形顶部的Y坐标。</param>
        /// <param name="right">矩形右侧的X坐标。</param>
        /// <param name="bottom">矩形底部的Y坐标。</param>
        public RECT(int left, int top, int right, int bottom)
        {
            Left = left;
            Top = top;
            Right = right;
            Bottom = bottom;
        }
    }

还有一种比较简单的方法 Graphics.CopyFromScreen ,看看调用 DEMO

private void GraphicsCaptureButton_OnClick(object sender, RoutedEventArgs e)
        {
            var image = CaptureScreen1();
            CaptureImage.Source = ConvertBitmapToBitmapSource(image);
        }
        /// <summary>
        /// 截图屏幕
        /// </summary>
        /// <returns></returns>
        public static Bitmap CaptureScreen1()
        {
            IntPtr desktopWindow = GetDesktopWindow();
            //获取窗口位置大小
            GetWindowRect(desktopWindow, out var lpRect);
            return CaptureScreenByGraphics(00, lpRect.Width, lpRect.Height);
        }
        /// <summary>
        /// 截图屏幕
        /// </summary>
        /// <param name="x">x坐标</param>
        /// <param name="y">y坐标</param>
        /// <param name="width">截取的宽度</param>
        /// <param name="height">截取的高度</param>
        /// <returns></returns>
        public static Bitmap CaptureScreenByGraphics(int x, int y, int width, int height)
        {
            var bitmap = new Bitmap(width, height);
            using var graphics = Graphics.FromImage(bitmap);
            graphics.CopyFromScreen(x, y, 00new System.Drawing.Size(width, height), CopyPixelOperation.SourceCopy);
            return bitmap;
        }

Graphics.CopyFromScreen 调用简单了很多,与 GDI 有什么区别?

Graphics.CopyFromScreen 内部也是通过 GDI.BitBlt 来完成屏幕捕获的,封装了提供更高级别、易用性高的 API

测试了下,第一种方法 GDI32 性能比 Graphics.CopyFromScreen 性能略微好一点,冷启动时更明显点,试了 2 次耗时大概少个 10 多ms。

所以对于一般应用场景,使用 Graphics.CopyFromScreen 就足够了,但如果你需要更高的控制权和性能优化,建议使用 Gdi32.BitBlt

CaptureByGdiDll 效果如下

CaptureByGraphics 效果如下

上面Demo详见:kybs00/CaptureImageDemo (github.com)[1]

参考资料
[1]

kybs00/CaptureImageDemo (github.com): https://github.com/kybs00/CaptureImageDemo


WPF开发者
「WPF开发者」现役微软MVP,专注 WPF 和 Avalonia 技术分享与传播。
 最新文章