[WPF自定义控件库]使用WindowChrome的问题
1. 前言
上一篇文章介绍了使用windowchrome自定义window,实际使用下来总有各种各样的问题,这些问题大部分都不影响使用,可能正是因为不影响使用所以一直没得到修复(也有可能别人根本不觉得这些是问题)。
这篇文章我总结了一些实际遇到的问题及其解决方案。
2. windowchrome最大化的问题
2.1 影响chrome尺寸的几个值
上一篇文章提到有几个值用于计算chrome的尺寸:
属性 | 值(像素) | 描述 |
---|---|---|
sm_cxframe/sm_cyframe | 4 | the thickness of the sizing border around the perimeter of a window that can be resized, in pixels. sm_cxsizeframe is the width of the horizontal border, and sm_cysizeframe is the height of the vertical border.this value is the same as sm_cxframe. |
sm_cxpaddedborder | 4 | the amount of border padding for captioned windows, in pixels.windows xp/2000: this value is not supported. |
sm_cycaption | 23 | the height of a caption area, in pixels. |
在有标题的标准window,chrome的顶部尺寸为sm_cyframe + sm_cxpaddedborder + sm_cycaption = 31,左右两边尺寸为sm_cxframe + sm_cxpaddedborder = 8,底部尺寸为sm_cyframe + sm_cxpaddedborder = 8。
具体的计算方式可以参考firefox的源码:
// mcaptionheight is the default size of the nc area at // the top of the window. if the window has a caption, // the size is calculated as the sum of: // sm_cyframe - the thickness of the sizing border // around a resizable window // sm_cxpaddedborder - the amount of border padding // for captioned windows // sm_cycaption - the height of the caption area // // if the window does not have a caption, mcaptionheight will be equal to // `getsystemmetrics(sm_cyframe)` mcaptionheight = getsystemmetrics(sm_cyframe) + (hascaption ? getsystemmetrics(sm_cycaption) + getsystemmetrics(sm_cxpaddedborder) : 0); // mhorresizemargin is the size of the default nc areas on the // left and right sides of our window. it is calculated as // the sum of: // sm_cxframe - the thickness of the sizing border // sm_cxpaddedborder - the amount of border padding // for captioned windows // // if the window does not have a caption, mhorresizemargin will be equal to // `getsystemmetrics(sm_cxframe)` mhorresizemargin = getsystemmetrics(sm_cxframe) + (hascaption ? getsystemmetrics(sm_cxpaddedborder) : 0); // mvertresizemargin is the size of the default nc area at the // bottom of the window. it is calculated as the sum of: // sm_cyframe - the thickness of the sizing border // sm_cxpaddedborder - the amount of border padding // for captioned windows. // // if the window does not have a caption, mvertresizemargin will be equal to // `getsystemmetrics(sm_cyframe)` mvertresizemargin = getsystemmetrics(sm_cyframe) + (hascaption ? getsystemmetrics(sm_cxpaddedborder) : 0);
在wpf中这几个值分别映射到systemparameters的相关属性:
系统值 | systemparameters属性 | 值 |
---|---|---|
sm_cxframe/sm_cyframe | windowresizeborderthickness | 4,4,4,4 |
sm_cxpaddedborder | 无 | 4 |
sm_cycaption | windowcaptionheight | 23 |
另外还有windownonclientframethickness,相当于windowresizeborderthickness的基础上,top+=windowcaptionheight,值为 4,27,4,4。
sm_cxpaddedborder在wpf里没有对应的值,我写了个windowparameters的类,添加了这个属性:
/// <summary> /// returns the border thickness padding around captioned windows,in pixels. windows xp/2000: this value is not supported. /// </summary> public static thickness paddedborderthickness { [securitycritical] get { if (_paddedborderthickness == null) { var paddedborder = nativemethods.getsystemmetrics(sm.cxpaddedborder); var dpi = getdpi(); size framesize = new size(paddedborder, paddedborder); size framesizeindips = dpihelper.devicesizetological(framesize, dpi / 96.0, dpi / 96.0); _paddedborderthickness = new thickness(framesizeindips.width, framesizeindips.height, framesizeindips.width, framesizeindips.height); } return _paddedborderthickness.value; } }
2.2 windowchrome的实际大小和普通window不同
先说说我的环境,windows 10,1920 * 1080 分辨率,100% dpi。
<windowchrome.windowchrome> <windowchrome /> </windowchrome.windowchrome> <window.style> <style targettype="{x:type window}"> <setter property="template"> <setter.value> <controltemplate targettype="{x:type window}"> <border> <grid> <adornerdecorator> <contentpresenter /> </adornerdecorator> <resizegrip x:name="windowresizegrip" horizontalalignment="right" istabstop="false" visibility="collapsed" verticalalignment="bottom" /> </grid> </border> <controltemplate.triggers> <multitrigger> <multitrigger.conditions> <condition property="resizemode" value="canresizewithgrip" /> <condition property="windowstate" value="normal" /> </multitrigger.conditions> <setter property="visibility" targetname="windowresizegrip" value="visible" /> </multitrigger> </controltemplate.triggers> </controltemplate> </setter.value> </setter> </style> </window.style>
按上一篇文章介绍的方法打开一个使用windowchrome的window(大小为800 * 600),在visualstudio的实时可视化树可以看到adornerdecorator的实际大小和window的实际大小都是800 * 600(毕竟边windowchrome里的border、grid等都没设margin或padding)。然后用inspect观察它的边框。可以看到window实际上的范围没什么问题。但和标准window的对比就可以看出有区别,我在之前的文章中介绍过标准window的实际范围和用户看到的并不一样。
上面两张图分别是通过inspect观察的标准window(上图)和使用windowchrome的window(下图),可以看到标准window左右下三个方向有些空白位置,和边框加起来是8个像素。windowchrome则没有这个问题。
2.3 最大化状态下margin和标题高度的问题
windowchrome最大化时状态如上图所示,大小也变为1936 * 1066,这个大小没问题,有问题的是它不会计算好client-area的尺寸,只是简单地加大non-client的尺寸,导致client-area的尺寸也成了1936 * 1066。标准window在最大化时non-client area的尺寸为1936 * 1066,client-area的尺寸为1920 * 1027。
2.4 最大化时chrome尺寸的问题
结合window(窗体)的ui元素及行为这篇文章,windowchrome最大化时的client-area的尺寸就是window尺寸(1936 * 1066)减去windownonclientframethickness(4,27,4,4)再减去paddedborderthickness(4,4,4,4)。这样就准确地计算出client-area在最大化状态下的尺寸为1920 * 1027。
在自定义window的controltempalte中我使用trigger在最大化状态下将边框改为0,然后加上windowresizeborderthickness的padding和paddedborderthickness的margin:
<trigger property="windowstate" value="maximized"> <setter targetname="maximizebutton" property="visibility" value="collapsed" /> <setter targetname="restorebutton" property="visibility" value="visible" /> <setter targetname="windowborder" property="borderthickness" value="0" /> <setter targetname="windowborder" property="padding" value="{x:static systemparameters.windowresizeborderthickness}" /> <setter property="margin" targetname="layoutroot" value="{x:static local:windowparameters.paddedborderthickness}" /> </trigger>
以前我还试过让borderthickness保持为1,margin改为7,但后来发现运行在高于100% dpi的环境下出了问题,所以改为绑定到属性。
在不同dpi下这几个属性值如下:
dpi | non-client area 尺寸 | client area 尺寸 | windownonclientframethickness | paddedborderthickness |
---|---|---|---|---|
100 | 1936 * 1066 | 1920 * 1027 | 4,4,4,4 | 4,4,4,4 |
125 | 1550.4 | 1536 | 3.2,3.2,3.2,3.2 | 4,4,4,4 |
150 | 1294.66666666667 | 280 | 3.3333,3.3333,3.3333,3.3333 | 4,4,4,4 |
175 | 1110.85714285714 | 1097.14285714286 | 2.8571428,2.8571428,2.8571428,2.8571428 | 4,4,4,4 |
200 | 973 | 960 | 2.5,2.5,2.5,2.5 | 4,4,4,4 |
可以看到paddedborderthickness总是等于4,所以也可以使用不绑定paddedborderthickness的方案:
<border x:name="windowborder" borderthickness="3" borderbrush="{templatebinding borderbrush}" background="{templatebinding background}" > <border.style> <style targettype="{x:type border}"> <style.triggers> <datatrigger binding="{binding windowstate, relativesource={relativesource templatedparent}}" value="maximized"> <setter property="margin" value="{x:static systemparameters.windowresizeborderthickness}"/> <setter property="padding" value="1"/> </datatrigger> </style.triggers> </style> </border.style>
但我还是更喜欢paddedborderthickness,这是心情上的问题(我都写了这么多代码了,你告诉我直接用4这个神奇的数字就好了,我断然不能接受)。而且有可能将来windows的窗体设计会改变,绑定系统的属性比较保险。
最后,其实应该监视systemparameters的staticpropertychanged事件然后修改paddedborderthickness,因为windownonclientframethickness和windowresizeborderthickness会在系统主题改变时改变,但不想为了这小概率事件多写代码就偷懒了。
3. sizetocontent的问题
sizetocontent属性用于指示window是否自动调整它的大小,但当设置'sizetocontent="widthandheight"'时就会出问题:
上图左面时一个没内容的自定义window,右边是一个没内容的系统window,两个都设置了sizetocontent="widthandheight"
。可以看到自定义windowchorme多出了一些黑色的区域,仔细观察这些黑色区域,发觉它的尺寸大概就是non-client area的尺寸,而且内容就是windowchrome原本的内容。
sizetocontent="widthandheight"
时window需要计算clientarea的尺寸然后再确定window的尺寸,但使用windowchrome自定义window时程序以为整个controltempalte的内容都是clientarea,把它当作了clientarea的尺寸,再加上non-client的尺寸就得出了错误的window尺寸。controletemplate的内容没办法遮住整个windowchrome的内容,于是就出现了这些黑色的区域。
解决方案是在onsourceinitialized时简单粗暴地要求再计算一次尺寸:
protected override void onsourceinitialized(eventargs e) { base.onsourceinitialized(e); if (sizetocontent == sizetocontent.widthandheight && windowchrome.getwindowchrome(this) != null) { invalidatemeasure(); } }
以前我曾建议在oncontentrendered
中执行这段代码,但后来发现调试模式,或者性能比较差的场合会有些问题,所以改为在onsourceinitialized
中执行了。
4. flashwindow的问题
如果一个window设置了owner并且以showdialog的方式打开,点击它的owner将对这个window调用flashwindowex功能,即闪烁几下,并且还有提示音。除了这种方式还可以用编程的方式调用flashwindow功能。
windowchrome提供通知flashwindow发生的事件,flashwindow发生时虽然window看上去在active/inactive 状态间切换,但isactive属性并不会改变。
要处理这个问题,可以监听wm_ncactivate消息,它通知window的non-client area是否需要切换active/inactive状态。
intptr handle = new windowinterophelper(this).handle; hwndsource.fromhwnd(handle).addhook(new hwndsourcehook(wndproc)); protected override void onactivated(eventargs e) { base.onactivated(e); setvalue(isnonclientactivepropertykey, true); } protected override void ondeactivated(eventargs e) { base.ondeactivated(e); setvalue(isnonclientactivepropertykey, false); } private intptr wndproc(intptr hwnd, int msg, intptr wparam, intptr lparam, ref bool handled) { if (msg == windownotifications.wm_ncactivate) setvalue(isnonclientactivepropertykey, wparam == _truevalue); return intptr.zero; }
需要添加一个只读的isnonclientactive依赖属性,controltemplate通过trigger使边框置灰:
<trigger property="isnonclientactive" value="false"> <setter property="borderbrush" value="#ff6f7785" /> </trigger>
5. resizeborder的问题
5.1 resizeborder尺寸的问题
标准window可以单击并拖动以调整窗口大小的区域为8像素(可以理解为sm_cxframe的4像素加上sm_cxpaddedborder的4像素)。
windowchrome实际大小就是看起来的大小,默认的resizeborderthickness是4像素,就是从chrome的边框向内的4像素范围,再多就会影响client-area里各元素的正常使用。
由于标准window的课拖动区域几乎在window的外侧,而且有8个像素,而windowchrome只能有4个像素,所以windowchrome拖动起来手感没那么好。
5.2 拖动边框产生的性能问题
最后提一下windowchrome的性能问题,正常操作我觉得应该没什么问题,只有拖动左右边缘尤其是左边缘改变window大小的时候右边的边缘会很不和谐。其实这个问题不是什么大问题,看看这个空的什么都没有的skype窗体都会这样,所以不需要特别在意。
6. 其它自定义window的方案
在kino.toolkit.wpf里我只提供了最简单的使用windowchrome的方案,这个方案只能创建没有圆角的window,而且不能自定义边框阴影颜色。如果真的需要更高的*度可以试试参考其它方案。
6.1 visualstudio
visualstudio当然没有开源,但并不妨碍我们去参考它的源码。可以在以下dll找到microsoft.visualstudio.platformui.mainwindow
:
x:\program files (x86)\microsoft visual studio\2017\enterprise\common7\ide\microsoft.visualstudio.shell.ui.internal.dll
6.2 firstfloor.modernui
modern ui for wpf (mui),a set of controls and styles converting your wpf application into a great looking modern ui app.
6.3 mahapps.metro
mahapps.metro,a framework that allows developers to cobble together a metro or modern ui for their own wpf applications with minimal effort.
6.4 fluent.ribbon
fluent.ribbon is a library that implements an office-like user interface for the windows presentation foundation (wpf).
6.5 handycontrol
handycontroll是一套wpf控件库,它几乎重写了所有原生样式,同时包含50多款额外的控件,还提供了一些好看的window。
6.6 sakuno.userinterface
sakuno.userinterface,a framework with some powerful tools that allows developers to build a wpf application in modern ui.
7. 参考
windowchrome class (system.windows.shell) microsoft docs
systemparameters class (system.windows) microsoft docs
wpf windows 概述 _ microsoft docs
getsystemmetrics function microsoft docs
flashwindowex function microsoft docs
window class (system.windows) microsoft docs
inspect - windows applications microsoft docs
8. 源码
下一篇: Integer Cache(带你脱坑)
推荐阅读
-
[WPF自定义控件库] 模仿UWP的ProgressRing
-
[WPF自定义控件库]使用TextBlockHighlightSource强化高亮的功能,以及使用TypeConverter简化调用
-
[WPF 学习] 3.用户控件库使用资源字典的困惑
-
[WPF自定义控件库]好用的VisualTreeExtensions
-
[WPF自定义控件库]了解WPF的布局过程,并利用Measure为Expander添加动画
-
[WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互
-
[WPF自定义控件库]简单的表单布局控件
-
[WPF自定义控件库]使用WindowChrome的问题
-
[WPF自定义控件库]使用WindowChrome自定义RibbonWindow
-
[WPF自定义控件]使用WindowChrome自定义Window Style