WPF 自定义控件之自定义窗口

近日由于需要给我这个博客的编辑工具添加一个高亮文本的功能,所以就打开了原先的项目进行修改,在这个过程中我产生了一个新的想法,就是把它的 UI 改成类似 Visual Studio 的样式,所以就有了下面这个新版本:

图 1
图 2

这个窗口里的元素极其简单,就上方一个TextBox,底部一排按钮。唯一的区别就是这个窗口是自定义窗口,按钮是自定义按钮。换句话说,这种样式即使在没有主题的操作系统上依旧可以完美显示。

图 3

上图为窗口右上角按钮样式,从左到右依次为MouseOverMouseLeftButtonDownMouseLeftButtonDown三个事件触发时的样式。

一、窗口的构成要素

构成一个窗口的三大要素是:边框、标题栏、内容容器。所以我就总结了一下实现这三大要素需要用到的一些技术点。既然是自定义,那就彻底点儿,所以,我这个窗口就简单的几个元素组成:

<Border Name = "WinBorder" Style="{StaticResource WindowBorderStyle}">
<DockPanel Margin="0">
<Border Name="TitleBar" Style="{StaticResource TitleBarStyle}" DockPanel.Dock="Top">
<Grid>
<StackPanel Style="{StaticResource TitleBarTitleStackPanelStyle}">
<Image Name="TitleIcon" Style="{StaticResource TitleBarTitleIconStyle}">Image>
<TextBlock Name="TitleText" Style="{StaticResource TitleBarTitleTextStyle}">TextBlock>
StackPanel>
<StackPanel Style="{StaticResource TitleBarButtonStackPanelStyle}">
<Border Name="WinMinButton" Style="{StaticResource WinMinButtonBorderStyle}">
<Image Name="WinMinButtonIcon" Style="{StaticResource WinMinButtonIconStyle}">Image>
Border>
<Border Name="WinMaxButton" Style="{StaticResource WinMaxButtonBorderStyle}">
<Image Name="WinMaxButtonIcon" Style="{StaticResource WinMaxButtonIconStyle}">Image>
Border>
<Border Name="WinCloseButton" Style="{StaticResource WinCloseButtonBorderStyle}">
<Image Name="WinCloseButtonIcon" Style="{StaticResource WinCloseButtonIconStyle}">Image>
Border>
StackPanel>
Grid>
Border>
<AdornerDecorator>
<ContentPresenter>ContentPresenter>
AdornerDecorator>
DockPanel>
Border>

上面这段代码是定义在<Window.Template>中的,我重写了<Window>元素的默认模板以实现自定义窗口。{StaticResource WindowBorderStyle}表示引用静态资源WindowBorderStyle,静态资源在编译时即确定,我预先把它们定义在一个名为WindowStyles.xaml的文件中:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<SolidColorBrush x:Key="WindowBorderBrush" Color="{StaticResource WindowBorderColor}">SolidColorBrush>
<SolidColorBrush x:Key="WindowBackgroudBrush" Color="{StaticResource WindowBackgroudColor}">SolidColorBrush>
<SolidColorBrush x:Key="ControlsBorderColorBrush" Color="{StaticResource ControlsBorderColor}">SolidColorBrush>

<Style x:Key="WindowBorderStyle" TargetType="{x:Type Border}">
<Setter Property="BorderBrush" Value="{StaticResource WindowBorderBrush}">Setter>
<Setter Property="BorderThickness" Value="1">Setter>
<Setter Property="Background" Value="{StaticResource WindowBackgroudBrush}">Setter>
<Setter Property="BitmapEffect">
<Setter.Value>
<OuterGlowBitmapEffect GlowColor="#e5e6ed" GlowSize="5">OuterGlowBitmapEffect>
Setter.Value>
Setter>
Style>
ResourceDictionary>

上面这个就是自定义窗口的边框样式,而WindowBorderColor等样式则单独定义在另外一个XAML文件ControlColors.xaml中:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<Color x:Key="WindowBorderColor">#9b9fb9Color>
<Color x:Key="WindowBackgroudColor">#eeeef2Color>
ResourceDictionary>

这样做的好处是如果以后想改变窗口的边框样式,只需修改ControlColors.xaml文件即可。

二、标题栏

