专栏首页dino.c的专栏[WPF自定义控件库]使用WindowChrome自定义RibbonWindow

[WPF自定义控件库]使用WindowChrome自定义RibbonWindow

1. 为什么要自定义RibbonWindow

自定义Window有可能是设计或功能上的要求,可以是非必要的,而自定义RibbonWindow则不一样:

  • 如果程序使用了自定义样式的Window,为了统一外观需要把RibbonWindow一起修改样式。
  • 为了解决RibbonWindow的BUG。

如上图所示,在Windows 10 上运行打开RibbonWindow,可以看到标题栏的内容(包括分隔符)没有居中对齐,缺少下边框。

在最大化的时候标题栏内容甚至超出屏幕范围。

WPF提供的Ribbon是个很古老很古老的控件,附带的RibbonWindow也十分古老。RibbonWindow在以前应该可以运行良好,但多年没有更新,在.NET 4.5(或者说是WIN7平台,我没仔细考究)后就出现了这个问题。作为专业软件这可能没法接受,而这个问题微软好像也没打算修复。以前的做法通常是使用Fluent.Ribbon之类的第三方组件,因为我已经在Kino.Toolkit.Wpf中提供了使用WindowChrome自定义的Window,为了统一外观于是顺手自定义一个ExtendedRibbonWindow

2. 问题产生的原因

RibbonWindow是派生自Window,并使用了WindowChrome,它的ControlTemplate大概是这样:

<Grid>
    <Border Name="PART_ClientAreaBorder"
            Background="{TemplateBinding Control.Background}"
            BorderBrush="{TemplateBinding Control.BorderBrush}"
            BorderThickness="{TemplateBinding Control.BorderThickness}"
            Margin="{Binding Path=(SystemParameters.WindowNonClientFrameThickness)}" />
    <Border BorderThickness="{Binding Path=(WindowChrome.WindowChrome).ResizeBorderThickness, RelativeSource={RelativeSource TemplatedParent}}">
        <Grid>
            <Image Name="PART_Icon"
                   WindowChrome.IsHitTestVisibleInChrome="True"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top"
                   Width="{Binding Path=(SystemParameters.SmallIconWidth)}"
                   Height="{Binding Path=(SystemParameters.SmallIconHeight)}" />
            <AdornerDecorator>
                <ContentPresenter Name="PART_RootContentPresenter" />
            </AdornerDecorator>
            <ResizeGrip Name="WindowResizeGrip"
                        WindowChrome.ResizeGripDirection="BottomRight"
                        HorizontalAlignment="Right"
                        VerticalAlignment="Bottom"
                        Visibility="Collapsed"
                        IsTabStop="False" />
        </Grid>
    </Border>
</Grid>

Ribbon的Chrome部分完全依赖于WindowChrome,PART_ClientAreaBorder负责为ClientArea提供背景颜色。在PART_ClientAreaBorder后面的另一个Border才是真正的ClientArea部分,它用于放置Ribbon。因为Ribbon的一些按钮位于标题栏,所以Ribbon必须占用标题栏的位置,并且由Ribbon显示原本应该由Window显示的标题。WindowChrome的标题栏高度是SystemParameters.WindowNonClientFrameThickness.Top,在Windows 10,100% DPI的情况下为27像素。而Ribbon标题栏部分使用了SystemParameters.WindowCaptionHeight作为高度,这个属性的值为23,所以才会出现对不齐的问题。

<DockPanel Grid.Column="0" Grid.ColumnSpan="3" LastChildFill="True" Height="{Binding Path=(SystemParameters.WindowCaptionHeight)}">

而最大化的时候完全没有调整Ribbon的Margin,并且WindowChrome本身在最大化就会有问题。所以不能直接使用WindowChrome,而应该使用自定义的UI覆盖WindowChrome的内容。

3. 自定义RibbonWindow

我在Kino.Toolkit.Wpf提供了一个自定义RibbonWindow,基本上代码和ControlTempalte与自定义Window一样,运行效果如上图所示。在自定义RibbonWindow里我添加了RibbonStyle属性,默认值是一个解决Ribbon标题栏问题的Ribbon样式,里面使用SystemParameters.WindowNonClientFrameThickness作为标题的高度。

