题外话

打开博客园,查看首页左栏的”推荐博客”,排名前五的博客分别是(此处非广告):Artech、小坦克、圣殿骑士、腾飞(Jesse)、数据之巅。再看看它们博客的最新更新时间:Artech(2014-08-07) 、小坦克(2012-02-13)、圣殿骑士(2015-06-30)、腾飞(Jesse)(2013-12-18) 、数据之巅(2016-02-19) 。虽然数据之巅在最近发表了一篇博客,但是再看看倒数第二篇,更新时间是2015-11-18。从数据的现象看感觉现在技术大牛们博客更新的越来越少,当然随着技术沉淀到一定程度,大牛们可能各自的重点不会放在只研究技术上面。但是,我想说的是,现在我们能深入学习的资源确实越来越少了。最近几年各种技术处在飞速发展中,浮躁的程序猿越来越多,大部分人都是处在熟练使用各种框架的程度,而能静下心来研究某些框架的具体实现少之甚少。往往比较大型的公司,真正需要的人却是那些不仅懂框架而且能分析框架的人。题外话就说到这里吧。。。

显示效果图

先简单罗列下这次系统写的几个简单控件。首先声明展示效果的目的是实现功能,请直接忽略各自的美观感受。首先是一个登录界面,效果如下:

MessageBox效果:

Window、可关闭Item的TabControl、可停靠的DockingPanel效果:

基础知识

Style

如果了解HTML和CSS,那么WPF的Style的学习起来也比较容易,Style的设计思想就是仿照CSS来实现的。首先我们看看WPF中一个Button实现:

<Button FontSize=”22” Background=”Purple” Foreground=”White” Height=”50” Width=”50” RenderTransformOrigin=”.5,.5”>

一个Button的代码和HTML元素内嵌样式非常相似:

<Button Style="width:60px;heigth:50px;color:white;background:purple;font-size:22px" />

我们知道THML可通过CSS把样式提取出去,WPF中的Style同样也可以。在一个Window界面,我们可以在Window.Resources中添加独立样式:

<Style x:Key=”buttonStyle”><Setter Property=”Button.FontSize” Value=”22”/><Setter Property=”Button.Background” Value=”Purple”/><Setter Property=”Button.Foreground” Value=”White”/><Setter Property=”Button.Height” Value=”50”/><Setter Property=”Button.Width” Value=”50”/><Setter Property=”Button.RenderTransformOrigin” Value=”.5,.5”/>
</Setter>
</Style>

在声明Style时,可标记TargetType声明样式应用到的元素类型。例如:

<Style x:Key=”buttonStyle” TargetType=”{x:Type Button}”>

一般的Style我们都会指定唯一的Key值,但有时候我们可以不指定Key,直接创建隐式样式。例如:

<Style TargetType=”{x:Type Button}”>

需要说明的是,这里没有指定Key并不是说该Style没有key值,其实在编译过程中,这总情况的Style都会默认按照TargetType的类型默认指定一个Key值。并且应用到所有类型为TargetType的元素上。

在实现Style时,一般都会涉及到触发器,触发器包括:Property triggers、Data triggers、Event triggers、MultiTrigger。接下来分别介绍下这四个触发器。

(1)Property Trigger:只能应用到依赖属性上,当某个依赖属性的值的满足某些条件就会触发某些改变。先看看代码:

<Style x:Key=”buttonStyle” TargetType=”{x:Type Button}”>
<Style.Triggers><Trigger Property=”IsMouseOver” Value=”True”><Setter Property=”RenderTransform”><Setter.Value><RotateTransform Angle=”10”/></Setter.Value></Setter><Setter Property=”Foreground” Value=”Black”/></Trigger>
</Style.Triggers>
</Style>

上面的触发器实现的功能是,当鼠标移到Button上,字体颜色为Black,并且旋转10度。特别要注意的是,和HTML元素a:hover功能相似,触发器也有回置的功能,就是说当鼠标移出Button时,Button的字体颜色和旋转角度恢复到初始值。

(2)DataTrigger:和Property Trigger非常相似,但除了应用到依赖属性外,DataTrigger可应用到任意的.Net属性上。DataTrigger的代码写法和PropertyTrigger还是有区别的,它是通过Bingding来绑定需要判断的属性。例如:

<StackPanel Width=”200”><StackPanel.Resources><Style TargetType=”{x:Type TextBox}”>    <Style.Triggers><DataTriggerBinding

