• C# wpf 实现截屏框热键截屏功能


    wpf截屏系列

    第一章 使用GDI实现截屏
    第二章 使用GDI+实现截屏
    第三章 使用DockPanel制作截屏框
    第四章 实现截屏框热键截屏(本章)
    第五章 实现截屏框实时截屏
    第六章 使用ffmpeg命令行实现录屏



    前言

    《C# wpf 使用DockPanel实现截屏框》中我们实现了一个截屏框,接下来就要实现相应的截屏功能了。获取截屏区域然后使用GDI+截屏,在这里不少的细节需要处理,比如响应热键弹出截屏界面、点击拖出截屏框、截屏区域任意反向拖动、处理不同dpi下的坐标位置等等。


    一、实现步骤

    1、响应热键

    我们直接使用win32 api的RegisterHotKey和UnregisterHotKey即可。在Window的SourceInitialized事件中注册热键,如下是注册alt+d为热键的示例代码

    [System.Runtime.InteropServices.DllImport("user32")]
    private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint controlKey, uint virtualKey);
    
    [System.Runtime.InteropServices.DllImport("user32")]
    private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    HotKey是对RegisterHotKey、UnregisterHotKey做了封装的对象,网上可以搜到此处略。

     private void Window_SourceInitialized(object sender, EventArgs e)
     {
         //注册alt+d热键,0x44为d,其他虚拟键值请查看:https://learn.microsoft.com/zh-tw/windows/win32/inputdev/virtual-key-codes
         HotKey k = new HotKey(this, HotKey.KeyFlags.MOD_ALT, 0x44);
         k.OnHotKey += K_OnHotKey;
         Visibility = Visibility.Collapsed;
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2、截屏显示

    (1)获取屏幕区域

    我们需要使用win32 api获取屏幕区域,采用wpf的方法取得的屏幕分辨率是基于dpi的,就算是用PointToScreen进行转换,在程序运行过程中改了系统dpi后依然会不准确,所以需要直接取得屏幕的实际像素分辨率,用于gdi+截屏。

      const int DESKTOPVERTRES = 117;
            const int DESKTOPHORZRES = 118;
            [DllImport("gdi32.dll")]
            static extern int GetDeviceCaps(
       IntPtr hdc, // handle to DC  
       int nIndex // index of capability  
       );
            [DllImport("user32.dll")]
            static extern IntPtr GetDC(IntPtr ptr);
            [DllImport("user32.dll", EntryPoint = "ReleaseDC")]
            static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDc);
            ///   
            /// 获取真实设置的桌面分辨率大小  
            ///   
            static Size DESKTOP
            {
                get
                {
                    IntPtr hdc = GetDC(IntPtr.Zero);
                    Size size = new Size();
                    size.Width = GetDeviceCaps(hdc, DESKTOPHORZRES);
                    size.Height = GetDeviceCaps(hdc, DESKTOPVERTRES);
                    ReleaseDC(IntPtr.Zero, hdc);
                    return size;
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    (2)截取并显示

    利用上面步骤获取到的截屏区域,结合《C# wpf 使用GDI+实现截屏》里的简单截屏即完成。取得Bitmap对象后,参考我的另一篇文章《C# wpf Bitmap转换成WriteableBitmap(BitmapSource)的方法》将其转换为转换成wpf对象,然后通过ImageBrush赋值为控件的Background即可以显示在控件上。

    //截屏并显示到窗口
    void Snapshot()
    {
        //获取桌面实际分辨率,可以解决程序运行后修改dpi,截图区域不正常的问题
        var leftTop = new Point(0, 0);
        var rightBottom = new Point(DESKTOP.Width, DESKTOP.Height);
        var bitmap = Snapshot((int)leftTop.X, (int)leftTop.Y, (int)(rightBottom.X - leftTop.X), (int)(rightBottom.Y - leftTop.Y));
        var bmp = BitmapToWriteableBitmap(bitmap);
        bitmap.Dispose();
        //显示到窗口
        grdGlobal.Background = new ImageBrush(bmp);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3、自动捕获窗口

    qq和微信的截屏都有自动捕获窗口功能,我们也可以自己实现这种功能。

    (1)获取系统所有窗口

    通过win32 api可以枚举系统所有窗口,我们需要将所有窗口的位置大小记录下来,网上可以找到WindowList相关代码此处略。

    //获取桌面所有窗口
    _windows = WindowList.GetAllWindows();
    IntPtr hwnd = new WindowInteropHelper(this).Handle;
    //去除不可见窗口以及自己
    _windows.RemoveAll((ele) => { return !ele.isVisible || ele.Handle == hwnd; });
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (2)根据鼠标位置搜索窗口

    //窗口是以z顺序排列的查找到第一个匹配的窗口即可
    var screenPoint = grdGlobal.PointToScreen(point);
    foreach (var window in _windows)
    {
        if (window.rect.Contains(screenPoint))
        //获取在鼠标所在区域的窗口
        {
            try
            {
                if (window.rect.Right > window.rect.Left && window.rect.Bottom > window.rect.Top)
                //
                {
                    var topLeft = grdGlobal.PointFromScreen(window.rect.TopLeft);
                    var bottomRight = grdGlobal.PointFromScreen(window.rect.BottomRight);
                    Thickness thickness = new Thickness(topLeft.X, topLeft.Y, grdGlobal.ActualWidth - bottomRight.X, grdGlobal.ActualHeight - bottomRight.Y);
                     //修正边界
                    if (thickness.Left < 0) thickness.Left = 0;
                    if (thickness.Top < 0) thickness.Top = 0;
                    if (thickness.Right < 0) thickness.Right = 0;
                    if (thickness.Bottom < 0) thickness.Bottom = 0;
                    //将截屏框显示在窗口位置
                    leftPanel.Width = thickness.Left;
                    topPanel.Height = thickness.Top;
                    rightPanel.Width = thickness.Right;
                    bottomPanel.Height = thickness.Bottom;
                    break;
                }
            }
            catch { }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    (3)效果预览

    在这里插入图片描述

    2、点击拖出截屏框

    出现截屏界面之后,参考qq或微信的实现,第一次点击是可以拖出截屏框框选的。如果是采样绘制的方法很简单,直接绘制矩形就可以了。但是基于控件要实现这个功能需要一定的技巧,在《C# wpf 使用DockPanel实现截屏框》的基础上实现这个功能。

    (1)移动到点击位置

    在鼠标按下事件或移动实现中

    //将截屏框移动到点击位置
    leftPanel.Width = p.X;
    topPanel.Height = p.Y;
    rightPanel.Width = grdGlobal.ActualWidth - p.X;
    bottomPanel.Height = grdGlobal.ActualHeight - p.Y;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (2)模拟按下事件

    接着上面的代码,thumb为右下角拖动点。

    //手动触发截屏框滑块拖动事件
    MouseButtonEventArgs downEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left)
    { RoutedEvent = FrameworkElement.MouseLeftButtonDownEvent };
    thumb.RaiseEvent(downEvent);
    
    • 1
    • 2
    • 3
    • 4

    (3)修正偏移

    由于是模拟的点击事件,可能会出现鼠标不在Thumb上的情况,此时需要对thumb位置进行修正,在Thumb的DragStarted事件中记录偏移。

    //滑块需要的偏移量
    Point? _thumbOffset;
    var thumb = sender as FrameworkElement;
    if (!new Rect(0, 0, thumb.ActualWidth, thumb.ActualHeight).Contains(new Point(e.HorizontalOffset, e.VerticalOffset)))
    //鼠标起始位置超出了控件范围,则记录中心点偏移在拖动时修正
    {
        _thumbOffset = new Point(e.HorizontalOffset - thumb.ActualWidth / 2, e.VerticalOffset - thumb.ActualHeight / 2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在Thumb的DragDelta事件中添加修正逻辑

    var horizontalChange = e.HorizontalChange;
    var verticalChange = e.VerticalChange;
    if (_thumbOffset != null)
    //修正偏移
    {
        horizontalChange += _thumbOffset.Value.X;
        verticalChange += _thumbOffset.Value.Y;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    (4)效果预览

    在这里插入图片描述

    3、反向拖动

    这一步不是必须的,但是有的话操作体验会更好,比如qq和微信的截图就支持反向拖动。如果我们使用gdi或gdi+绘制截屏框则天然支持反向拖动,因为RECT的大小可以为负数。但是基于控件则有一定的难度了,由于控件宽高不能为负数,我们需要实现事件转移机制,依然是在《C# wpf 使用DockPanel实现截屏框》的基础上实现这个功能。

    (1)判断边界

    原本《C# wpf 使用DockPanel实现截屏框》的逻辑的Thumb到了边界就不进行任何操作了,现在要拓展为到达边界则进行事件转移。
    横向的Thumb

    if (width >= 0)
    {
        leftPanel.Width = left >= 0 ? left : 0;
        rightPanel.Width = right >= 0 ? right : 0;
    }
    else{
    //此处将事件转移到反方向的Thumb
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    纵向的Thumb

    if (height >= 0)
    {
        topPanel.Height = top >= 0 ? top : 0;
        bottomPanel.Height = bottom >= 0 ? bottom : 0;
    }
    else
    {
    //此处将事件转移到反方向的Thumb
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    (2)事件转移

    //当前的Thumb触发鼠标弹起事件,结束拖动
    MouseButtonEventArgs upEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left)
    { RoutedEvent = FrameworkElement.MouseLeftButtonUpEvent };
    thumb.RaiseEvent(upEvent);
    //反方向的Thumb触发鼠标按下事件,开始拖动
    MouseButtonEventArgs downEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left)
    { RoutedEvent = FrameworkElement.MouseLeftButtonDownEvent };
    t.RaiseEvent(downEvent);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    (3)修正边界

    完成上述两步之后已经可以做到反向拖动了,但是会有个问题,当多动过快的时截屏框的位置会发生移动,要解决这个问题则需要在事件转移时修正边界位置,即使两条边重合。
    横向的Thumb

    if (thumb.HorizontalAlignment == HorizontalAlignment.Left)
    //从左到右转移的修正
    {
        leftPanel.Width = grdGlobal.ActualWidth - rightPanel.Width;
    }
    else
    //从右到左转移的修正
    {
        rightPanel.Width = grdGlobal.ActualWidth - leftPanel.Width;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    纵向的Thumb

     if (thumb.VerticalAlignment == VerticalAlignment.Top)
     //从上到下转移的修正
     {
         topPanel.Height = grdGlobal.ActualHeight - bottomPanel.Height;
     }
     else
     //从下到上转移的修正
     {
         bottomPanel.Height = grdGlobal.ActualHeight - topPanel.Height;
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    (4)效果预览

    在这里插入图片描述

    4、截取图片

    由于前面截取是整个桌面的图像,保存时需要根据截屏框截取画面,我们使用WriteableBitmap对象就可以实现。

    //获取截屏框的图片
    WriteableBitmap GetClipImage()
    {
        var bursh = grdGlobal.Background as ImageBrush;
        if (bursh != null)
        {
            //裁剪
            //全屏图片
            var screenWb = bursh.ImageSource as WriteableBitmap;
            //获取截取区域
            var leftTop = clipRect.PointToScreen(new Point(0, 0));
            var rightBottom = clipRect.PointToScreen(new Point(clipRect.ActualWidth, clipRect.ActualHeight));
            var rect = new Int32Rect((int)leftTop.X, (int)leftTop.Y, (int)(rightBottom.X - leftTop.X), (int)(rightBottom.Y - leftTop.Y));
            //创建截取图片对象
            var wb = new WriteableBitmap(rect.Width, rect.Height, 0, 0, screenWb.Format, null);
            //写入截取区域数据
            wb.WritePixels(rect, screenWb.BackBuffer, screenWb.PixelHeight * screenWb.BackBufferStride, screenWb.BackBufferStride, 0, 0);
            return wb;
        }
        return null;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    5、设置粘贴板

    直接使用Clipboard.SetImage即可,参数类型为BitmapSource,是WriteableBitmap的基类。

     Clipboard.SetImage(GetClipImage());
    
    • 1

    二、关于dpi

    1、适配不同dpi

    有处理dpi不同的情况,在任意dpi下都能正常截图。

    2、不支持dpi实时修改

    (1)现象

    程序启动后实时修改dpi,截屏显示的画面会模糊,主要原因是不同api之间的dpi计算不统一。系统dpi实时修改后wpf界面会响应oloaded自动调整大小,但部分程序内部的dpi(比如getWindowRect)是不会变化的,尤其是渲染图片依然按照程序启动时的dpi去计算,所以会进行缩放,显示的画面必然模糊。
    这里举一个具体的例子流程如下:
    win11 分辨率1920x1080
    1、初始系统dpi为120(1.25倍)
    2、程序启动
    3、程序dpi为120
    5、全屏窗口大小1536x864,通过win32 api获取则是1920x1080,截屏1920x1080显示,截屏画面无损
    6、系统dpi设置为96(1倍)
    7、此时程序dpi为120
    8、全屏窗口大小1920x1080,通过win32 api获取则是2400x1350,截屏1920x1080显示,截屏画面模糊。
    按像素点绘制,画面显示在左上角无法充满窗口。

    (2)尝试的解决方案

    笔者采样了多种方式尝试解决
    1、提前缩放图片再显示。
    2、参考微软解决dpi问题的方法。
    3、使用gdi+的graphics直接通过hdc以像素点为单位绘制。
    4、使用gdi的bitblt进行hdc拷贝。
    以上方法都没效果画面依然模糊

    3、建议

    需要支持dpi实时改变,可以将截图功能作为单独的程序,响应热键后再启动。


    三、完整代码

    https://download.csdn.net/download/u013113678/88308050
    说明:截屏的操作方式和qq、微信差不多,目前设置的热键为alt+d。


    四、效果预览

    1、截屏粘贴到qq

    在这里插入图片描述

    2、截屏保存到文件

    在这里插入图片描述


    总结

    以上就是今天要讲的内容,本文介绍了wpf截屏框热键截屏的方法。需要实现的功能还是比较多的,而且有些功能难度也不小,几经尝试才找到合适的实现方法,至于实时改变dpi的模糊的问题,这个目前的结论是无法解决,这并不是wpf的局限,用c++ mfc也不行,除非存在一个设置程序全局dpi的win32 api接口笔者没有发现。所以这个问题目前只能通过独立程序启动解决。但是总的来说实现的效果是很不错的,尤其是反向拖动,通过事件转移的方式实现,界面操作还是很流畅。

  • 相关阅读:
    网络-Ajax
    vscode debug时 stepinto 无法跳进代码
    js字符串对比之localeCompare()方法-对字符串进行排序——大于0-升序、小于0-降序 & 对el-table的列进行排序sort-change
    hot100-课程表
    字典服务的设计与管理
    计数与期望
    cassandra-Altering of types is not allowed
    网络安全入门教程(非常详细)从零基础入门到精通,看完这一篇就够了。
    Spring Data JPA系列5:让IDEA自动帮你写JPA实体定义代码
    qt example plugandpaint 插件 动态库 pnp_extrafiltersd.dll无法加载问题
  • 原文地址:https://blog.csdn.net/u013113678/article/details/132484304