欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

[WPF自定义控件库]使用WindowChrome的问题

程序员文章站 2022-06-28 21:54:05
1. 前言 "上一篇文章" 介绍了使用WindowChrome自定义Window,实际使用下来总有各种各样的问题,这些问题大部分都不影响使用,可能正是因为不影响使用所以一直没得到修复(也有可能别人根本不觉得这些是问题)。 这篇文章我总结了一些实际遇到的问题及其解决方案。 2. WindowChrom ......

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的实际范围和用户看到的并不一样。

[WPF自定义控件库]使用WindowChrome的问题

[WPF自定义控件库]使用WindowChrome的问题

上面两张图分别是通过inspect观察的标准window(上图)和使用windowchrome的window(下图),可以看到标准window左右下三个方向有些空白位置,和边框加起来是8个像素。windowchrome则没有这个问题。

2.3 最大化状态下margin和标题高度的问题

[WPF自定义控件库]使用WindowChrome的问题

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"'时就会出问题:

[WPF自定义控件库]使用WindowChrome的问题

上图左面时一个没内容的自定义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功能。

[WPF自定义控件库]使用WindowChrome的问题

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尺寸的问题

[WPF自定义控件库]使用WindowChrome的问题

标准window可以单击并拖动以调整窗口大小的区域为8像素(可以理解为sm_cxframe的4像素加上sm_cxpaddedborder的4像素)。

[WPF自定义控件库]使用WindowChrome的问题

windowchrome实际大小就是看起来的大小,默认的resizeborderthickness是4像素,就是从chrome的边框向内的4像素范围,再多就会影响client-area里各元素的正常使用。

由于标准window的课拖动区域几乎在window的外侧,而且有8个像素,而windowchrome只能有4个像素,所以windowchrome拖动起来手感没那么好。

5.2 拖动边框产生的性能问题

最后提一下windowchrome的性能问题,正常操作我觉得应该没什么问题,只有拖动左右边缘尤其是左边缘改变window大小的时候右边的边缘会很不和谐。其实这个问题不是什么大问题,看看这个空的什么都没有的skype窗体都会这样,所以不需要特别在意。

[WPF自定义控件库]使用WindowChrome的问题

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