解决使用多个屏幕时, v2rayN窗口大于当前窗口的问题 获取窗口所在当前屏幕 WPF与WinForms共存

需求

上一篇的需求 "设置窗口的尺寸, 不要超过当前屏幕" 其实并没有解决得很完满.

在上一篇中, 只解决了使用一个屏幕的情况. 虽然从使用的单词来看, 似乎WorkArea应该指当前操作的屏幕, 但是很遗憾, 它在程序世界中的定义不是单词的语义让我们想象的那样. 很明确, 它的定义是主屏幕的工作区域(除去任务栏, 也就是最底下有开始按钮的那一条).


https://learn.microsoft.com/en-us/dotnet/api/system.windows.systemparameters.workarea?view=windowsdesktop-7.0


解决思路

获取程序运行所在当前屏幕, 根据屏幕的尺寸, 位置和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;
}

保存, 编译, 开始调试(或执行). 

用菜单也可以, 用快捷键也可以, 用快捷按钮也可以.


现在你就可以试试看窗口还会不会显示不全了.


========


正确答案放在GitHub上

https://github.com/crazypeace/v2rayN/tree/WindowsForms


========

解决过程

我问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

我去问GPT了很多次, 很多不同的问法. 但它一直回答的方案都是使用System.Windows.Forms命名空间, 我猜测可能它吃的原料不够广, 或者因为到它采集语料为止的程序员们都喜欢使用这种方案. 

而且它还说, 在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的结果都是说不要放在构造函数中, 而是在窗口加载完后获取这个值.

https://stackoverflow.com/questions/11204251/presentationsource-fromvisualthis-returns-null-value-in-wpf


好了, 对这些没有Window_Loaded函数的窗口, 我们就自己添加, 再把调整窗口大小的代码搬进去.

编译, 执行, 测试.  发现, 在小屏幕生成的窗口, 拖到大屏幕上, 想显示多一点内容, 拖窗口边框拖不动, 窗口最大化但还是只显示那么一点内容. 

原因是我们指定了窗口尺寸的最大值.

修改为, 调整窗口当前尺寸不超过屏幕.

// 设置窗口尺寸不超过当前屏幕的尺寸
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;
}


编译, 执行, 测试. 看起来一切正常了.


评论

The Hot3 in Last 30 Days