=”{Binding RelativeSource={RelativeSource Self}, Path=

Text}”Value=”disabled”><Setter Property=”IsEnabled” Value=”False”/></DataTrigger></Style.Triggers><Setter Property=”Background”Value=”{Binding RelativeSource={RelativeSource Self}, Path=Text}”/></Style></StackPanel.Resources><TextBox Margin=”3”/>
</StackPanel>

TextBox的Text属性是一个非依赖属性,如果Text的值为disabled,设置TextBox的IsEanbled为false。DataTrigger是通过Binding来绑定TextBox的Text属性。

(3)EventTrigger:当元素某些事件被触发时,可执行某些动画或者元素的某些属性发生变化,这些动作可通过EventTrigger实现。看看下面的代码:

<Button.Triggers><EventTrigger RoutedEvent=”Button.Click”><EventTrigger.Actions><BeginStoryboard><Storyboard TargetProperty=”Width”><DoubleAnimation From=”50” To=”100”Duration=”0:0:5” AutoReverse=”True”/></Storyboard></BeginStoryboard></EventTrigger.Actions></EventTrigger>
</Button.Triggers>

上面的代码实现功能是,当用户点击按钮时,触发一个动画,按钮的Width在5秒钟的时间内从50增加到100个像素。

(4)MultiTrigger:前面介绍的触发器都是当元素的某一个属性或者事件触发时操作。有些时候,我们需要判断多个条件是否同时满足,才执行某个操作。这个功能可通过MultiTrigger实现。例如,我们的Button按钮,当鼠标移到上面并且获取焦点时,设置字体颜色为Black和旋转度为10度。实现代码如下:

<Style.Triggers><MultiTrigger><MultiTrigger.Conditions><Condition Property=”IsMouseOver” Value=”True”/><Condition Property=”IsFocused” Value=”True”/></MultiTrigger.Conditions><Setter Property=”RenderTransform”><Setter.Value><RotateTransform Angle=”10”/></Setter.Value></Setter><Setter Property=”Foreground” Value=”Black”/></MultiTrigger>
</Style.Triggers>

Templates

常常在设计一个系统时,都会实现一套自己的UI界面,有些时候我们不得不重写样式比较简单的WPF Window界面。Template给开发者提供完全重写界面控件的技术,例如我们可以实现自己的窗口TitleBar,并且在上面添加菜单功能。我们在重写了控件UI的同时也保留了控件的各种功能。模板在分割可视化和逻辑上也功不可没,例如一些界面的样式交互效果我们完全可以在模板中实现,而源代码部分完全只考虑业务逻辑。我们看看下面一个常规的Button样式模板代码:

<ControlTemplate x:Key=”buttonTemplate” TargetType=”{x:Type Button}”><Grid><Ellipse x:Name=”outerCircle” Width=”100” Height=”100”><Ellipse.Fill><LinearGradientBrush StartPoint=”0,0” EndPoint=”0,1”><GradientStop Offset=”0” Color=”Blue”/><GradientStop Offset=”1” Color=”Red”/></LinearGradientBrush></Ellipse.Fill></Ellipse><Ellipse Width=”80” Height=”80”><Ellipse.Fill><LinearGradientBrush StartPoint=”0,0” EndPoint=”0,1”><GradientStop Offset=”0” Color=”White”/><GradientStop Offset=”1” Color=”Transparent”/></LinearGradientBrush></Ellipse.Fill></Ellipse></Grid><ControlTemplate.Triggers><Trigger Property=”IsMouseOver” Value=”True”><Setter TargetName=”outerCircle” Property=”Fill” Value=”Orange”/></Trigger><Trigger Property=”IsPressed” Value=”True”><Setter Property=”RenderTransform”><Setter.Value><ScaleTransform ScaleX=”.9” ScaleY=”.9”/></Setter.Value></Setter><Setter Property=”RenderTransformOrigin” Value=”.5,.5”/></Trigger></ControlTemplate.Triggers>
</ControlTemplate>

上面的代码完全重写了Button的默认样式,默认的Button是一个矩形,而现在变成了一个Ellipse椭圆形。并且使用渐变的颜色填充椭圆。我们知道默认的Button当我们鼠标移上去或者被单击时都会有样式变化。所以,我们还得给Button添加触发器,这里我们使用的是Property Trigger。当鼠标移到Button上(IsMouseOver=true),设置椭圆的填充颜色为Orange。当按钮被按下时(IsPressed=true),改变渐变颜色的起始位置。

Theme

一个比较完善的系统都提供了切换系统主题的功能, 我们就拿Window 10系统来说,看看下面的截图:

Window 10系统为用户提供了很多主题选择,当我们选择不同的主题,系统的菜单或者窗口的颜色都会发生变化,但是界面的结构是没有改变的。WPF的主题也完全一样。当界面开发人员重写玩控件的模板后,一般都会把模板中写某些资源提取出来,放到公共的资源文件中去。这些被提取出来的资源一般包括:界面背景色、边框颜色、字体颜色、以及图片等。我们先看一个TabControl的自定义模板:

<Style x:Key="CustomTabControlStyle" TargetType="{x:Type control:CustomTabControl}" BasedOn="{StaticResource {x:Type TabControl}}"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type control:CustomTabControl}"><DockPanel LastChildFill="True"><Border DockPanel.Dock="Top" Background="{DynamicResource Tab_Control_Background_Normal}"BorderBrush="{DynamicResource CustomControlBorderBrush}"BorderThickness="1, 1, 1, 0"><TabPanel Margin="0,0,0,0" IsItemsHost="True" /></Border><Border Background="White" BorderBrush="{DynamicResource CustomControlBorderBrush}" BorderThickness="1"> <ContentPresenter ContentSource="SelectedContent" /></Border></DockPanel></ControlTemplate></Setter.Value></Setter><Style.Triggers>...</Style.Triggers></Style>

代码中,Border的背景色动态的使用了Tab_Control_Background_Normal资源,边框颜色使用CustomControlBorderBrush资源。而这些被引用的资源都是单独存放在一个资源文件中。这个资源文件的部分代码如下:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"><!-- Common Style --><SolidColorBrush x:Key="CustomControlBackground" Color="#EDEDED" /><SolidColorBrush x:Key="CustomControlBorderBrush" Color="#b9bac1" /><!-- Button Style --><LinearGradientBrush x:Key="ButtonBackgroundColorBusrh" EndPoint="0,1" StartPoint="0,0"><GradientStop Color="#fdfdfd" Offset="0"/><GradientStop Color="#f5f5f5" Offset="0.5"/><GradientStop Color="#e9e9e9" Offset="1"/></LinearGradientBrush>
</ResourceDictionary>

基础部分就先简单的介绍到这里,接下来我们看看WPF的UI开发需要那些技能。

具备技能

1.Blend :要做WPF的界面开发,Blend是必备的技能。通过Blend可以很方便的画出UI,并且可以通过手动的方式添加动画效果。Blend的界面和Visual Studio很相似,所以上手也比较快。

2.PhotoShop:不管是做Web前段还是WPF前段,使用PhotoShop切图以及简单的设计某些图片效果的技能也是需要掌握的。并且界面开发人员只掌握到这个程度就可以了。

3.Snoop:是一个捕获WPF界面层次结构的工具,通过简单的操作就可以看到我们开发的WPF界面的完整层次结构,并且能够看到每层元素的属性值。当界面出现某些问题时,通过Snoop分析界面可达到事半功倍的效果。另外,在通过Visual Studio调试代码时,通过监视功能也能看到界面的层次结构。

4.素材资源:这里提供一些资源网站Icon Find(http://findicons.com/)、Icon Finder(https://www.iconfinder.com/)。

工程结构

UI界面的工程一般包括:Component和Theme两个工程。自定义控件一般都是添加在Component工程里边,而控件的样式模板都会添加到Theme工程里边。

先看下我们的Component工程,工程名称为HeaviSoft.FrameworkBase.Component。工程结构如下:

如果我们新增一个WPF的Custom Control,Visual Studio会自动在工程下面创建/Themes/Generic.xaml文件。并且自动在Generic.xaml文件中创建一个简单的控件模板。代码如下:

<Style TargetType="{x:Type docking:CustomDocumentPanel}"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type docking:CustomDocumentPanel}"><Border Background="{TemplateBinding Background}"BorderBrush="{TemplateBinding BorderBrush}"BorderThickness="{TemplateBinding BorderThickness}"><ContentPresenter /></Border></ControlTemplate></Setter.Value></Setter></Style>

上面的模板展示出来就是一片空白的效果,什么都有没有。所以我们得重写Template。其实稍后我们在Theme会重写这些控件的模板,但为什么这里也需要重写?Component下写的模板是控件的默认模板。但我们没有任何主题时,就显示该默认模板。

