自定义 Window
在客户端程序中,经常需要用到自定义一个 Window ,大部分是为了好看吧。做了很多研究和实践之后,觉得需要把这个过程写下来,以供查阅。
WPF 提供的丰富的功能使得自定义 Window 变得简单,但是也不是一个简单的 Style 就能做到的事情。虽然 WPF 中的控件是 Lookless 的,但是 Window 类有他自己的特殊之处,做个简单的实验就能看出,对于普通的 WPF 控件,用 XamlWriter.Write 方法就能将某个类型对象的模板输出出来,这样就可以看到该控件的内部构造。但是如果输出 Window 对象的默认模板,就会发现模板非常简单,其中并没有包含标题栏以及最大化最小化按钮的定义,具体的实现不得而知,但是至少说明 Window 的默认 Style 不是按照 WPF 的规范来实现的。
为了实现任意风格的 Window 就需要重写 Window 的默认模板,第一步要做的就是创建 Window 的一个派生类,并创建自定义 Style ,然后重写 DefaultStyleKey 属性让 WPF 引擎来将样式和 Window 的派生类装载到一起。具体做法如下:
1. 创建一个 WPF Application 项目
2. 在项目中添加 Themes 文件夹,并在该文件夹下添加名称为 Generic.xaml 的 ResourceDictionary 。该文件夹和文件的名称和位置都是固定的,也就是说必须这么做才能让 WPF 引擎让自定义控件和默认 Style 协同工作,因为微软对此进行了硬编码。
3. 创建自定义窗口类为 HeaderedWindow ,为什么定义为 HeaderedWindow 呢?因为我觉得 Window 对象更像是一个 HeaderedContentControl 而不是一个 ContentControl ,因为它的标题栏更像是一个 Header ,而不仅仅是为了显示图标和标题! HeaderedWindow 类的定义如下:
public class HeaderedWindow : Window
{
static HeaderedWindow()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof (HeaderedWindow ), new FrameworkPropertyMetadata (typeof (HeaderedWindow )));
}
}
4. 在 Generic.xaml 文件中添加 HeaderedWindow 的默认样式,如下:
< Style TargetType ="{ x : Type l : HeaderedWindow }">
< Setter Property ="WindowStyle" Value ="None"/>
< Setter Property ="ResizeMode" Value ="NoResize"/>
< Setter Property ="Background" Value ="Gray"/>
< Setter Property ="BorderBrush" Value ="#FF5A3D1C"/>
< Setter Property ="BorderThickness" Value ="1"/>
< Setter Property ="MinWidth" Value ="90"/>
< Setter Property ="MinHeight" Value ="33"/>
< Setter Property ="VerticalContentAlignment" Value ="Stretch"/>
< Setter Property ="HorizontalContentAlignment" Value ="Stretch"/>
</ Style >
最重要的两个属性是 WindowStyle 和 ResizeMode ,将 WindowStyle 属性设置为 None 将会去除默认的标题栏,将 ResizeMode 设置为 NoResize 将会去除 Window 边框,该边框在 Window 7 比较难看,不过却有他的作用, 就是用来调整窗口大小,我们把它去掉了,就意味着我们需要自己来编写调整窗口大小的代码了。
5. 测试一下这个 HeaderedWindow 吧,编写如下测试代码:
< local : HeaderedWindow x : Class ="CustomWindowDemo.MainWindow"
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns : x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns : local ="clr-namespace:CustomWindowDemo"
Title ="MainWindow" Height ="350" Width ="525">
< Grid >
</ Grid >
</ local : HeaderedWindow >
后置代码类必须要从 HeaderedWindow 继承才行。运行程序发现窗口黑乎乎一片。这是因为我们还没有重写 Window 的 ControlTemplate 的缘故。
6. 一个窗口应该有如下基本要素
a) 图标,就是 Icon 属性,自定义窗口应该有将其显示出来的能力
b) 标题,就是 Title 属性,自定窗口应该将其显示在标题栏中
c) 最大化,最小化,关闭按钮
d) 为了美观, Window 的边框应该定义出来。但是当窗口最大化的时候应该隐藏边框,扩大窗口的使用面积,这也是 Windows 下窗口的标准行为。
e) 八个方向的 Resizer 应该定义出来,这些 Resize 其实就是一些透明的 Thumb 控件,用来支持拖动事件,并改变窗口的定位和高宽。
对于 HeaderedWindow 还有几个扩展要素需要提供:
a) 自定义 Header 属性,用户可以重新定义 Window 的 Header ,在这里我们将 Window 的 Icon 和 Title 定义为他的默认 Header ,所以有一个 ShowDefaultHeader 属性用来控制默认 Header 的可见性。 Header 属性是一个 Object 对象,其用法和 HeaderedContentControl 控件的 Header 几乎一模一样。
b) ShowResizeGrip 属性,用来控制是否在右下角显示一个抓手模样的图形,用来指示用此处可以拖动,当然不显示也可以调整窗口大小,不过显示这个抓手增加了易用性。
c) CanResize 属性,由于我们在样式中将窗口设置 ResizeMode 为 NoResize ,所以需要提供一个额外的属性用来设置当前窗口是否可以调整大小。当 CanResize 为 False 的时候,所有可以调整窗口大小的控件将被隐藏。
d) 最后一个比较有意思的属性就是 IsFullScreenMaximize ,当该属性设置为 True 的时候,那么当最大化的时候,窗口将把任务栏覆盖住。是真正意义上的“最大化”了。
7. 如果想制作不规则形状的窗口,需要将 AllowTransparency 属性设置为 true ,不过当此属性为 true 的时候,该 WPF 窗口将不能作为 WinForm 控件或者 ActiveX 控件的宿主了。所以需要慎重考虑。
最后附上全部代码以及使用方式,以供查阅!
Generic.xaml 的内容:
< ResourceDictionary xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns : x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns : sys ="clr-namespace:System;assembly=mscorlib"
xmlns : l ="clr-namespace:CustomWindowDemo">
< l : BoolToVisibilityConverter x : Key ="BoolToVisibilityConverter"></ l : BoolToVisibilityConverter >
< Style x : Key ="styleWindowButtonMinimize" BasedOn ="{ x : Null }" TargetType ="{ x : Type Button }">
< Setter Property ="Template">
< Setter.Value >
< ControlTemplate TargetType ="{ x : Type Button }">
< Grid x : Name ="buttonClose">
< Ellipse Stroke ="{ x : Null }" StrokeThickness ="1" x : Name ="btnEllipse" >
< Ellipse.Fill >
< RadialGradientBrush >
< GradientStop Color ="#BFABA7A4" Offset ="0.777"/>
< GradientStop Color ="#FF897F77" Offset ="1"/>
</ RadialGradientBrush >
</ Ellipse.Fill >
</ Ellipse >
< Path x : Name ="iconMin" Width ="11.1641" Height ="2.06641" Canvas.Left ="3.97266" Canvas.Top ="8.51962" Stretch ="Fill" Fill ="#FFFFFFFF" Data ="F1 M 5.00659,8.51962C 4.43613,8.51831 3.97266,8.98184 3.97266,9.55347C 3.97266,10.1238 4.435,10.586 5.00659,10.586L 14.1028,10.586C 14.6733,10.586 15.1353,10.1238 15.1367,9.55215C 15.1367,8.98056 14.6733,8.51962 14.1028,8.51962L 5.00659,8.51962 Z "/>
< ContentPresenter SnapsToDevicePixels ="{ TemplateBinding SnapsToDevicePixels }" HorizontalAlignment ="{ TemplateBinding HorizontalContentAlignment }" VerticalAlignment ="{ TemplateBinding VerticalContentAlignment }" RecognizesAccessKey ="True"/>
</ Grid >
< ControlTemplate.Triggers >
< Trigger Property ="IsFocused" Value ="True"/>
< Trigger Property ="IsDefaulted" Value ="True"/>
< Trigger Property ="IsMouseOver" Value ="True">
< Setter Property ="Fill" Value ="#FF5D4E3E" TargetName ="btnEllipse"/>
</ Trigger >
< Trigger Property ="IsPressed" Value ="True"/>
< Trigger Property ="IsEnabled" Value ="False"/>
</ ControlTemplate.Triggers >
</ ControlTemplate >
</ Setter.Value >
</ Setter >
</ Style >
< Style x : Key ="styleWindowButtonMaximize" BasedOn ="{ x : Null }" TargetType ="{ x : Type ToggleButton }">
< Setter Property ="Template">
< Setter.Value >
< ControlTemplate TargetType ="{ x : Type ToggleButton }">
< Grid >
< Ellipse x : Name ="btnEllipse" Stroke ="{ x : Null }" StrokeThickness ="1">
< Ellipse.Fill >
< RadialGradientBrush >
< GradientStop Color ="#BFABA7A4" Offset ="0.777"/>
< GradientStop Color ="#FF897F77" Offset ="1"/>
</ RadialGradientBrush >
</ Ellipse.Fill >
</ Ellipse >
< Path x : Name ="iconMax" Width ="12.4011" Height ="9.93359" Stretch ="Fill" Fill ="#FFFFFFFF" Data ="F1 M 390.641,289.034C 389.073,289.034 387.8,290.308 387.8,291.875L 387.8,296.125C 387.8,297.692 389.073,298.967 390.641,298.967L 397.362,298.967C 398.927,298.967 400.201,297.692 400.201,296.125L 400.201,291.875C 400.201,290.308 398.927,289.034 397.362,289.034L 390.641,289.034 Z M 389.399,296.125L 389.399,291.875C 389.399,291.19 389.956,290.634 390.641,290.634L 397.362,290.634C 398.045,290.634 398.602,291.19 398.602,291.875L 398.602,296.125C 398.602,296.81 398.045,297.367 397.362,297.367L 390.641,297.367C 389.956,297.367 389.399,296.81 389.399,296.125 Z "/>
< Path x : Name ="iconRestore" Visibility ="Collapsed" Width ="11.6719" Height ="9.48242" Stretch ="Fill" Fill ="#FFFFFFFF" Data ="F1 M 411.826,302.421C 411.802,301.775 411.28,301.259 410.629,301.259L 404.483,301.259C 403.836,301.257 403.312,301.775 403.286,302.421L 403.277,302.421C 403.277,302.421 403.277,303.254 403.277,304.177L 407.126,304.177C 407.984,304.177 408.679,304.853 408.722,305.728L 408.722,307.822L 410.629,307.822C 411.294,307.822 411.834,307.283 411.836,306.615C 411.836,306.507 411.836,302.421 411.836,302.421L 411.826,302.421 Z M 407.126,304.956L 400.979,304.956C 400.545,304.955 400.19,305.301 400.164,305.728L 400.164,309.925C 400.164,310.374 400.531,310.741 400.981,310.741L 407.126,310.741C 407.575,310.741 407.942,310.374 407.944,309.923L 407.934,305.742C 407.918,305.301 407.565,304.956 407.126,304.956 Z "/>
< ContentPresenter SnapsToDevicePixels ="{ TemplateBinding SnapsToDevicePixels }" HorizontalAlignment ="{ TemplateBinding HorizontalContentAlignment }" VerticalAlignment ="{ TemplateBinding VerticalContentAlignment }" RecognizesAccessKey ="True"/>
</ Grid >
< ControlTemplate.Triggers >
< Trigger Property ="IsFocused" Value ="True"/>
< Trigger Property ="IsMouseOver" Value ="True">
< Setter Property ="Fill" Value ="#FF6E8F9A" TargetName ="btnEllipse"/>
</ Trigger >
< Trigger Property ="IsPressed" Value ="True"/>
< Trigger Property ="IsEnabled" Value ="False"/>
< Trigger Property ="IsChecked" Value ="True">
< Setter TargetName ="iconMax" Property ="Visibility" Value ="Collapsed" />
< Setter TargetName ="iconRestore" Property ="Visibility" Value ="Visible" />
</ Trigger >
</ ControlTemplate.Triggers >
</ ControlTemplate >
</ Setter.Value >
</ Setter >
</ Style >
< Style x : Key ="styleMainWindowButtonClose" BasedOn ="{ x : Null }" TargetType ="{ x : Type Button }">
< Setter Property ="Template">
< Setter.Value >
< ControlTemplate TargetType ="{ x : Type Button }">
< Grid x : Name ="buttonClose">
< Ellipse Stroke ="{ x : Null }" StrokeThickness ="1" x : Name ="btnEllipse" >
< Ellipse.Fill >
< RadialGradientBrush >
< GradientStop Color ="#BFABA7A4" Offset ="0.777"/>
< GradientStop Color ="#FF897F77" Offset ="1"/>
</ RadialGradientBrush >
</ Ellipse.Fill >
</ Ellipse >
< Path x : Name ="iconX" Width ="8.50003" Height ="8.50006" Stretch ="Fill" Fill ="#FF8D1200" Data ="F1 M 401.281,299.818L 398.795,297.333L 401.281,294.847C 401.684,294.445 401.684,293.79 401.281,293.386C 400.877,292.982 400.222,292.982 399.82,293.386L 397.334,295.872L 394.848,293.386C 394.445,292.982 393.79,292.982 393.386,293.386C 392.982,293.79 392.982,294.445 393.386,294.848L 395.872,297.334L 393.387,299.818C 392.984,300.222 392.982,300.876 393.387,301.281C 393.791,301.684 394.445,301.683 394.848,301.279L 397.333,298.795L 399.818,301.281C 400.222,301.684 400.877,301.684 401.281,301.281C 401.684,300.877 401.684,300.222 401.281,299.818 Z "/>
< ContentPresenter SnapsToDevicePixels ="{ TemplateBinding SnapsToDevicePixels }" HorizontalAlignment ="{ TemplateBinding HorizontalContentAlignment }" VerticalAlignment ="{ TemplateBinding VerticalContentAlignment }" RecognizesAccessKey ="True"/>
</ Grid >
< ControlTemplate.Triggers >
< Trigger Property ="IsFocused" Value ="True"/>
< Trigger Property ="IsDefaulted" Value ="True"/>
< Trigger Property ="IsMouseOver" Value ="True">
< Setter Property ="Fill" Value ="#FF333332" TargetName ="btnEllipse"/>
< Setter Property ="Fill" Value ="#FFFFFFFF" TargetName ="iconX"/>
</ Trigger >
< Trigger Property ="IsPressed" Value ="True"/>
< Trigger Property ="IsEnabled" Value ="False"/>
</ ControlTemplate.Triggers >
</ ControlTemplate >
</ Setter.Value >
</ Setter >
</ Style >
<!--Window Style-->
< Style TargetType ="{ x : Type l : HeaderedWindow }">
< Setter Property ="WindowStyle" Value ="None"/>
< Setter Property ="ResizeMode" Value ="NoResize"/>
< Setter Property ="Background">
< Setter.Value >
< LinearGradientBrush MappingMode ="Absolute" StartPoint ="327.657227,-92.486328" EndPoint ="495.033722,694.958313">
< LinearGradientBrush.Transform >
< MatrixTransform Matrix ="2.000000,0.000000,-0.000000,-1.000000,-4.095703,626.486816" />
</ LinearGradientBrush.Transform >
< GradientStop Offset ="0.743034" Color ="LightPink" />
< GradientStop Offset ="0.835913" Color ="Red" />
< GradientStop Offset ="0.840783" Color ="DarkRed" />
< GradientStop Offset ="0.842105" Color ="Red" />
< GradientStop Offset ="0.990868" Color ="LightPink" />
</ LinearGradientBrush >
</ Setter.Value >
</ Setter >
< Setter Property ="BorderBrush" Value ="#FF5A3D1C"/>
< Setter Property ="BorderThickness" Value ="1"/>
< Setter Property ="MinWidth" Value ="90"/>
< Setter Property ="MinHeight" Value ="33"/>
< Setter Property ="VerticalContentAlignment" Value ="Stretch"/>
< Setter Property ="HorizontalContentAlignment" Value ="Stretch"/>
< Setter Property ="Template">
< Setter.Value >
< ControlTemplate TargetType ="{ x : Type l : HeaderedWindow }">
< Border BorderBrush ="{ TemplateBinding BorderBrush }"
BorderThickness ="{ TemplateBinding BorderThickness }">
< Grid SnapsToDevicePixels ="True" Background ="{ TemplateBinding Background }">
< Grid.RowDefinitions >
< RowDefinition Height ="33" />
< RowDefinition />
</ Grid.RowDefinitions >
<!-- Window header-->
< Grid Name ="PART_HeaderContainer" Background ="Transparent">
< StackPanel Orientation ="Horizontal" Margin ="4,0" Visibility ="{ Binding RelativeSource ={ x : Static RelativeSource .TemplatedParent}, Path =ShowDefaultHeader, Converter ={ StaticResource BoolToVisibilityConverter }}">
< Image Height ="24" Width ="24" HorizontalAlignment ="Left" VerticalAlignment ="Center" Source ="{ TemplateBinding Icon }"/>
< TextBlock Text ="{ TemplateBinding Title }"
Margin ="4,0"
VerticalAlignment ="Center"
FontFamily ="Arial"
FontSize ="15"
Foreground ="#FFFFFFFF"
TextWrapping ="NoWrap"/>
</ StackPanel >
< StackPanel HorizontalAlignment ="Right" Orientation ="Horizontal" Margin ="0,0,10,0">
< Button x : Name ="PART_MinimizeButton" IsTabStop ="False" Cursor ="Hand" Style ="{ StaticResource styleWindowButtonMinimize }" Width ="19" Height ="19" Margin ="0,0,4,0" ToolTip ="Minimize"
Visibility ="{ Binding RelativeSource ={ x : Static RelativeSource .TemplatedParent}, Path =CanResize, Converter ={ StaticResource BoolToVisibilityConverter }}"/>
< ToggleButton x : Name ="PART_RestoreButton" IsTabStop ="False" Cursor ="Hand" Style ="{ StaticResource styleWindowButtonMaximize }" Width ="19" Height ="19" Margin ="0,0,4,0" ToolTip ="Maximize"
Visibility ="{ Binding RelativeSource ={ x : Static RelativeSource .TemplatedParent}, Path =CanResize, Converter ={ StaticResource BoolToVisibilityConverter }}"/>
< Button x : Name ="PART_CloseButton" IsTabStop ="False" Cursor ="Hand" Style ="{ StaticResource styleMainWindowButtonClose }" Width ="19" Height ="19" Margin ="0,0,0,0" ToolTip ="Close"/>
</ StackPanel >
< ContentPresenter ContentSource ="Header" VerticalAlignment ="Stretch" HorizontalAlignment ="Stretch"/>
</ Grid >
< Grid Grid.RowSpan ="2" x : Name ="PART_ResizerContainers" Visibility ="Hidden">
< Grid.Resources >
< sys : Double x : Key ="StraightResizerSize"> 8 </ sys : Double >
< sys : Double x : Key ="SlantResizerSize"> 16 </ sys : Double >
< Style TargetType ="{ x : Type Thumb }">
< Setter Property ="Template">
< Setter.Value >
< ControlTemplate >
< Rectangle Fill ="Transparent"/>
</ ControlTemplate >
</ Setter.Value >
</ Setter >
</ Style >
</ Grid.Resources >
< Thumb Width ="{ StaticResource StraightResizerSize }" VerticalAlignment ="Stretch" HorizontalAlignment ="Left" Cursor ="SizeWE" x : Name ="PART_LeftResizer"/>
< Thumb Height ="{ StaticResource StraightResizerSize }" VerticalAlignment ="Top" HorizontalAlignment ="Stretch" Cursor ="SizeNS" x : Name ="PART_TopResizer"/>
< Thumb Width ="{ StaticResource StraightResizerSize }" VerticalAlignment ="Stretch" HorizontalAlignment ="Right" Cursor ="SizeWE" x : Name ="PART_RightResizer"/>
< Thumb Height ="{ StaticResource StraightResizerSize }" VerticalAlignment ="Bottom" HorizontalAlignment ="Stretch" Cursor ="SizeNS" x : Name ="PART_BottomResizer"/>
< ResizeGrip Width ="{ StaticResource SlantResizerSize }" Height ="{ StaticResource SlantResizerSize }" HorizontalAlignment ="Right" VerticalAlignment ="Bottom" Visibility ="{ Binding RelativeSource ={ x : Static RelativeSource .TemplatedParent}, Path =ShowResizeGrip, Converter ={ StaticResource BoolToVisibilityConverter }}"/>
< Thumb Width ="{ StaticResource SlantResizerSize }" Height ="{ StaticResource SlantResizerSize }" HorizontalAlignment ="Right" VerticalAlignment ="Bottom" Cursor ="SizeNWSE" x : Name ="PART_BottomRightResizer"/>
< Thumb Width ="{ StaticResource SlantResizerSize }" Height ="{ StaticResource SlantResizerSize }" HorizontalAlignment ="Right" VerticalAlignment ="Top" Cursor ="SizeNESW" x : Name ="PART_TopRightResizer"/>
< Thumb Width ="{ StaticResource SlantResizerSize }" Height ="{ StaticResource SlantResizerSize }" HorizontalAlignment ="Left" VerticalAlignment ="Top" Cursor ="SizeNWSE" x : Name ="PART_TopLeftResizer"/>
< Thumb Width ="{ StaticResource SlantResizerSize }" Height ="{ StaticResource SlantResizerSize }" HorizontalAlignment ="Left" VerticalAlignment ="Bottom" Cursor ="SizeNESW" x : Name ="PART_BottomLeftResizer"/>
</ Grid >
< Border x : Name ="PART_ContentBorder" Grid.Row ="1"
BorderBrush ="#FF5A3D1C"
BorderThickness ="2"
Margin ="8,0,8,8"
ClipToBounds ="True">
< AdornerDecorator >
< ContentPresenter Margin ="{ TemplateBinding Padding }"
VerticalAlignment ="{ TemplateBinding VerticalContentAlignment }"
HorizontalAlignment ="{ TemplateBinding HorizontalContentAlignment }"/>
</ AdornerDecorator >
</ Border >
</ Grid >
</ Border >
< ControlTemplate.Triggers >
< MultiTrigger >
< MultiTrigger.Conditions >
< Condition Property ="CanResize" Value ="True"/>
< Condition Property ="WindowState" Value ="Normal"/>
</ MultiTrigger.Conditions >
< Setter TargetName ="PART_ResizerContainers" Property ="Visibility" Value ="Visible"/>
</ MultiTrigger >
< Trigger Property ="WindowState" Value ="Maximized">
< Setter TargetName ="PART_ContentBorder" Property ="Margin" Value ="0"/>
< Setter TargetName ="PART_ContentBorder" Property ="BorderThickness" Value ="0,2,0,0"/>
</ Trigger >
</ ControlTemplate.Triggers >
</ ControlTemplate >
</ Setter.Value >
</ Setter >
</ Style >
</ ResourceDictionary >
以上样式中使用了一个Converter类,代码如下:
namespace CustomWindowDemo
{
public class BoolToVisibilityConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (bool )value ? Visibility .Visible : Visibility .Hidden;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException ();
}
#endregion
}
}
HeaderedWindow 的代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Windows.Markup;
using System.Reflection;
using System.Windows.Media.Animation;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Diagnostics;
namespace CustomWindowDemo
{
[TemplatePart (Name = HeaderContainerName, Type = typeof (FrameworkElement ))]
[TemplatePart (Name = MinimizeButtonName, Type = typeof (Button ))]
[TemplatePart (Name = RestoreButtonName, Type = typeof (ToggleButton ))]
[TemplatePart (Name = CloseButtonName, Type = typeof (Button ))]
[TemplatePart (Name = TopResizerName, Type = typeof (Thumb ))]
[TemplatePart (Name = LeftResizerName, Type = typeof (Thumb ))]
[TemplatePart (Name = RightResizerName, Type = typeof (Thumb ))]
[TemplatePart (Name = BottomResizerName, Type = typeof (Thumb ))]
[TemplatePart (Name = BottomRightResizerName, Type = typeof (Thumb ))]
[TemplatePart (Name = TopRightResizerName, Type = typeof (Thumb ))]
[TemplatePart (Name = TopLeftResizerName, Type = typeof (Thumb ))]
[TemplatePart (Name = BottomLeftResizerName, Type = typeof (Thumb ))]
public class HeaderedWindow : Window
{
#region Template Part Name
private const string HeaderContainerName = "PART_HeaderContainer" ;
private const string MinimizeButtonName = "PART_MinimizeButton" ;
private const string RestoreButtonName = "PART_RestoreButton" ;
private const string CloseButtonName = "PART_CloseButton" ;
private const string TopResizerName = "PART_TopResizer" ;
private const string LeftResizerName = "PART_LeftResizer" ;
private const string RightResizerName = "PART_RightResizer" ;
private const string BottomResizerName = "PART_BottomResizer" ;
private const string BottomRightResizerName = "PART_BottomRightResizer" ;
private const string TopRightResizerName = "PART_TopRightResizer" ;
private const string TopLeftResizerName = "PART_TopLeftResizer" ;
private const string BottomLeftResizerName = "PART_BottomLeftResizer" ;
#endregion
#region Dependency Properties
public static readonly DependencyProperty ShowDefaultHeaderProperty =
DependencyProperty .Register("ShowDefaultHeader" , typeof (bool ), typeof (HeaderedWindow ), new FrameworkPropertyMetadata (true ));
public static readonly DependencyProperty ShowResizeGripProperty =
DependencyProperty .Register("ShowResizeGrip" , typeof (bool ), typeof (HeaderedWindow ), new FrameworkPropertyMetadata (false ));
public static readonly DependencyProperty CanResizeProperty =
DependencyProperty .Register("CanResize" , typeof (bool ), typeof (HeaderedWindow ), new FrameworkPropertyMetadata (true ));
public static readonly DependencyProperty HeaderProperty =
DependencyProperty .Register("Header" , typeof (object ), typeof (HeaderedWindow ), new FrameworkPropertyMetadata (null , OnHeaderChanged));
public static readonly DependencyProperty HeaderTemplateProperty =
DependencyProperty .Register("HeaderTemplate" , typeof (DataTemplate ), typeof (HeaderedWindow ), new FrameworkPropertyMetadata (null ));
public static readonly DependencyProperty HeaderTempateSelectorProperty =
DependencyProperty .Register("HeaderTempateSelector" , typeof (DataTemplateSelector ), typeof (HeaderedWindow ), new FrameworkPropertyMetadata (null ));
public static readonly DependencyProperty IsFullScreenMaximizeProperty =
DependencyProperty .Register("IsFullScreenMaximize" , typeof (bool ), typeof (HeaderedWindow ), new FrameworkPropertyMetadata (false ));
private static void OnHeaderChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
HeaderedWindow win = sender as HeaderedWindow ;
win.RemoveLogicalChild(e.OldValue);
win.AddLogicalChild(e.NewValue);
}
public bool ShowDefaultHeader
{
get { return (bool )GetValue(ShowDefaultHeaderProperty); }
set { SetValue(ShowDefaultHeaderProperty, value ); }
}
public bool CanResize
{
get { return (bool )GetValue(CanResizeProperty); }
set { SetValue(CanResizeProperty, value ); }
}
public bool ShowResizeGrip
{
get { return (bool )GetValue(ShowResizeGripProperty); }
set { SetValue(ShowResizeGripProperty, value ); }
}
public object Header
{
get { return (object )GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value ); }
}
public DataTemplate HeaderTemplate
{
get { return (DataTemplate )GetValue(HeaderTemplateProperty); }
set { SetValue(HeaderTemplateProperty, value ); }
}
public DataTemplateSelector HeaderTempateSelector
{
get { return (DataTemplateSelector )GetValue(HeaderTempateSelectorProperty); }
set { SetValue(HeaderTempateSelectorProperty, value ); }
}
public bool IsFullScreenMaximize
{
get { return (bool )GetValue(IsFullScreenMaximizeProperty); }
set { SetValue(IsFullScreenMaximizeProperty, value ); }
}
#endregion
static HeaderedWindow()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof (HeaderedWindow ), new FrameworkPropertyMetadata (typeof (HeaderedWindow )));
}
#region Private Fields
private FrameworkElement headerContainer;
private Button minimizeButton;
private ToggleButton restoreButton;
private Button closeButton;
private Thumb topResizer;
private Thumb leftResizer;
private Thumb rightResizer;
private Thumb bottomResizer;
private Thumb bottomRightResizer;
private Thumb topRightResizer;
private Thumb topLeftResizer;
private Thumb bottomLeftResizer;
#endregion
public override void OnApplyTemplate()
{
base .OnApplyTemplate();
headerContainer = GetTemplateChild<FrameworkElement >(HeaderContainerName);
headerContainer.MouseLeftButtonDown += HeaderContainerMouseLeftButtonDown;
closeButton = GetTemplateChild<Button >(CloseButtonName);
closeButton.Click += delegate { Close(); };
restoreButton = GetTemplateChild<ToggleButton >(RestoreButtonName);
restoreButton.Checked += delegate { ChangeWindowState(WindowState .Maximized); };
restoreButton.Unchecked += delegate { ChangeWindowState(WindowState .Normal); };
StateChanged += new EventHandler (HeaderedWindowStateChanged);
minimizeButton = GetTemplateChild<Button >(MinimizeButtonName);
minimizeButton.Click += delegate { ChangeWindowState(WindowState .Minimized); };
topResizer = GetTemplateChild<Thumb >(TopResizerName);
topResizer.DragDelta += new DragDeltaEventHandler (ResizeTop);
leftResizer = GetTemplateChild<Thumb >(LeftResizerName);
leftResizer.DragDelta += new DragDeltaEventHandler (ResizeLeft);
rightResizer = GetTemplateChild<Thumb >(RightResizerName);
rightResizer.DragDelta += new DragDeltaEventHandler (ResizeRight);
bottomResizer = GetTemplateChild<Thumb >(BottomResizerName);
bottomResizer.DragDelta += new DragDeltaEventHandler (ResizeBottom);
bottomRightResizer = GetTemplateChild<Thumb >(BottomRightResizerName);
bottomRightResizer.DragDelta += new DragDeltaEventHandler (ResizeBottomRight);
topRightResizer = GetTemplateChild<Thumb >(TopRightResizerName);
topRightResizer.DragDelta += new DragDeltaEventHandler (ResizeTopRight);
topLeftResizer = GetTemplateChild<Thumb >(TopLeftResizerName);
topLeftResizer.DragDelta += new DragDeltaEventHandler (ResizeTopLeft);
bottomLeftResizer = GetTemplateChild<Thumb >(BottomLeftResizerName);
bottomLeftResizer.DragDelta += new DragDeltaEventHandler (ResizeBottomLeft);
}
private T GetTemplateChild<T>(string childName) where T : FrameworkElement , new ()
{
return (GetTemplateChild(childName) as T) ?? new T();
}
private void HeaderContainerMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ClickCount == 1)
{
DragMove();
}
else
{
ChangeWindowState(WindowState == WindowState .Maximized ? WindowState .Normal : WindowState .Maximized);
}
}
private void ChangeWindowState(WindowState state)
{
if (state == WindowState .Maximized)
{
if (!IsFullScreenMaximize && IsLocationOnPrimaryScreen())
{
MaxHeight = SystemParameters .WorkArea.Height;
MaxWidth = SystemParameters .WorkArea.Width;
}
else
{
MaxHeight = double .PositiveInfinity;
MaxWidth = double .PositiveInfinity;
}
}
WindowState = state;
}
private void HeaderedWindowStateChanged(object sender, EventArgs e)
{
if (WindowState == WindowState .Minimized)
{
restoreButton.IsChecked = null ;
}
else
{
restoreButton.IsChecked = WindowState == WindowState .Maximized;
}
}
private bool IsLocationOnPrimaryScreen()
{
return Left < SystemParameters .PrimaryScreenWidth && Top < SystemParameters .PrimaryScreenHeight;
}
#region Resize
private void ResizeBottomLeft(object sender, DragDeltaEventArgs e)
{
ResizeLeft(sender, e);
ResizeBottom(sender, e);
}
private void ResizeTopLeft(object sender, DragDeltaEventArgs e)
{
ResizeTop(sender, e);
ResizeLeft(sender, e);
}
private void ResizeTopRight(object sender, DragDeltaEventArgs e)
{
ResizeRight(sender, e);
ResizeTop(sender, e);
}
private void ResizeBottomRight(object sender, DragDeltaEventArgs e)
{
ResizeBottom(sender, e);
ResizeRight(sender, e);
}
private void ResizeBottom(object sender, DragDeltaEventArgs e)
{
if (ActualHeight <= MinHeight && e.VerticalChange < 0)
{
return ;
}
if (double .IsNaN(Height))
{
Height = ActualHeight;
}
Height += e.VerticalChange;
}
private void ResizeRight(object sender, DragDeltaEventArgs e)
{
if (ActualWidth <= MinWidth && e.HorizontalChange < 0)
{
return ;
}
if (double .IsNaN(Width))
{
Width = ActualWidth;
}
Width += e.HorizontalChange;
}
private void ResizeLeft(object sender, DragDeltaEventArgs e)
{
if (ActualWidth <= MinWidth && e.HorizontalChange > 0)
{
return ;
}
if (double .IsNaN(Width))
{
Width = ActualWidth;
}
Width -= e.HorizontalChange;
Left += e.HorizontalChange;
}
private void ResizeTop(object sender, DragDeltaEventArgs e)
{
if (ActualHeight <= MinHeight && e.VerticalChange > 0)
{
return ;
}
if (double .IsNaN(Height))
{
Height = ActualHeight;
}
Height -= e.VerticalChange;
Top += e.VerticalChange;
}
#endregion
}
}
使用 HeaderedWindow 的代码:
XAML :
< local : HeaderedWindow x : Class ="CustomWindowDemo.MainWindow"
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns : x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns : local ="clr-namespace:CustomWindowDemo"
Title ="MainWindow" Height ="350" Width ="525">
< Grid >
</ Grid >
</ local : HeaderedWindow >
.cs
public partial class MainWindow : HeaderedWindow
{
public MainWindow()
{
InitializeComponent();
}
}
最后的 Window 外观如下所示: