WPF 自定义窗口样式有多种方式,不过基本核心实现都是在修改 Win32 窗口样式。然而,Windows 上的应用就应该有 Windows 应用的样子嘛,在保证自定义的同时也能与其他窗口样式保持一致当然能最大程度保证 Windows 操作系统上的体验一致性。

本文将使用 WindowChrome 来自定义窗口样式,使其既保留原生窗口样式和交互习惯,又能够具备一定的自定义空间。


 

使用 Windows 原生窗口体验的应用

在自定义窗口样式的同时保证一致的 Windows 窗口风格体验的优秀应用有这些:

  • Windows 10 UWP 应用

    • 当然少不了 UWP 应用,毕竟这就是 Windows 10 窗口体验的代表
  • Google Chrome
    • 如果我不提第三方应用,你们肯定会说微软都是自己拿内部 API,拿黑科技做的
  • Windows 文件资源管理器
    • Windows 文件资源管理器也有一些自定义(例如在标题栏上放按钮,虽然实际做得很丑),不过整体来说还没 Chrome 做得精致呢


▲ Chrome 普通窗口


▲ Chrome 最大化窗口

为什么不做无边框窗口?

WPF 自定义窗口可是非常容易的,完全自定义样式、异形都不在话下。

<Window x:Class="Walterlv.Whitman.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Walterlv.Whitman"
mc:Ignorable="d" Title="Whitman" Width="800" Height="450"
WindowStyle="None" AllowsTransparency="True">
</Window>

然而,这就不贴近原生窗口体验了,有这么多事情都不好模拟:

  • 最小化、最大化、关闭按钮

    • 按钮要多大?位置在哪里?图标边距又是多少,颜色值又是什么?鼠标滑入划出的动画效果如何?
  • 窗口标题栏交互
    • 标题栏上有右键菜单,如果自己模拟,基本上这个就要自己重新实现了。
  • 窗口的位置和尺寸
    • 你需要自己实现一套窗口的拖拽调整位置功能,需要自己实现一套拖拽调整大小的功能。而自己实现的方式在触摸屏下还很容易出现失效的情况。
  • 窗口的阴影
    • 要完全模拟 Windows 10 上的窗口阴影效果实在是一件头疼的事情,因为并不知道各种阴影参数是多少;就算模拟出来,性能也是个严重的问题。
  • 窗口的边框颜色
    • 虽然窗口边框是被广为吐槽的一点,但为了保证一致的窗口体验,这也是需要模拟的;正常情况和失焦的情况颜色还不一样。
  • 第三方应用集成
    • 第三方截图应用可以毫无障碍地捕捉到标准窗口的外框范围,但如果我们没有模拟好(而是拿一个 WPF 无边框窗口模拟),那么第三方截图应用就截不准(可能会超出窗口本来的大小)。

开始使用 WindowChrome

理论上 WindowChrome 的使用是非常简单的(呃……理论上)。你只需要在 <Window /> 节点里写如下代码便能够完成客户区(Client Area)到非客户区(Non-client Area)的覆盖:

<WindowChrome.WindowChrome>
<WindowChrome />
</WindowChrome.WindowChrome>

然而,默认的行为却并不那么像原生 Windows 10 窗口。事实上,这样的写法只是简单地把窗口的客户区覆盖到非客户区,原生窗口中的交互还在,但样式都已经被遮挡了。


▲ 样式已经被遮挡

不止是样式被遮挡,我们应该能注意相比于原生还有这些不同:

  1. 我们的边框是白色的,原生的边框是系统主题色
  2. 鼠标划入我们窗口内才开始拖拽改变大小,但原生的在阴影区域就能开始调整大小了

现在,为了能够观察到 WindowChrome 各种属性设置的效果,我们为 Window 定义一个新的 Template,里面就是空的,这样就没有什么内容能够遮挡我们设置的样式了。

<Window.Template>
<ControlTemplate TargetType="Window">
<Border />
</ControlTemplate>
</Window.Template>


