解决使用多个屏幕时, v2rayN窗口大于当前窗口的问题 获取窗口所在当前屏幕 WPF与WinForms共存
需求
上一篇的需求 "设置窗口的尺寸, 不要超过当前屏幕" 其实并没有解决得很完满.
在上一篇中, 只解决了使用一个屏幕的情况. 虽然从使用的单词来看, 似乎WorkArea应该指当前操作的屏幕, 但是很遗憾, 它在程序世界中的定义不是单词的语义让我们想象的那样. 很明确, 它的定义是主屏幕的工作区域(除去任务栏, 也就是最底下有开始按钮的那一条).
解决思路
获取程序运行所在当前屏幕, 根据屏幕的尺寸, 位置和DPI, 调整窗口的属性.
具体实践
安装 Microsoft Visual Studio
在 Microsoft Store 里有
https://apps.microsoft.com/store/detail/XPDCFJDKLZJLP8
安装组件选 .NET 桌面开发
下载源码
https://github.com/2dust/v2rayN
解压, 进入 v2ray-master 目录, 进入 v2rayN 目录, 打开 .sln 文件.
修改源码
打开 .sln 文件.
引入 WinForms (System.Windows.Forms)
我搜索了很多, 在使用多个屏幕时, 在纯WPF环境下没有办法获取程序运行的当前屏幕, 只找到用WinForms的方案, 或者用WIN32API.
在项目.proj文件的属性里面, 可以打开对WinForms的支持.
解决引入WinForms后的编译问题
双击编译的错误提示, 打开代码文件以后, 把下面这一段直接增加到文件的头部.
using Application = System.Windows.Application;using Clipboard = System.Windows.Clipboard;using DataFormats = System.Windows.DataFormats;using DataObject = System.Windows.DataObject;using DragDropEffects = System.Windows.DragDropEffects;using DragEventArgs = System.Windows.DragEventArgs;using FontFamily = System.Windows.Media.FontFamily;using IDataObject = System.Windows.IDataObject;using KeyEventArgs = System.Windows.Input.KeyEventArgs;using MessageBox = System.Windows.MessageBox;using MouseEventArgs = System.Windows.Input.MouseEventArgs;using OpenFileDialog = Microsoft.Win32.OpenFileDialog;using Point = System.Windows.Point;using SaveFileDialog = Microsoft.Win32.SaveFileDialog;using TextBox = System.Windows.Controls.TextBox;using UserControl = System.Windows.Controls.UserControl;
不断的编译, 报错, 打开报错的文件, 把这一大段声明丢进去, 再编译... 最终就会全部编译通过了.
在窗口的Windows_Loaded函数中修改窗口属性
修改窗口尺寸不能在构造函数中进行, 因为获取不到DPI缩放信息.
在所有的窗口代码文件里找Window_Loaded函数. 添加设置窗口属性的代码.
比如, 绿色部分是新增的.
如果窗口代码文件没有Windows_Loaded函数的窗口, 那么需要添加这一函数.
在 Owner 后面添加一行
this.Loaded += Window_Loaded;
再利用智能提示的功能, 生成方法"Window_Loaded".
自动帮你生成了函数的架子. 把内容删掉, 填写我们自己的.
// 获取屏幕的 DPI 缩放因素double dpiFactor = 1;PresentationSource source = PresentationSource.FromVisual(this);if (source != null){dpiFactor = source.CompositionTarget.TransformToDevice.M11;}// 获取当前屏幕的尺寸var screen = System.Windows.Forms.Screen.FromHandle(new System.Windows.Interop.WindowInteropHelper(this).Handle);var screenWidth = screen.WorkingArea.Width / dpiFactor;var screenHeight = screen.WorkingArea.Height / dpiFactor;var screenTop = screen.WorkingArea.Top / dpiFactor;var screenLeft = screen.WorkingArea.Left / dpiFactor;var screenBottom = screen.WorkingArea.Bottom / dpiFactor;var screenRight = screen.WorkingArea.Right / dpiFactor;// 设置窗口尺寸不超过当前屏幕的尺寸if (this.Width > screenWidth){this.Width = screenWidth;}if (this.Height > screenHeight){this.Height = screenHeight;}// 设置窗口不要显示在屏幕外面if (this.Top < screenTop){this.Top = screenTop;}if (this.Left < screenLeft){this.Left = screenLeft;}if (this.Top + this.Height > screenBottom){this.Top = screenBottom - this.Height;}if (this.Left + this.Width > screenRight){this.Left = screenRight - this.Width;}
保存, 编译, 开始调试(或执行).
用菜单也可以, 用快捷键也可以, 用快捷按钮也可以.
解决过程
我问GPT如何在使用多个屏幕时, 程序窗口尺寸不要超过当前所在屏幕的尺寸.
代码搬过来.
编译的时候报错.
错误 CS0234 命名空间“System.Windows”中不存在类型或命名空间名“Forms”(是否缺少程序集引用?) v2rayN D:\_work\v2rayN-master1\v2rayN\v2rayN\Views\AddServerWindow.xaml.cs 177
我试过在代码顶部声明System.Windows.Forms命名空间.
但是编译还是不行.
错误 CS0234 命名空间“System.Windows”中不存在类型或命名空间名“Forms”(是否缺少程序集引用?) v2rayN D:\_work\v2rayN-master1\v2rayN\v2rayN\Views\AddServerWindow.xaml.cs 10
而且它还说, 在WPF环境下也是可以使用System.Windows.Forms命令空间的. 我对.net编程没有涉猎过, 所以我使用我的编程直觉认为WPF和System.Windows.Forms是冲突的, 认为GPT应该是产生了AI幻觉. 然后就在这里卡了很久.
我换了好多种表达方法去问GPT, 就是只得到这一些相似的回答. 我开始猜测也许真的没办法在WPF环境下做到这个效果. 因为GPT甚至有几次回答时把WIN32API都搬出来了.
后来我发现这其实是一个突破口, 我经过google和询问GPT, 找到了让WPF环境可以使用System.Windows.Forms命令空间的方法. 在项目.proj文件的属性里面, 可以打开对System.Windows.Forms的支持.
但是打开这个设置以后, 编译的时候就冒出来好多错误.
有一点编程基础的人看了就明白, 肯定是在WPF和System.Windows.Forms这两个命令空间中, 有一些重名的定义.
双击错误, 跳转到报错的地方. 有一个给你提示如何修改的功能.
点击这个按钮也行, 使用快捷键也行, 使用这个功能. 选下面这一项.
因为:
首先, 这是在项目中引用System.Windows.Forms之间就可以编译的代码, 所以肯定要指定引用不是System.Windows.Forms的那一个定义.
其次, 写成using的形式, 可以只在这个代码文件的头部写一次. 如果是非using的形式, 那么在每次使用到这个定义时都要显式声明, 太麻烦了.
不断地在每一个这样的报错的地方, 选择using不是System.Windows.Forms的定义的方式, 修改完, 再编译. 再报错, 再修改... 最终就可以编译通过.
这个声明反正写得多, 没用到是没毛病的. 所以我趟过了一遍坑之后, 给你一个简单粗暴的方法 ---- 你双击错误提示打开代码文件以后, 把下面这一段直接增加到文件的头部.
using Application = System.Windows.Application;using Clipboard = System.Windows.Clipboard;using DataFormats = System.Windows.DataFormats;using DataObject = System.Windows.DataObject;using DragDropEffects = System.Windows.DragDropEffects;using DragEventArgs = System.Windows.DragEventArgs;using FontFamily = System.Windows.Media.FontFamily;using IDataObject = System.Windows.IDataObject;using KeyEventArgs = System.Windows.Input.KeyEventArgs;using MessageBox = System.Windows.MessageBox;using MouseEventArgs = System.Windows.Input.MouseEventArgs;using OpenFileDialog = Microsoft.Win32.OpenFileDialog;using Point = System.Windows.Point;using SaveFileDialog = Microsoft.Win32.SaveFileDialog;using TextBox = System.Windows.Controls.TextBox;using UserControl = System.Windows.Controls.UserControl;
不断的编译, 报错, 把这一大段声明丢进去, 再编译... 最终就会全部编译通过了.
运行起来看看效果? 不行.
鼠标在这个位置点一下, 在代码中设置一个断点/breakpoint, 在VS Studio里面显示为一个棕红色的圆点. 它的作用是程序运行到这一句时会暂停下来.
再次使用添加服务器功能, 唤出窗口. 可以看到程序暂停在这里. 注意观察正文的局部变量窗口. 可以看到变量 screenWidth 的值 为1920, 变量screenHeight 的值 为1080.
如果你手里有上一篇的方案实施效果成功的调试环境, 你也可以在类似的位置设置断点. 但是默认没有显示 this.MaxWidth 和 this.MaxHeight 的值.
当然鼠标移上去, 会有显示, 但是如果长期调试工作, 还是在窗口有显示这些你关心的变量的值比较好.
在下图中箭头所指的地方鼠标右键, 菜单中选择"添加监视".
把this.MaxWidth 和 this.MaxHeight添加进去, 可以看到它们的值 是 1280 和 673 . 再联想到DPI 为 175%, 那么说明 System.Windows.Forms 的方法是按像素点计算的, 而WPF是经过了DPI比例转换的.
去问GPT如何查询系统的DPI 或 屏幕缩放比例.
不能完全照抄, 稍微阅读一下. 写到代码里这样子:
再编译, 执行, 测试一下. 嗯. 又出现了窗口下边框"缩起来"了的情况.
(我的测试环境是一个大屏幕, 一个小屏幕. 大屏幕为主屏幕. 小屏幕1080p设置为175%)
还是在代码里去看断点暂停时的各个变量. 发现很奇怪的, 窗口的左上角都在屏幕外面了, 但是top属性却不是负数.
再结合我的两个屏幕的环境思考.
这个窗口应该是下面这么个情况, 所以 top 属性不是负数.
这样的话, 我们要得到那个屏幕的top.
代码改成这样:
// 设置窗口不要显示在屏幕外面if (this.Top < screenTop / dpiFactor){this.Top = screenTop / dpiFactor;}
再测试. 嗯, 没问题了!
实施到每一个窗口
在所有的Windows代码文件里的Window_Loaded函数里也这样修改就行了.
这里面不是每个文件里都有Window_Loaded函数的.
比如, DNS设置窗口就没有这个函数.
找到初始化函数/构造函数/Constructor , 就是 public 后面跟着的就是窗口的名称的那个位置.
在 Owner 后面添加一行
this.Loaded += Window_Loaded;
再利用智能提示的功能, 生成方法"Window_Loaded".
自动帮你生成了函数的架子. 把内容删掉, 填写我们自己的.
这里也许你会问, 为什么不能像上一篇方案中那样写到初始化函数/构造函数/Constructor里面. 这样不是省事了吗?
好, 我们以主窗口为例, 看一下写到构造函数里是什么效果.
主窗口是这个:
我们做如此修改:
编译生成的可执行文件在这个位置
v2rayN-master1\v2rayN\v2rayN\bin\Debug\net6.0-windows
执行. 乍一看挺好. 但, 如果我们把窗口拖到大屏幕上把尺寸拉高, 然后关闭, 然后在小屏幕的文件管理器上打开可执行文件, 你会发现窗口高度超过屏幕了.
我们的代码没起作用吗? 在代码中打断点, 看看究竟.
发现, 获取窗口的DPI时, 变量为空/null. 在编程的世界里, 空/null的意思就是不存在.
看来在窗口显示出来之前, 是获取不到DPI的.
问goolge和GPT的结果都是说不要放在构造函数中, 而是在窗口加载完后获取这个值.
编译, 执行, 测试. 发现, 在小屏幕生成的窗口, 拖到大屏幕上, 想显示多一点内容, 拖窗口边框拖不动, 窗口最大化但还是只显示那么一点内容.
原因是我们指定了窗口尺寸的最大值.
修改为, 调整窗口当前尺寸不超过屏幕.
// 设置窗口尺寸不超过当前屏幕的尺寸if (this.Width > screenWidth / dpiFactor){this.Width = screenWidth / dpiFactor;}if (this.Height > screenHeight / dpiFactor){this.Height = screenHeight / dpiFactor;}
再构造不同的大小屏幕之间的位置关系测试.
把小屏幕设置为主屏幕, 把窗口拖到大屏幕上调整为大尺寸, 再退出程序. 然后在小屏幕上打开, 这样程序会在小屏幕上显示.
发现只判断了窗口的top属性是不够的. 窗口的左上角可能会在各种各样奇奇怪怪的位置.
所以应该要判断窗口的整个区域都要在屏幕的范围内.
假设我们对窗口相关的知识一无所知, 去问GPT.
学习关键逻辑, 代码改成这样:
// 获取屏幕的 DPI 缩放因素double dpiFactor = 1;PresentationSource source = PresentationSource.FromVisual(this);if (source != null){dpiFactor = source.CompositionTarget.TransformToDevice.M11;}// 获取当前屏幕的尺寸var screen = System.Windows.Forms.Screen.FromHandle(new System.Windows.Interop.WindowInteropHelper(this).Handle);var screenWidth = screen.WorkingArea.Width / dpiFactor;var screenHeight = screen.WorkingArea.Height / dpiFactor;var screenTop = screen.WorkingArea.Top / dpiFactor;var screenLeft = screen.WorkingArea.Left / dpiFactor;var screenBottom = screen.WorkingArea.Bottom / dpiFactor;var screenRight = screen.WorkingArea.Right / dpiFactor;// 设置窗口尺寸不超过当前屏幕的尺寸if (this.Width > screenWidth){this.Width = screenWidth;}if (this.Height > screenHeight){this.Height = screenHeight;}// 设置窗口不要显示在屏幕外面if (this.Top < screenTop){this.Top = screenTop;}if (this.Left < screenLeft){this.Left = screenLeft;}if (this.Top + this.Height > screenBottom){this.Top = screenBottom - this.Height;}if (this.Left + this.Width > screenRight){this.Left = screenRight - this.Width;}
编译, 执行, 测试. 看起来一切正常了.
评论
发表评论