只要创建一个控件,Generic.xaml中就会增加一个TargetType为控件类型的模板,如果增加个10个控件,Generic.xaml中就增加了10个Style,代码量比较庞大。所以,我都会单独按照控件的名称在Theme下单独创建一个同名的资源文件。并把Generic.xaml中的对应样式移动到这个同名资源文件。而Generic.xaml中值用存放资源引用路径即可。例如:

<ResourceDictionaryxmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="clr-namespace:HeaviSoft.FrameworkBase.Component" xmlns:docking="clr-namespace:HeaviSoft.FrameworkBase.Component.Docking"><ResourceDictionary.MergedDictionaries><ResourceDictionary Source="/HeaviSoft.FrameworkBase.Component;component/Themes/CustomWindow.xaml" /><ResourceDictionary Source="/HeaviSoft.FrameworkBase.Component;component/Themes/CustomTextBox.xaml" /><ResourceDictionary Source="/HeaviSoft.FrameworkBase.Component;component/Themes/CustomMessageBox.xaml" /><ResourceDictionary Source="/HeaviSoft.FrameworkBase.Component;component/Themes/CustomTabControl.xaml" /><ResourceDictionary Source="/HeaviSoft.FrameworkBase.Component;component/Themes/CustomTabItem.xaml" /></ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

Component工程结构介绍完了,接下来看Theme工程。我们的Theme工程命名为HeaviSoft.FrameworkBase.Theme。工程结构如下:

工程包含CustomControl和Themes两个文件夹,CustomControl文件夹下有多个以控件名称命名的.xaml文件,例如Button.xaml文件,它里边的代码就是真正我们自己实现的Button模板。Themes下包含一个GrayWhite文件夹,这个文件夹就是我们的一个主题,它下面存放了各个控件模板需要的资源,包括图片、颜色、字体等。例如,我们在CustomControl下添加了CustomWindow界面模板,界面右上角需要关闭、最大化、最小化按钮图片。这些图片资源存放在/Themes/GrayWhite/Images/Window下。而CustomWindow需要的颜色以及字体资源分别存放在/Themes/GrayWhite/ColorBrush.xaml和/Themes/GrayWhite/Text.xaml中。同Component相似,/Themes/Generic.xaml的内容就是引用模板、图片、颜色、字体资源文件。在系统启动时,只需要动态加载/Themes/Generic.xaml文件就可加载所有的资源文件了。

开发步骤

UI控件的开发步骤我们就拿实现自定义Window来举例。先看看实现的效果:

自定义窗口包含两个部分,TitleBar和Body。TitleBar从左到右分别包含了图标、标题、最小化、最大化、关闭按钮。而Body部分主要包含我们在界面添加的内容。接下来就让我们一步步的去实现上图的界面功能。

第一步,Component添加自定义控件

首先,选择HeaviSoft.FrameworkBase.Component工程,添加Custom Control(WPF)。添加后,CustomWindow的默认代码为:

public class CustomWindow : Window{static CustomWindow(){DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomWindow), new FrameworkPropertyMetadata(typeof(CustomWindow)));}
}

CustomWindow类里边需要添加什么内容我们先不管。Visual Studio在添加CustomWindow文件的同时,会在/Themes/Generic.xaml中添加样式:

<Style TargetType="{x:Type control:CustomWindow}"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type control:CustomWindow}"><Border Background="{TemplateBinding Background}"BorderBrush="{TemplateBinding BorderBrush}"BorderThickness="{TemplateBinding BorderThickness}"></Border></ControlTemplate></Setter.Value></Setter></Style>

上面的样式其实只是显示一个空的Window界面,它在什么时候会被用到?当我们没有引用任何的主题时,默认加载该样式。

第二步,Theme中添加模板

接下来我们切换到HeaviSoft.FrameworkBase.Theme工程,在CustomControl文件夹下添加资源文件CustomWindow.xaml。然后在/Themes/Generic.xaml中添加CustomWindow.xaml文件的引用。引用的代码如下:

<ResourceDictionary Source="/HeaviSoft.FrameworkBase.Theme;component/CustomControl/CustomWindow.xaml" />

接下来我们就在CustomWindow.xaml中实现自定义Window模板,这里先给出代码,然后慢慢分析:

<Style x:Key="WindowStyle" TargetType="{x:Type control:CustomWindow}"><Setter Property="WindowStyle" Value="None" /><Setter Property="AllowsTransparency" Value="True" /><Setter Property="Background" Value="Transparent" /><Setter Property="ResizeMode" Value="NoResize" /><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type control:CustomWindow}"><Border x:Name="MainBorder" Background="{DynamicResource CustomControlBackground}" CornerRadius="6" BorderBrush="{DynamicResource CustomControlBorderBrush}" BorderThickness="1" ><Grid><Grid.RowDefinitions><RowDefinition Height="50"/><RowDefinition Height="1" /><RowDefinition Height="*" /></Grid.RowDefinitions><Border Grid.Row="0" Panel.ZIndex="101"  x:Name="PART_TITLEBAR" BorderThickness="0" CornerRadius="6, 6, 0, 0" Background="{DynamicResource Window_TitleBar_Background}"><DockPanel  LastChildFill="False" ><Image DockPanel.Dock="Left" Source="{TemplateBinding Icon}" /><Label VerticalAlignment="Center" DockPanel.Dock="Left" Content="{TemplateBinding Title}"Style="{DynamicResource TitleStyle}"/><Button x:Name="PART_CLOSE" Style="{DynamicResource Window_Titlebar_ButtonStyle}" DockPanel.Dock="Right"><Image x:Name="c" Margin="3, 0" Style="{DynamicResource TitleButtonImageStyle}"DockPanel.Dock="Right" Width="24" Height="24" Source="{DynamicResource Window_Button_Close_ImageBrush}"   /></Button><Button x:Name="PART_MAXIMIZE_RESTORE"  Style="{DynamicResource Window_Titlebar_ButtonStyle}" DockPanel.Dock="Right"><Image Name="MaximizeRestoreImage" Margin="3, 0" Style="{DynamicResource TitleButtonImageStyle}"DockPanel.Dock="Right" Width="24" Height="24" Source="{DynamicResource Window_Button_Max_ImageBrush}"  /></Button><Button x:Name="PART_MINIMIZE" Style="{DynamicResource Window_Titlebar_ButtonStyle}" DockPanel.Dock="Right"><Image Margin="3, 0" Style="{DynamicResource TitleButtonImageStyle}"DockPanel.Dock="Right" Width="24" Height="24" Source="{DynamicResource Window_Button_Min_ImageBrush}" RenderTransformOrigin="0.5,0.5"  ></Image></Button></DockPanel></Border><Border BorderBrush="{DynamicResource CustomControlBorderBrush}" BorderThickness="1" Grid.Row="1" /><ContentPresenter Grid.Row="2" Margin="8, 5" /></Grid></Border><ControlTemplate.Triggers><DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=WindowState}" Value="Maximized"><Setter TargetName="MaximizeRestoreImage" Property="Source" Value="{DynamicResource Window_Button_Maximum_ImageBrush}" /></DataTrigger></ControlTemplate.Triggers></ControlTemplate></Setter.Value></Setter></Style>

首先我们不会使用Window默认的TitleBar,所以我们要隐藏掉Window默认的TitleBar,通过设置:WindowStyle为None就可隐藏掉标题栏和边框。在之前看到的效果图中,我们看到Window有使用圆角效果,那么必须设置三个属性值:

<Setter Property="AllowsTransparency" Value="True" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="ResizeMode" Value="NoResize" />

AllowsTransparency为True允许窗口透明,Background为Transparent继承父窗口的背景色(也即设置背景色为透明)。ResizeMode为NoResize禁止手动拖动窗口大小,隐藏虚线边框。

然后设置Template的Value,一般都是直接创建一个ControlTemplate节点,并指定TargetType为CustomWindow。

<ControlTemplate TargetType="{x:Type control:CustomWindow}">...
</ControlTemplate>

接下就该添加模板的具体实现内容,首先我们要设置有圆角的边框,那么我们可以在Root节点添加一个Border节点,设置背景色Background和边框宽度BorderThickness、边框颜色BorderBrush,并设置CornerRadius圆角度。代码如下:

<Border x:Name="MainBorder" Background="{DynamicResource CustomControlBackground}" CornerRadius="6" BorderBrush="{DynamicResource CustomControlBorderBrush}" BorderThickness="1" >...
</Border>

我们知道整个界面包含标题和内容,我还会在标题和内容之间添加一条分割线,所以我们可通过一个包含三行的Grid来实现,第零行为标题栏、第一行为一条Bordrer实现的分割线、第二行就直接为我们的内容,可通过ContentPresenter存放这些内容。第一行和第二行的实现如下:

<Grid><Grid.RowDefinitions><RowDefinition Height="50"/><RowDefinition Height="1" /><RowDefinition Height="*" /></Grid.RowDefinitions>...省略标题栏代码<Border BorderBrush="{DynamicResource CustomControlBorderBrush}" BorderThickness="1" Grid.Row="1" /><ContentPresenter Grid.Row="2" Margin="8, 5" /></Grid>