▲ 没有遮挡的窗口

然而即便如此,我们也只解决了系统主题色边框的问题,没有解决调整窗口的拖拽热区问题。而且边框还如此之丑。

GlassFrameThickness

在官方文档 WindowChrome.GlassFrameCompleteThickness Property (System.Windows.Shell) 中有说,如果指定 GlassFrameThickness 值为 -1,那么可以做到整个窗口都遮挡,但实际上全遮挡的效果也是不对劲的,就像下面这样:

<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="-1" />
</WindowChrome.WindowChrome>


▲ GlassFrameThickness 为 -1

不止边框颜色不见了,连右上角的三个按钮的位置都跟原生不同,这个窗口的位置不贴边。

显然,GlassFrameThickness 属性我们不能指定为 -1。

也不能指定为 0,你可以试试,会发现连阴影都不见了,这更不是我们想要的效果。


▲ GlassFrameThickness 为 0

那我们指定为其他正数呢?


▲ 指定为其他正数

显然,没有一个符合我们的要求。

NonClientFrameEdges

但好在我们还有一个属性可以尝试 —— NonClientFrameEdges。官方文档 WindowChrome.NonClientFrameEdges Property (System.Windows.Shell) 对此的解释是:

Gets or sets a value that indicates which edges of the window frame are not owned by the client.

即指定哪一边不属于客户区。

考虑到我们前面的尝试中发现左、下、右的边框都是不符合要求的,所以我们现在将值设置为 Left,Bottom,Right

<WindowChrome.WindowChrome>
<WindowChrome NonClientFrameEdges="Left,Bottom,Right" />
</WindowChrome.WindowChrome>


▲ 比较接近的效果

这回我们终于看到了比较接近原生窗口的效果了,除了窗口的边框效果在激活和非激活状态下与原生窗口一致,连右上角三个按钮的位置也是贴近原生窗口的。甚至拖拽调整窗口大小时的光标热区也是类似的:


▲ 拖拽光标热区

唯一不符合要求的是标题栏高度,这时我们可以继续设置 GlassFrameThickness,把顶部设置得更高一些。

然而设置到多少呢?我测量了一下 Microsoft Store 应用的按钮高度,是 32。

但是,这 32 包括了顶部 1 像素的边框吗?我使用放大镜查看,发现是包含的。

而我们的 GlassFrameThickness 属性也是包含这个 1 像素边框的。所以含义一致,我们可以考虑直接将 32 设置到属性中:

<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="0 32 0 0" NonClientFrameEdges="Left,Bottom,Right" />
</WindowChrome.WindowChrome>

然而实测结果是 —— 又被耍了,虽然标题栏有 32 的高度,但按钮只有 30 而已:

而且在最大化窗口之后,按钮高度继续压缩。标题栏只剩下 24 的高度,按钮只剩下 22 的高度了。

这显然也模拟得不像。于是,我们霸气一点,直接把顶部边距改得更大。为了凑个整,我写 64 好了。

<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="0 64 0 0" NonClientFrameEdges="Left,Bottom,Right" />
</WindowChrome.WindowChrome>

虽然正常状态下的按钮依然是 30 高度,但最大化时还是 30 高度这一点与原生 UWP 窗口和 Chrome 的行为是类似的。(UWP 窗口按钮 32 高度,最大化 32 高度;Google Chrome 窗口按钮 30 高度,最大化 27 高度。)

所以,截至这里,我们算是模拟得比较像了。

其他的属性需要尝试吗?CornerRadius, ResizeBorderThickness, ResizeGripDirection, UseAeroCaptionButtons 在默认情况下的行为就已经够了;而 IsHitTestVisibleInChrome 是个与 WPF 相关的附加属性,与模拟窗口样式没有关系。所以基本模拟就靠前面的两个属性了。

定制 Window 的控件模板