<DockPanel Grid.Column="0"
           Grid.ColumnSpan="3"
           Margin="0,-1,0,0"
           Height="{Binding Path=(SystemParameters.WindowNonClientFrameThickness).Top}"
           LastChildFill="True">

RibbonWindow还添加了一个StyleTypedProperty:

[StyleTypedProperty(Property = nameof(RibbonStyle), StyleTargetType = typeof(Ribbon))]

StyleTypedProperty 应用于类定义并确定类型为 TargetType 的属性的 Style。使用了这个属性的控件可以在Blend中使用 "右键"->"编辑其他模板"->"编辑RibbonSytle" 创建Ribbon的Style。

不过虽然我这么贴心地加上这个Attribute,但我的Blend复制Ribbon模板总是报错。

4. 结语

我也见过一些很专业的软件没处理RibbonWindow,反正外观上的问题忍一忍就过去了,实在受不了可以买一个有现代化风格的控件库,只是为了标题栏对不齐这种小事比较难说服上面同意引入一个新的组件。除了使用我提供的解决方案,stackoverflow也由不少关于这个问题的讨论及解决方案可供参考,例如这个:

c# - WPF RibbonWindow + Ribbon = Title outside screen - Stack Overflow

顺便一提,ExtendedRibbonWindow需要继承RibbonWindow,所以没法直接集成ExtendedWindow。因为ExtendedWindow很多功能都试用附加属性和控件代码分离,所以ExtendedRibbonWindow需要重复的代码不会太多。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互

    WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件的外观。例设我需要一个控件在鼠标进入的时候背景变成蓝色,我可以用下面这段代码实现:

    dino.c
  • [WPF 自定义控件]自定义一个“传统”的 Validation.ErrorTemplate

    数据绑定模型允许您将与您Binding的对象相关联ValidationRules。 如果用户输入的值无效,你可能希望在应用程序 用户界面 (UI) 上提供一些有...

    dino.c
  • [UWP]为番茄钟应用设计一个平平无奇的状态按钮

    OnePomodoro应用里有个按钮用来控制计时器的启动/停止,本来这应该是一个包含“已启动”和“已停止”两种状态的按钮,但我以前在WPF和UWP上做过太多St...

    dino.c
  • 1048576GB地图数据,AI技术酷炫渲染,《微软飞行模拟器》游戏即将上线

    作为即将成为第一款将整个地球化作虚拟世界来供玩家玩的游戏,微软的《微软飞行模拟器》(Microsoft Flight Simulator)游戏地图利用卫星和无人...

    AI科技大本营
  • Notes for python (2)

    #!/usr/bin/python # Filename: using_tuple.py zoo = ('wolf', 'elephant', 'pe...

    py3study
  • TDD跟单元测试的关系如何处理?

    TDD和单元测试不是一个维度的概念。TDD是一种软件开发方法,单元测试是软件开发中的一种产物。简单区分就是:TDD是怎么做事情,单元测试是做什么事情。单元测试也...

    袁慎建@ThoughtWorks
  • 学点基本功:机器学习常用损失函数小结

    。而损失函数(Loss Function)则是这个过程中关键的一个组成部分,用来衡量模型的输出

    AI科技大本营
  • Excel量化分析案例:洪水疏浚渠道工程扩建项目方案比较

    某地区的洪水疏浚项目目前具备700立方英尺的疏浚能力,经过工程分析和历史数据研究,得到不同渠道容量下发生洪水的概率,并给出不同建设方案的投资额,如下表:

    沉默的白面书生
  • 使用SAP CRM Mock framework进行单元测试

    There is an interface IF_CRM_PRODUCT_PROXY in CRM which declares almost all func...

    Jerry Wang
  • 解决laravel session失效的问题

    最新在学习laravel,用到了session,因为laravel没法用$_SESSION 所以只能用框架的session。

    砸漏

扫码关注云+社区

领取腾讯云代金券