前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[WPF]使用WindowChrome自定义Window Style

[WPF]使用WindowChrome自定义Window Style

作者头像
dino.c
发布2019-01-18 10:40:54
2.3K0
发布2019-01-18 10:40:54
举报
文章被收录于专栏:dino.c的专栏

1. 前言

做了WPF开发多年,一直未曾自己实现一个自定义Window Style,无论是《WPF编程宝典》或是各种博客都建议使用WindowStyle="None" AllowsTransparency="True",于是想当然以为这样就可以了。最近来了兴致想自己实现一个,才知道WindowStyle="None" 的方式根本不好用,原因有几点:

  • 如果Window没有阴影会很难看,但自己添加DropShadowEffect又十分影响性能。
  • 需要自定义弹出、关闭、最大化、最小化动画,而自己做肯定不如Windows自带动画高效。
  • 需要实现Resize功能。
  • 其它BUG。

光是性能问题就足以放弃WindowStyle="None" 的实现方式,幸好还有使用WindowChrome的实现方式,但一时之间也找不到理想的实现,连MSDN上的文档( WindowChrome Class )都太过时,.NET 4.5也没有SystemParameters2这个类,只好参考一些开源项目(如 Modern UI for WPF )自己实现了。

2. Window基本功能

Window的基本功能如上图所示。注意除了标准的“最小化”、“最大化/还原”、"关闭"按钮外,Icon上单击还应该能打开窗体的系统菜单,双击则直接关闭窗体。

我想实现类似Office 2016的Window效果:阴影、自定义窗体颜色。阴影、动画效果保留系统默认的就可以了,基本上会很耐看。

大多数自定义Window都有圆角,但我并不喜欢,低DPI的情况下只有几个像素组成的圆角通常都不会很圆滑(如下图),所以保留直角。

另外,激活、非激活状态下标题栏颜色变更:

最终效果如下:

3. 实现

3.1 定义CustomWindow控件

首先,为了方便以后的扩展,我定义了一个名为CustomWindow的模板化控件派生自Window。

代码语言:javascript
复制
public class CustomWindow : Window
{
    public CustomWindow()
    {
        DefaultStyleKey = typeof(CustomWindow);
        CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, CloseWindow));
        CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, MaximizeWindow, CanResizeWindow));
        CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, MinimizeWindow, CanMinimizeWindow));
        CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, RestoreWindow, CanResizeWindow));
        CommandBindings.Add(new CommandBinding(SystemCommands.ShowSystemMenuCommand, ShowSystemMenu));
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        base.OnMouseLeftButtonDown(e);
        if (e.ButtonState == MouseButtonState.Pressed)
            DragMove();
    }

    protected override void OnContentRendered(EventArgs e)
    {
        base.OnContentRendered(e);
        if (SizeToContent == SizeToContent.WidthAndHeight)
            InvalidateMeasure();
    }

    #region Window Commands

    private void CanResizeWindow(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = ResizeMode == ResizeMode.CanResize || ResizeMode == ResizeMode.CanResizeWithGrip;
    }

    private void CanMinimizeWindow(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = ResizeMode != ResizeMode.NoResize;
    }

    private void CloseWindow(object sender, ExecutedRoutedEventArgs e)
    {
        this.Close();
        //SystemCommands.CloseWindow(this);
    }

    private void MaximizeWindow(object sender, ExecutedRoutedEventArgs e)
    {
        SystemCommands.MaximizeWindow(this);
    }

    private void MinimizeWindow(object sender, ExecutedRoutedEventArgs e)
    {
        SystemCommands.MinimizeWindow(this);
    }

    private void RestoreWindow(object sender, ExecutedRoutedEventArgs e)
    {
        SystemCommands.RestoreWindow(this);
    }


    private void ShowSystemMenu(object sender, ExecutedRoutedEventArgs e)
    {
        var element = e.OriginalSource as FrameworkElement;
        if (element == null)
            return;

        var point = WindowState == WindowState.Maximized ? new Point(0, element.ActualHeight)
            : new Point(Left + BorderThickness.Left, element.ActualHeight + Top + BorderThickness.Top);
        point = element.TransformToAncestor(this).Transform(point);
        SystemCommands.ShowSystemMenu(this, point);
    }

    #endregion
}

主要是添加了几个CommandBindings,用于给标题栏上的按钮绑定。

3.2 使用WindowChrome

对于WindowChrome,MSDN是这样描述的:

若要自定义窗口,同时保留其标准功能,可以使用WindowChrome类。 WindowChrome类窗口框架的功能分离开来视觉对象,并允许您控制的客户端和应用程序窗口的非工作区之间的边界。

在CustomWindow的DefaultStyle中添加如下Setting:

代码语言:javascript
复制
<Setter Property="WindowChrome.WindowChrome">
    <Setter.Value>
        <WindowChrome CornerRadius="0"
                      GlassFrameThickness="1"
                      UseAeroCaptionButtons="False"
                      NonClientFrameEdges="None" />
    </Setter.Value>
</Setter>

这样除了包含阴影的边框,整个Window的内容就可以由用户定义了。

3.3 Window基本布局

代码语言:javascript
复制
<Border BorderBrush="{TemplateBinding BorderBrush}"
        BorderThickness="{TemplateBinding BorderThickness}"
        x:Name="WindowBorder">
    <Grid x:Name="LayoutRoot"
          Background="{TemplateBinding Background}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid x:Name="PART_WindowTitleGrid"
              Grid.Row="0"
              Height="26.4"
              Background="{TemplateBinding BorderBrush}">
           ....
        </Grid>
        <AdornerDecorator Grid.Row="1" KeyboardNavigation.IsTabStop="False">
            <ContentPresenter x:Name="MainContentPresenter"
                              KeyboardNavigation.TabNavigation="Cycle" />
        </AdornerDecorator>
        <ResizeGrip x:Name="ResizeGrip"
                    HorizontalAlignment="Right"
                    VerticalAlignment="Bottom"
                    Grid.Row="1"
                    IsTabStop="False"
                    Visibility="Hidden"
                    WindowChrome.ResizeGripDirection="BottomRight" />
    </Grid>
</Border>

Window的标准布局很简单,大致上就是标题栏和内容。 PART_WindowTitleGrid是标题栏,具体内容下一节再讨论。

ContentPresenter的内容即Window的Client Area的范围。

ResizeGrip是当ResizeMode = ResizeMode.CanResizeWithGrip;时出现的Window右下角的大小调整手柄,基本上用于提示窗口可以通过拖动边框改调整小。

AdornerDecorator 为可视化树中的子元素提供 AdornerLayer,如果没有它的话一些装饰效果不能显示(例如下图Button控件的Focus效果),Window的 ContentPresenter 外面套个 AdornerDecorator 是 必不能忘的。

3.4 布局标题栏

代码语言:javascript
复制
<Button x:Name="Minimize"
        ToolTip="Minimize"
        WindowChrome.IsHitTestVisibleInChrome="True"
        Command="{Binding Source={x:Static SystemCommands.MinimizeWindowCommand}}"
        ContentTemplate="{StaticResource MinimizeWhite}"
        Style="{StaticResource TitleBarButtonStyle}"
        IsTabStop="False" />

标题栏上的按钮实现如上,将Command绑定到SystemCommands,并且设置WindowChrome.IsHitTestVisibleInChrome="True",标题栏上的内容要设置这个附加属性才能响应鼠标操作。

代码语言:javascript
复制
<Button VerticalAlignment="Center"
        Margin="7,0,5,0"
        Content="{TemplateBinding Icon}"
        Height="{x:Static SystemParameters.SmallIconHeight}"
        Width="{x:Static SystemParameters.SmallIconWidth}"
        WindowChrome.IsHitTestVisibleInChrome="True"
        IsTabStop="False">
    <Button.Template>
        <ControlTemplate TargetType="{x:Type Button}">
            <Image Source="{TemplateBinding Content}" />
        </ControlTemplate>
    </Button.Template>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <i:InvokeCommandAction Command="{x:Static SystemCommands.ShowSystemMenuCommand}" />
        </i:EventTrigger>
        <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction Command="{x:Static SystemCommands.CloseWindowCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

标题栏上的Icon也是一个按钮,单机打开SystemMenu,双击关闭Window。Height和Widht的值分别使用了SystemParameters.SmallIconHeightSystemParameters.SmallIconWidth,SystemParameters包含可用来查询系统设置的属性,能使用SystemParameters的地方尽量使用总是没错的。

按钮的样式没实现得很好,这点暂时将就一下,以后改进吧。

3.5 处理Triggers

代码语言:javascript
复制
<ControlTemplate.Triggers>
    <Trigger Property="IsActive"
             Value="False">
        <Setter Property="BorderBrush"
                Value="#FF6F7785" />
    </Trigger>
    <Trigger Property="WindowState"
             Value="Maximized">
        <Setter TargetName="Maximize"
                Property="Visibility"
                Value="Collapsed" />
        <Setter TargetName="Restore"
                Property="Visibility"
                Value="Visible" />
        <Setter TargetName="LayoutRoot"
                Property="Margin"
                Value="7" />
    </Trigger>
    <Trigger Property="WindowState"
             Value="Normal">
        <Setter TargetName="Maximize"
                Property="Visibility"
                Value="Visible" />
        <Setter TargetName="Restore"
                Property="Visibility"
                Value="Collapsed" />
    </Trigger>
    <Trigger Property="ResizeMode"
             Value="NoResize">
        <Setter TargetName="Minimize"
                Property="Visibility"
                Value="Collapsed" />
        <Setter TargetName="Maximize"
                Property="Visibility"
                Value="Collapsed" />
        <Setter TargetName="Restore"
                Property="Visibility"
                Value="Collapsed" />
    </Trigger>

    <MultiTrigger>
        <MultiTrigger.Conditions>
            <Condition Property="ResizeMode"
                       Value="CanResizeWithGrip" />
            <Condition Property="WindowState"
                       Value="Normal" />
        </MultiTrigger.Conditions>
        <Setter TargetName="ResizeGrip"
                Property="Visibility"
                Value="Visible" />
    </MultiTrigger>
</ControlTemplate.Triggers>

虽然我平时喜欢用VisualState的方式实现模板化控件UI再状态之间的转变,但有时还是Trigger方便快捷,尤其是不需要做动画的时候。 注意当WindowState=Maximized时要将LayoutRoot的Margin设置成7,如果不这样做在最大化时Window边缘部分会被遮蔽,很多使用WindowChrome自定义Window的方案都没有处理这点。

3.6 处理导航

另一点需要注意的是键盘导航。一般来说Window中按Tab键,焦点会在Window的内容间循环,不要让标题栏的按钮获得焦点,也不要让ContentPresenter 的各个父元素获得焦点,所以在ContentPresenter 上设置KeyboardNavigation.TabNavigation="Cycle"。为了不让标题栏上的各个按钮获得焦点,在各个按钮上还设置了IsTabStop="False"

3.7 DragMove

有些人喜欢不止标题栏,按住Window的任何空白部分都可以拖动Window,只需要在代码中添加DragMove即可:

代码语言:javascript
复制
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);
    if (e.ButtonState == MouseButtonState.Pressed)
        DragMove();
}

3.8 移植TransitioningContentControl

索性让Window打开时内容也添加一些动画。我将Silverlight Toolkit的TransitioningContentControl复制过来,只改了一点动画,并且在OnApplyTemplate()最后添加了这句:VisualStateManager.GoToState(this, Transition, true);。最后将Window中的ContentPresenter 替换成这个控件,效果还不错(实际效果挺流畅的,可是GIF看起来不怎么样):

3.9 SizeToContent问题

有个比较麻烦的问题,当设置SizeToContent="WidthAndHeight",打开Window会出现以下错误。

看上去是内容的Size和Window的Size计算错误,目前的解决方法是在CustomWindow中添加以下代码,简单粗暴,但可能引发其它问题:

代码语言:javascript
复制
protected override void OnContentRendered(EventArgs e)
{
    base.OnContentRendered(e);
    if (SizeToContent == SizeToContent.WidthAndHeight)
        InvalidateMeasure();
}

5. 结语

第一次写Window样式,想不到遇到这么多需要注意的地方。 目前只是个很简单的Demo,没有添加额外的功能,希望对他人有帮助吧。 编码在Window10上完成,只在Windows7上稍微测试了一下,不敢保证兼容性。 如有错漏请指出。

6. 参考

Window Styles and Templates WindowChrome 类 SystemParameters 类 mahapps.metro Modern UI for WPF

7. 源码

GitHub - WindowDemo

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017-09-08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 前言
  • 2. Window基本功能
  • 3. 实现
    • 3.1 定义CustomWindow控件
      • 3.2 使用WindowChrome
        • 3.3 Window基本布局
          • 3.4 布局标题栏
            • 3.5 处理Triggers
              • 3.6 处理导航
                • 3.7 DragMove
                  • 3.8 移植TransitioningContentControl
                    • 3.9 SizeToContent问题
                    • 5. 结语
                    • 6. 参考
                    • 7. 源码
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档