这里需要说明的是,一般在写自定义控件时,控件的内容可通过ContentPresenter来显示。

其实整个界面实现比较复杂的部分是标题栏部分,我们要显示图标、标题、最小化、最大化、关闭按钮。图标、标题靠左显示,而最小化、最大化、关闭按钮靠右显示。这里我们想起了DockPanel面板,图标和标题实现如下:

<DockPanel  LastChildFill="False" ><Image DockPanel.Dock="Left" Source="{TemplateBinding Icon}" /><Label VerticalAlignment="Center"  DockPanel.Dock="Left"  Content="{TemplateBinding Title}"Style="{DynamicResource TitleStyle}"/>...
</DockPanel>

显示的图标和标题我们可通过TemplateBinding直接绑定Window的图标和标题。DockPanel右边主要是三个按钮,我们这里拿关闭按钮分析:

<Button x:Name="PART_CLOSE"  Style="{DynamicResource Window_Titlebar_ButtonStyle}" DockPanel.Dock="Right"><Image x:Name="c" Margin="3, 0"   Style="{DynamicResource TitleButtonImageStyle}" DockPanel.Dock="Right" Width="24" Height="24"  Source="{DynamicResource Window_Button_Close_ImageBrush}"   /></Button>

关闭按钮是一个Button类型元素,它的内容就是显示一个图标,我们知道默认的Button按钮,本身有一些Normal、MouseOver、Press状态的效果。但这些效果不是我们想要的,所以必须得重写,上面代码我们设置Button的Style为Window_Titlebar_ButtonStyle就是重写设置了Button的样式,而嵌套的Image样式也是引用的TitleButtonImageStyle样式。Window_Titlebar_ButtonStyle的样式代码为:

<Style x:Key="Window_Titlebar_ButtonStyle" TargetType="{x:Type ButtonBase}"><Setter Property="BorderThickness" Value="0" /><Setter Property="Background" Value="Transparent" /><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type ButtonBase}"><ControlTemplate.Triggers><Trigger Property="IsMouseOver" Value="True"><Setter Property="Background" Value="Transparent"/><Setter Property="BorderThickness" Value="0" /></Trigger><Trigger Property="IsEnabled" Value="False"><Setter Property="Background" Value="Transparent" /><Setter Property="BorderThickness" Value="0" /></Trigger></ControlTemplate.Triggers><ContentPresenter … />                    
                </ControlTemplate></Setter.Value></Setter></Style>

代码中,通过触发器重写了MouseOver和IsEnabled为False状态的显示效果,CustomWindow.xaml模板就分析到这里。

第三步,实现界面逻辑

现在我们只是实现了界面的可视化界面,当我们点击最小化按钮时需要把窗口最小化,怎样实现这样的功能?我们又不得不回到HeaviSoft.FrameworkBase.Component工程中添加的CustomWindow类,我们可以通过窗口的可视化层次关系找到最小化按钮,在查找按钮之前我们必须知道它们的Name,所以一般我们都会在类的头部声明模板部件名称。如下所示:

/// <summary>/// 自定义界面/// </summary>[TemplatePart(Name = "PART_TITLEBAR", Type = typeof(UIElement))][TemplatePart(Name = "PART_CLOSE", Type = typeof(Button))][TemplatePart(Name = "PART_MAXIMIZE_RESTORE", Type = typeof(Button))][TemplatePart(Name = "PART_MINIMIZE", Type = typeof(Button))]public class CustomWindow : Window
{
}

上面的代码可以让我们很直观的知道最小化按钮的名称为PART_MINIMIZE,然后我们声明一个Button属性:

/// <summary>
/// 最小化按钮/// </summary>
private Button MinimizeButton { get; set; }

接下来在什么时机捕获显示的按钮呢?肯定是必须得等在HeaviSoft.FrameworkBase.Theme工程下添加的CustomWindow.xaml模板被加载完后才能查找,正好Control控件为我们提供了可重写的方法OnApplyTemplate,我们可以在该方法中去递归遍历查找控件。实现代码如下:

public override void OnApplyTemplate(){base.OnApplyTemplate();AttachToVisualTree();}/// <summary>/// 附加可视化树到模板/// </summary>private void AttachToVisualTree(){AttachCloseButton();AttachMinButton();AttachMaximizeRestoreButton();AttachTitleBar();}/// <summary>/// 附加最小化按钮/// </summary>private void AttachMinButton(){if(MinimizeButton != null){MinimizeButton.Command = null;}var minimizeButton = GetChildControl<Button>("PART_MINIMIZE");if(minimizeButton != null){minimizeButton.Command = MinimizedCommand;MinimizeButton = minimizeButton;}}