窗口的外边框定义好了,接下来就是定义标题栏,标题栏有两个地方值得一提:

1、Logo图标。

Logo图标我使用的是Image对象:

<Image x:Key="Logo" Source="../res/logo.png">Image>

其中,引用logo图片的方式与CSS里面的引用方式一样,是一种相对的引用方式。那么,还有一种引用方式——绝对引用。绝对引用得这么写:pack://application:,,,/SuziUI.WPF;component/controls/res/logo.png,其中,红色部分是固定写法,不可修改。

另外,通过以上两种方式引用的资源,必须将资源的Build Action属性设置为Resource

2、标题栏右侧按钮。

按钮的样式与窗口的样式定义无差,唯一不一样的就是标题栏按钮的样式是需要通过鼠标事件去触发的。所以,我们需要给这些按钮定义事件,样式有三种,那么事件自然也对应有三种:MouseEnterMouseLeaveMouseLeftButtonDown,分别对应鼠标进入,鼠标移出,鼠标左键按下。

既然是要做成控件,那我们就需要在自定义窗口类里面写了,具体事件的写法我就不说了,比较简单,我来说一下如何在一个自定义控件的模板中找到指定名称的UI元素:继承自Control类的控件都有Template这个属性,通过这个属性我们就可以访问定义在模板里面的UI元素了,例如我想找到Name属性为WinBorder的UI元素,我们就可以在自定义的控件类中像下面这样去访问:

ControlTemplate template = this.Template;
if (template != null) {
var winBorder = template.FindName("WinBorder", this) as Border;
}

找到自定义UI元素后就可以对其进行各种操作了,这里就不再赘述了。这里要强调一点,就是当我们给Template里面的UI元素进行各种初始化操作的时候,我们应该把这些初始化操作写在哪里,写在自定义控件类的构造函数里面行吗?根据我的尝试,不行。那应该写在哪里呢?通过查看Control类的API,我们可以发现它有下面这个方法:

public virtual void OnApplyTemplate();

所以,我们可以在子类中重新该方法,并加入自定义UI元素的初始化操作,如下:

public override void OnApplyTemplate()
{
base.OnApplyTemplate();
InitializeTemplateComponents();
InitializeTemplateEvents();
}

所以,上面这些做完以后,一个标准的自定义窗口就完成了。不过,好像还少了些什么,是的,此时的窗口还没有拖动和Resize功能。下面我就说说这两个功能该怎么实现:

三、拖动与Resize

1、拖动功能

其实拖动功能非常好实现,默认的Window对象已经实现了此功能,我们只需要调用Window对象的一个方法即可:

this.DragMove();

该方法的功能是让窗口跟着你的鼠标移动,所以只要稍微处理一下该方法的启用和停止时机即可实现拖动功能。

2、Resize功能

这个功能我曾尝试过自己通过改变Window对象的宽高,以及左上角的位置来实现,但效果非常不好,并且难以控制。百思不得其解之后,我偶然想到用反编译工具去PresentationFramework.dll中搜索Resize关键字,结果意外的发现有个名为WindowChrome的类,当时觉得挺好奇的,于是就去MSDN上搜了一下,结果一下子就让我震精了,原来这个就是Framework为了让我们更方便的实现自定义Window而提供的一个具备基本Window功能的描述对象。于是照着MSDN上配置一下,就完美的实现了Resize功能。

<Setter Property="WindowChrome.WindowChrome">
<Setter.Value>
<WindowChrome CornerRadius="0"
GlassFrameThickness="1"
UseAeroCaptionButtons="True"
CaptionHeight="0"
NonClientFrameEdges="None">WindowChrome>
Setter.Value>
Setter>

另外,我还找到了获取屏幕宽和高(不包含任务栏)的方法:

获取屏幕工作区的宽度:System.Windows.SystemParameters.WorkArea.Width
获取屏幕工作区的高度:System.Windows.SystemParameters.WorkArea.Height

所以,最终就得到了本文开始展示的窗口。

四、扩展

1、消息对话框

图4

2、输入对话框

图5

3、确认对话框

图6

至于这几种窗口里面的自定义按钮是如何做的,将在下一篇文章《WPF自定义控件之自定义按钮》中为大家展示。

参考文献