C# wpf 实现截屏框热键截屏功能
wpf截屏系列
第一章 使用GDI+实现截屏
第二章 使用DockPanel制作截屏框
第三章 实现截屏框热键截屏(本章)
第四章 实现截屏框实时截屏
第五章 使用ffmpeg命令行实现录屏
文章目录
- wpf截屏系列
- 前言
- 一、实现步骤
- 1、响应热键
- 2、截屏显示
- (1)获取屏幕区域
- (2)截取并显示
- 3、自动捕获窗口
- (1)获取系统所有窗口
- (2)根据鼠标位置搜索窗口
- (3)效果预览
- 2、点击拖出截屏框
- (1)移动到点击位置
- (2)模拟按下事件
- (3)修正偏移
- (4)效果预览
- 3、反向拖动
- (1)判断边界
- (2)事件转移
- (3)修正边界
- (4)效果预览
- 4、截取图片
- 5、设置粘贴板
- 二、关于dpi
- 1、适配不同dpi
- 2、不支持dpi实时修改
- (1)现象
- (2)尝试的解决方案
- 3、建议
- 三、完整代码
- 四、效果预览
- 1、截屏粘贴到qq
- 2、截屏保存到文件
- 总结
前言
在《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);
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-codesHotKey k = new HotKey(this, HotKey.KeyFlags.MOD_ALT, 0x44);k.OnHotKey += K_OnHotKey;Visibility = Visibility.Collapsed;}
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);/// <summary> /// 获取真实设置的桌面分辨率大小 /// </summary> 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;}}
(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);
}
3、自动捕获窗口
qq和微信的截屏都有自动捕获窗口功能,我们也可以自己实现这种功能。
(1)获取系统所有窗口
通过win32 api可以枚举系统所有窗口,我们需要将所有窗口的位置大小记录下来,网上可以找到WindowList相关代码此处略。
//获取桌面所有窗口
_windows = WindowList.GetAllWindows();
IntPtr hwnd = new WindowInteropHelper(this).Handle;
//去除不可见窗口以及自己
_windows.RemoveAll((ele) => { return !ele.isVisible || ele.Handle == hwnd; });
(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 { }}
}
(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;
(2)模拟按下事件
接着上面的代码,thumb为右下角拖动点。
//手动触发截屏框滑块拖动事件
MouseButtonEventArgs downEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left)
{ RoutedEvent = FrameworkElement.MouseLeftButtonDownEvent };
thumb.RaiseEvent(downEvent);
(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);
}
在Thumb的DragDelta事件中添加修正逻辑
var horizontalChange = e.HorizontalChange;
var verticalChange = e.VerticalChange;
if (_thumbOffset != null)
//修正偏移
{horizontalChange += _thumbOffset.Value.X;verticalChange += _thumbOffset.Value.Y;
}
(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
}
纵向的Thumb
if (height >= 0)
{topPanel.Height = top >= 0 ? top : 0;bottomPanel.Height = bottom >= 0 ? bottom : 0;
}
else
{
//此处将事件转移到反方向的Thumb
}
(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);
(3)修正边界
完成上述两步之后已经可以做到反向拖动了,但是会有个问题,当多动过快的时截屏框的位置会发生移动,要解决这个问题则需要在事件转移时修正边界位置,即使两条边重合。
横向的Thumb
if (thumb.HorizontalAlignment == HorizontalAlignment.Left)
//从左到右转移的修正
{leftPanel.Width = grdGlobal.ActualWidth - rightPanel.Width;
}
else
//从右到左转移的修正
{rightPanel.Width = grdGlobal.ActualWidth - leftPanel.Width;
}
纵向的Thumb
if (thumb.VerticalAlignment == VerticalAlignment.Top)//从上到下转移的修正{topPanel.Height = grdGlobal.ActualHeight - bottomPanel.Height;}else//从下到上转移的修正{bottomPanel.Height = grdGlobal.ActualHeight - topPanel.Height;}
(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;
}
5、设置粘贴板
直接使用Clipboard.SetImage即可,参数类型为BitmapSource,是WriteableBitmap的基类。
Clipboard.SetImage(GetClipImage());
二、关于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接口笔者没有发现。所以这个问题目前只能通过独立程序启动解决。但是总的来说实现的效果是很不错的,尤其是反向拖动,通过事件转移的方式实现,界面操作还是很流畅。
相关文章:

C# wpf 实现截屏框热键截屏功能
wpf截屏系列 第一章 使用GDI实现截屏 第二章 使用DockPanel制作截屏框 第三章 实现截屏框热键截屏(本章) 第四章 实现截屏框实时截屏 第五章 使用ffmpeg命令行实现录屏 文章目录 wpf截屏系列前言一、实现步骤1、响应热键2、截屏显示(1&#…...

springboot + activiti实现activiti微服务化
概述 本文介绍如何将springbootactiviti进行整合,并配合eureka,zuul和feign实现activiti的微服务化,将流程控制和业务逻辑分离. 并实现了几个比较特殊的功能,比如时间段委托(某人请假或出差,出差时间内,所有待办交给被委托人处理),比如节点的无限级加签功能(流程本身有不确定性…...

c语言练习41:深入理解字符串函数strlen strcpy strcat
深入理解字符串函数strlen strcpy strcat 模拟实现:”strlen strcpy strcat strlen strcat: #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<assert.h> strlen 1.通过指针移动模拟 //int my_strlen(char* str) { // size_t c…...

Vue3+Vue-i18n+I18N ALLY+VSCODE 自动翻译多国语言
ps: 效果图放前面,符合的往下看,不符合的出门右转,希望多多点赞评论支持。 三种语言模式,分别是中文、英文、日文 批量翻译 最后的结果 配置vue-i18n 1、下载安装vue-i18n,9以上的版本。 2、创建对应文件夹 3、对应文件夹中代…...

idea意外退出mac
目录 问题描述 解决过程 问题描述 mac上的idea我很久没用了,之前用的时候还是发布新版的开源项目,这几天再用的时候,就出现了idea意外退出的问题,我上网查找了很久,对于我的问题都没有很好的解决。 解决过程 在寻求…...

百度智能云千帆大模型平台2.0来了!从大模型到生产力落地的怪兽级平台!!
目录 前言 最佳算力效能为企业降低门槛 最多大模型,最多数据集为企业保驾护航 企业级安全对于企业来说是硬性要求 前言 普通人或许感知不明显,但是对于企业而言,身处AI时代,是否选择投资大模型,是否拥抱人工智能…...
k8s nfs-client 添加挂载参数 —— 筑梦之路
背景介绍 为什么要使用noresvport参数挂载NAS?不重新挂载会有什么后果? 如果发生网络切换或者后端服务的HA倒换,小概率会造成NFS文件系统阻塞,那就可能需要几分钟时间连接才会自动恢复,极端情况下甚至需要重启ECS才能恢…...

【算法】堆排序 详解
堆排序 详解 堆排序代码实现 排序: 排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。 稳定性: 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,…...

解决Maven依赖下载问题:从阿里云公共仓库入手
🌷🍁 博主猫头虎(🐅🐾)带您 Go to New World✨🍁 🦄 博客首页——🐅🐾猫头虎的博客🎐 🐳 《面试题大全专栏》 🦕 文章图文…...
【Java基础】学习笔记2 - 数组运算符与main方法
目录 多态数组运算符hashCodefinalize 方法 第三阶段类变量类方法main 方法代码块单例模式饥饿式懒汉式 多态数组 顾名思义,就是在一个数组内体现多态 public class PolyArrDemo {public static void main(String[] args) {// 定义多态数组Fruit[] fruits new Fr…...

stable diffusion实践操作-复制-清空-保存提示词
系列文章目录 stable diffusion实践操作 stable diffusion实践操作-webUI教程 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 系列文章目录前言一、右上生成图标附近按钮介绍1. 箭头介绍(复现别人的…...

【Spring 事务和事务传播机制】
目录 1 事务概述 1.1 为什么需要事务 1.2 事务的特性 1.3 Spring 中事务的实现 2 Spring 声明式事务 2.1 Transactional 2.2 Transactional 的作用范围 2.3 Transactional 的各种参数 2.3.1 ioslation 2.4 事务发生了异常,也不回滚的情况 异常被捕获时 3 事务的传…...

【爬虫】实验项目二:模拟登录和数据持久化
目录 一、实验目的 二、实验预习提示 三、实验内容 实验要求 基本要求: 改进要求A: 改进要求B: 四、实验过程 基本要求: 源码如下: 改进要求A: 源码如下: 改进要求B: 源码如下&…...

图文版:以太网二层接口类型(含配套习题)
常见的以太网二层接口类型包括以下三种: 一、Access接口 access链路类型端口,一种交换机的主干道模式,2台交换机的2个端口之间是否能够建立干道连接,取决于这2个端口模式的组合。 Access端口在收到以太网帧后打开VLAN标签&#…...

生信豆芽菜-机器学习筛选特征基因
网址:http://www.sxdyc.com/mlscreenfeature 一、使用方法 1、准备数据 第一个文件:特征表达数据 第二个文件:分组信息,第一列为样本名,第二列为患者分组 第三个文件:分析基因名 2、选择机器学习的方…...

v-html富文本里面的图片设置宽高不起作用的原因
把scoped去掉...

pdf文档怎么压缩小一点?文件方法在这里
在日常工作和生活中,我们经常会遇到需要上传或者发送pdf文档的情况。但是,有时候pdf文档的大小超出了限制,需要我们对其进行压缩。那么,如何将pdf文档压缩得更小一点呢?下面,我将介绍三种方法,让…...

CMD关闭占用端口
1. netstat -ano | findstr :xxxx 2. taskkill /pid xxxx 3. 强制关闭taskkill/F /pid xxxx...

复制粘贴是怎么实现的
在上面的代码中,command 和 select 是自定义的函数。它们的作用如下: 实现复制粘贴的思路: 创建一个 textarea 标签将 textarea 移出可视区域给这个 textarea 赋值将这个 textarea 标签添加到页面中调用 textarea 的 select 方法调用 docum…...

mybatisplus多租户原理略解
概述 当前mybatisPlus版本 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.2</version> </dependency>jdk版本:17 springboot版本:…...
《Playwright:微软的自动化测试工具详解》
Playwright 简介:声明内容来自网络,将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具,支持 Chrome、Firefox、Safari 等主流浏览器,提供多语言 API(Python、JavaScript、Java、.NET)。它的特点包括&a…...

ardupilot 开发环境eclipse 中import 缺少C++
目录 文章目录 目录摘要1.修复过程摘要 本节主要解决ardupilot 开发环境eclipse 中import 缺少C++,无法导入ardupilot代码,会引起查看不方便的问题。如下图所示 1.修复过程 0.安装ubuntu 软件中自带的eclipse 1.打开eclipse—Help—install new software 2.在 Work with中…...
AspectJ 在 Android 中的完整使用指南
一、环境配置(Gradle 7.0 适配) 1. 项目级 build.gradle // 注意:沪江插件已停更,推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...

在Mathematica中实现Newton-Raphson迭代的收敛时间算法(一般三次多项式)
考察一般的三次多项式,以r为参数: p[z_, r_] : z^3 (r - 1) z - r; roots[r_] : z /. Solve[p[z, r] 0, z]; 此多项式的根为: 尽管看起来这个多项式是特殊的,其实一般的三次多项式都是可以通过线性变换化为这个形式…...
NPOI操作EXCEL文件 ——CAD C# 二次开发
缺点:dll.版本容易加载错误。CAD加载插件时,没有加载所有类库。插件运行过程中用到某个类库,会从CAD的安装目录找,找不到就报错了。 【方案2】让CAD在加载过程中把类库加载到内存 【方案3】是发现缺少了哪个库,就用插件程序加载进…...

PHP 8.5 即将发布:管道操作符、强力调试
前不久,PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5!作为 PHP 语言的又一次重要迭代,PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是,借助强大的本地开发环境 ServBay&am…...

android RelativeLayout布局
<?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:gravity&…...

uniapp 小程序 学习(一)
利用Hbuilder 创建项目 运行到内置浏览器看效果 下载微信小程序 安装到Hbuilder 下载地址 :开发者工具默认安装 设置服务端口号 在Hbuilder中设置微信小程序 配置 找到运行设置,将微信开发者工具放入到Hbuilder中, 打开后出现 如下 bug 解…...