OnApplyTemplate方法调用了AttachToVisualTree方法,而AttachToVisualTree方法中又调用了AttachMinButton方法,AttachMinButton方法通过GetChildControl<Button>("PART_MINIMIZE")来查找模板中的最小化按钮,找到之后绑定MinimizedCommand命令,命名的初始化代码片段如下:

<public CustomWindow()
{CreateCommandBindings();
}/// <summary>
/// 创建绑定命令
/// </summary>
private void CreateCommandBindings()
{CommandBindings.Add(new CommandBinding(ApplicationCommands.Close, (a, b) => { Close(); }));CommandBindings.Add(new CommandBinding(MinimizedCommand, (a, b) => { WindowState = WindowState.Minimized; }));CommandBindings.Add(new CommandBinding(MaximizeRestoreCommand, (a, b) => { WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; }));
}/// <summary>/// 最小化指令
/// </summary>private readonly RoutedCommand MinimizedCommand = new RoutedUICommand("Minmize", "Minmize", typeof(CustomWindow));/// <summary>
/// 最大化复原指令
/// </summary>
private readonly RoutedCommand MaximizeRestoreCommand = new RoutedUICommand("MaximizeRestore", "MaximizeRestore", typeof(CustomWindow));

代码也很简单,首先声明一个Command,然后在CreateCommandBindings方法中为声明的Command绑定命名,比如最小化指令绑定的方法为:(a, b) => { WindowState = WindowState.Minimized; },直接设置WindowState的值为Minimized最小化。其他的操作,例如最大化、关闭以及窗口的拖拽可直接查看源代码了解,这里就不在做分析了。

第四步,提取主题资源

一个自定义控件的实现我们差不多完成了90%,还剩一个任务就是把模板文件中的主题资源(例如颜色、图片)提取到主题/Themes/下面去。例如前面实现的Window模板中,最小化、最大化等按钮有使用图片,比较好的设计肯定是不会直接使用图片路径,所以我们把这些资源提取到/Themes/ColorBrush.xaml资源文件中去。提取资如下:

<SolidColorBrush x:Key="Window_Title_Font_Color" Color="#77818b" />
<SolidColorBrush x:Key="Window_Font_Color" Color="#77818b" />
<SolidColorBrush x:Key="Window_Dark_Font_Color" Color="#fff" />
<BitmapImage x:Key="Window_Button_Close_ImageBrush" UriSource="Images/Window/close.png" />
<BitmapImage x:Key="Window_Button_Min_ImageBrush" UriSource="Images/Window/min.png" />
<BitmapImage x:Key="Window_Button_Max_ImageBrush" UriSource="Images/Window/max.png" />
<BitmapImage x:Key="Window_Button_Maximum_ImageBrush" UriSource="Images/Window/maximum.png" />

Window模板中我们只使用这些Key值即可。而不关系图片的具体路径。这样设计,以后我们切换主题时就很方便。到目前为止,怎样完完整整的添加一个自定义控件就介绍完了。由于代码量比较大,所以在上面介绍时只贴出了部分代码,完整的代码可在GitHub上获取。

总结

能看到这里的,也辛苦各位了。本片主要介绍了:

(1).WPF界面设计的几个基础技术点,包括Style、Template、Theme。

(2)界面设计的工具和资源,包括Blend、PhotoShop、Snoop以及网站资源。

(3)工程结构。

(4)自定义控件实现过程。

如果本篇内容对大家有帮助,请点击页面右下角的关注。如果觉得不好,也欢迎拍砖。你们的评价就是博主的动力!下篇内容,敬请期待!

源代码

完整的代码存放在GitHub上,代码路径:https://github.com/heavis/Documentor_V01R01/。

转载于:https://www.cnblogs.com/w-wanglei/p/5274298.html