WindowChrome 提供客户区内容覆盖到非客户区的能力,所以我们通过定制 WindowControlTemplate 能够在保证原生窗口体验的同时,尽可能定制我们的窗口样式。

在按照以上的方式设置了 WindowChrome 之后,我们能够定制的客户区已经有下图所示的这么多了:


▲ 可定制的客户区

特别注意:可定制区域中顶部是包含那 1 像素的边距的,但其他三边不包含。

下面的窗口是我在 冷算法:自动生成代码标识符(类名、方法名、变量名) 中所述算法的一个应用,除了右上角的一个白色块,在保证接近原生窗口的情况下,定制了一些内容。


▲ 一个试验品

为了保证标题栏的标题文字也尽可能地接近原生窗口,我也通过测量得出了用于显示标题的 <TextBlock /> 的各种参数。整理之后,写成了下面的样式:

<Window.Template>
<ControlTemplate TargetType="Window">
<Border Padding="0 30 0 0">
<Grid x:Name="RootGrid" Background="{TemplateBinding Background}">
<Border Background="{TemplateBinding Background}"
VerticalAlignment="Top" Height="30" Margin="0 -29 140 0">
<TextBlock Foreground="White" Margin="16 0" VerticalAlignment="Center"
FontSize="12" Text="{TemplateBinding Title}" />
</Border>
<ContentPresenter />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="WindowState" Value="Maximized">
<Setter TargetName="RootGrid" Property="Margin" Value="6" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Window.Template>

需要注意,我写了一个触发器,当窗口最大化时根元素边距值设为 6。如果不设置,最大化时窗口边缘的像素将看不见。这是反复尝试的经验值,且在多种 DPI 下验证是依然有效的。实际上即便是最合适此时设置的 SystemParameters.WindowResizeBorderThickness 属性依然无法让窗口最大化时边缘距离保持为 0。

标题栏上的三大金刚

我们发现,在以上所有方法尝试完成后,还剩下右上角的三颗按钮的背景色无法定制。如果依然采用非客户区控件覆盖的方法,这三个按钮就会被遮挡,只能自己区模拟了,那是不小的工作量。

然而我们还发现,Google Chrome 是定制了这三个按钮的背景色的,正在研究它的做法。

原生 Windows 窗口体验

UWP 应用对窗口样式的定制能力是非常小的,远远小于传统 Win32 应用。但因为其与系统原生集成,如果要求保证原生窗口体验,UWP 的定制能力又是各种方法里面最大的,而且 API 非常简单。

如果你正在使用 UWP 开发应用,可参考林德熙的博客 win10 uwp 标题栏 来定制标题栏。


参考资料

