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. 源码

kino.toolkit.wpf_window at master