准备.Net转前端开发-WPF界面框架那些事,UI快速实现法相关推荐

  1. 2012年度最佳Web前端开发工具和框架总结

    2012年度最佳Web前端开发工具和框架总结 2013/01/18 | 分类: 工具与资源 | 1 条评论 | 标签: 前端, 开发工具, 开发框架 分享到:0 来源:梦想天空 技术的快速发展让很多人 ...

  2. 2012年度最佳 Web 前端开发工具和框架——《上篇》

    技术的快速发展让很多人学习起来无所适从,幸运的是,很多优秀的 Web 开发人员和设计人员在努力寻找各种有特色的解决方案. 因此,我们有了很多优秀的小工具和库,每一个都是用来解决特定的问题或维护一组特定 ...

  3. 2012年度最佳Web前端开发工具和框架

    摘要:技术的快速发展让很多人学习起来无所适从,幸运的是,很多优秀的 Web 开发人员和设计人员在努力寻找各种有特色的解决方案. 因此,我们有了很多优秀的小工具和库,每一个都是用来解决特定的问题或维护一 ...

  4. 视频教程-Web前端开发利器 SPRY框架之表单验证-JavaScript

    Web前端开发利器 SPRY框架之表单验证 有17年互联网行业从业经验,始终在教学第一线,勇于创新,从有效教学,不断向高效教学转变.始终坚持"学生为主体,教师为主导:商业化案例,企业化情境& ...

  5. 视频教程-Web前端开发利器 SPRY框架之页面效果-JavaScript

    Web前端开发利器 SPRY框架之页面效果 有17年互联网行业从业经验,始终在教学第一线,勇于创新,从有效教学,不断向高效教学转变.始终坚持"学生为主体,教师为主导:商业化案例,企业化情境& ...

  6. 视频教程-Web前端开发利器 SPRY框架之数据集XML-JavaScript

    Web前端开发利器 SPRY框架之数据集XML 有17年互联网行业从业经验,始终在教学第一线,勇于创新,从有效教学,不断向高效教学转变.始终坚持"学生为主体,教师为主导:商业化案例,企业化情 ...

  7. WPF - 界面美化 MahApps.Metro UI

    WPF - 界面美化 MahApps.Metro UI 欢迎使用MahApps.Metro 我的接入效果 接入方式 欢迎使用MahApps.Metro MahApps.Metro资料: 官网地址:ht ...

  8. 前端开发(layui框架)

    [ 声明:版权所有,欢迎转载,请勿用于商业用途. 联系信箱:feixiaoxing @163.com] 写软件使用成熟的框架,这是很常见的事情.之前一直以为前端开发都是从0到1开发的,后来发现也不是. ...

  9. 分享一个漂亮WPF界面框架创作过程及其源码

    本文会作为一个系列,分为以下部分来介绍: (1)见识一下这个界面框架: (2)界面框架如何进行开发: (3)辅助开发支持:Demo.模板.VsPackage制作. 框架源码如下所示. 本文介绍第(1) ...

最新文章

  1. 【Linux 内核】Linux 内核源码目录说明 ③ ( lib 目录 | LICENSES 目录 | mm 目录 | net 目录 | samples 目录 | scripts 目录 )
  2. C语言中, 有些时候数值名并不与指针等价.
  3. 特斯拉提升安全监控等级,推出“哨兵模式”
  4. matlab cat函数_如何用Matlab编写贪吃蛇游戏?(持续更新)
  5. 使用 iTextSharp 生成 PDF 表格
  6. Convolutional Neural Networks
  7. apper安卓×××
  8. windows安装MobaXterm
  9. 计算机二级机试题型,计算机二级机试题库
  10. 人工智能自动驾驶的意义,人工智能自动驾驶汽车
  11. matlab不能radon变换,Radon变换的理解
  12. Faster-RCNN理论
  13. Request method ‘GET‘ not supported 405错误辨析总结
  14. Android手机的USB
  15. iOS开发创建App内购买项目发现元数据丢失
  16. 向左转移测试需要整个团队的努力
  17. 汽车制造行业工厂数据、互联数据和移动数据存储解决方案
  18. ShareSDK移动APP社会化分享组件
  19. 【计算机网络】——通信协议综述(网络协议、网络分层、ifconfige命令行、DHCP与PXE)
  20. INTERVAL '1' MONTH TO MONTH

热门文章

  1. File转化为MultipartFile
  2. 支付宝(即时到账批量退款业务错误码)
  3. 正确的线程中止-标志位
  4. QT教程2:QT5的体系构架
  5. js解析url query_js如何解析url
  6. 卷组删除pv_Linux LVM(逻辑卷管理)的删除
  7. 如何获取握手包_白话详解TCP的三次握手到底做了些什么
  8. android studio放置在函数上面看_Android中用Kotlin协程和Retrofit进行网络请求和取消请求...
  9. 往有序链表的插入元素使原链表依旧有序
  10. git reset --hard、git reset --sort及git revert区别