WPF 使用 WindowChrome,在自定义窗口标题栏的同时最大程度保留原生窗口样式(类似 UWP/Chrome)的更多相关文章

  1. WPF使用WindowChrome实现自定义标题框功能

    代码: <Window x:Class="WpfDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx ...

  2. WPF编程,使用WindowChrome实现自定义窗口功能的一种方法。

    原文:WPF编程,使用WindowChrome实现自定义窗口功能的一种方法. 版权声明:我不生产代码,我只是代码的搬运工. https://blog.csdn.net/qq_43307934/arti ...

  3. WPF自定义界面WindowChrome

    WPF自定义界面WindowChrome 默认WPF的界面其实也还行,就是满足不了日渐增长的需求,界面还是需要有更高的自定义程度,包括标题栏也要能够塞下更多的操作控件. 默认窗口介绍 新建WPF项目, ...

  4. [WPF]使用WindowChrome自定义Window Style

    1. 前言 做了WPF开发多年,一直未曾自己实现一个自定义Window Style,无论是<WPF编程宝典>或是各种博客都建议使用WindowStyle="None" ...

  5. WPF.UIShell UIFramework之自定义窗口的深度技术 - 模态闪动(Blink)、窗口四边拖拽支持(WmNCHitTest)、自定义最大化位置和大小(WmGetMinMaxInfo)

    无论是在工作和学习中使用WPF时,我们通常都会接触到CustomControl,今天我们就CustomWindow之后的一些边角技术进行探讨和剖析. 窗口(对话框)模态闪动(Blink) 自定义窗口的 ...

  6. WPF 之 自定义窗体标题栏

    在WPF中自定义窗体标题栏,首先需要将窗体的WindowStyle属性设置为None,隐藏掉WPF窗体的自带标题栏.然后可以在窗体内部自定义一个标题栏. 例如,标题栏如下: <WrapPanel ...

  7. WPF 使用WindowChrome自定义窗体 保留原生窗体特性

    本文大幅度借鉴dino.c大佬的文章 https://www.cnblogs.com/dino623/p/uielements_of_window.html https://www.cnblogs.c ...

  8. 【Win10 应用开发】自定义应用标题栏

    Win 10 app对窗口标题栏的自定义包括两个层面:一是只定义标题中各部分的颜色,如标题栏上文本的颜色.三个系统按钮(最大化,最小化,关闭)的背景颜色等:另一层是把窗口的可视区域直接扩展到标题栏上, ...

  9. WPF仿网易云音乐系列(二、歌单创建窗口+登录设置模块)

    老衲牺牲午休时间写博客,都快把自己感动了,-_-!! 之前上一篇随笔,我看了下评论,有部分人说WPF已经凉凉了,这个我觉得,这只是一个达到自己目的的工具而已,只要自己能用这个工具,得心应手的做出自己想 ...

随机推荐

  1. 使用 Python 连接 Caché 数据库

    有不少医院的 HIS 系统用的是 Caché 数据库,比如北京协和医院.四川大学华西医院等.用过 Caché 开发的都知道,Caché 数据库的开发维护同我们常见的关系型数据库有很大差别,如 SQL ...

  2. SqlLocalDB命令

    SqlLocalDB info    (查询所有LocalDB实例) SqlLocalDB start 实例名称    (查看某个LocalDB实例状态信息) SqlLocalDB create 实例 ...

  3. 关系型数据库(RDBMS)与 MongoDB 的对应关系

    谈一下关系型数据库(RDBMS)与 MongoDB 的对应关系:

  4. Android----Material Design之(FloatActionButton,CoordinatorLayout,CollapsingToolbarLayout,AppBarLayout,TabLayout等)

    Material Design 的一些UI 平常开发还是用的比较多的,以前没写,最近总结一下,写一篇博客,要求版本在5.0以上. 主要介绍了FloatActionButton,CoordinatorL ...

  5. 设计模式--桥梁模式C++实现

    1定义 将抽象和实现解耦,使得两者可以独立变化 2类图 3实现 #pragma once #include<iostream> using namespace std; class Imp ...

  6. 重新学习MySQL数据库8:MySQL的事务隔离级别实战

    重新学习Mysql数据库8:MySQL的事务隔离级别实战 在Mysql中,事务主要有四种隔离级别,今天我们主要是通过示例来比较下,四种隔离级别实际在应用中,会出现什么样的对应现象. Read unco ...

  7. Django之model字段操作

    # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models import ...

  8. Shell 变量,Shell echo命令

    一.Shell 变量 定义变量时,变量名不加美元符号($,PHP语言中变量需要),如: your_name="runoob.com" 注意,变量名和等号之间不能有空格,这可能和你熟 ...

  9. C# 与vb.net 的Dictionary(字典)的键、值排序

    项目中可能需要用到Dictionary 排序,于是先做了一个小demo ,网上搜索真的没有能满足我需要的,都是类似的,于是理解改造,一上午就在查找,实践中过去了.现在把它实现了,把代码贴出来,算是一个 ...

  10. 十五、dbms_space(分析段增长和空间的需求)

    1.概述 作用:用于分析段增长和空间的需求. 2.包的组成 1).unused_space作用:用于返回对象(表.索引.簇)的未用空间语法:dbms_space.unused_space(segmen ...