准备.Net转前端开发-WPF界面框架那些事,UI快速实现法
题外话
打开博客园,查看首页左栏的”推荐博客”,排名前五的博客分别是(此处非广告):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快速实现法相关推荐
- 2012年度最佳Web前端开发工具和框架总结
2012年度最佳Web前端开发工具和框架总结 2013/01/18 | 分类: 工具与资源 | 1 条评论 | 标签: 前端, 开发工具, 开发框架 分享到:0 来源:梦想天空 技术的快速发展让很多人 ...
- 2012年度最佳 Web 前端开发工具和框架——《上篇》
技术的快速发展让很多人学习起来无所适从,幸运的是,很多优秀的 Web 开发人员和设计人员在努力寻找各种有特色的解决方案. 因此,我们有了很多优秀的小工具和库,每一个都是用来解决特定的问题或维护一组特定 ...
- 2012年度最佳Web前端开发工具和框架
摘要:技术的快速发展让很多人学习起来无所适从,幸运的是,很多优秀的 Web 开发人员和设计人员在努力寻找各种有特色的解决方案. 因此,我们有了很多优秀的小工具和库,每一个都是用来解决特定的问题或维护一 ...
- 视频教程-Web前端开发利器 SPRY框架之表单验证-JavaScript
Web前端开发利器 SPRY框架之表单验证 有17年互联网行业从业经验,始终在教学第一线,勇于创新,从有效教学,不断向高效教学转变.始终坚持"学生为主体,教师为主导:商业化案例,企业化情境& ...
- 视频教程-Web前端开发利器 SPRY框架之页面效果-JavaScript
Web前端开发利器 SPRY框架之页面效果 有17年互联网行业从业经验,始终在教学第一线,勇于创新,从有效教学,不断向高效教学转变.始终坚持"学生为主体,教师为主导:商业化案例,企业化情境& ...
- 视频教程-Web前端开发利器 SPRY框架之数据集XML-JavaScript
Web前端开发利器 SPRY框架之数据集XML 有17年互联网行业从业经验,始终在教学第一线,勇于创新,从有效教学,不断向高效教学转变.始终坚持"学生为主体,教师为主导:商业化案例,企业化情 ...
- WPF - 界面美化 MahApps.Metro UI
WPF - 界面美化 MahApps.Metro UI 欢迎使用MahApps.Metro 我的接入效果 接入方式 欢迎使用MahApps.Metro MahApps.Metro资料: 官网地址:ht ...
- 前端开发(layui框架)
[ 声明:版权所有,欢迎转载,请勿用于商业用途. 联系信箱:feixiaoxing @163.com] 写软件使用成熟的框架,这是很常见的事情.之前一直以为前端开发都是从0到1开发的,后来发现也不是. ...
- 分享一个漂亮WPF界面框架创作过程及其源码
本文会作为一个系列,分为以下部分来介绍: (1)见识一下这个界面框架: (2)界面框架如何进行开发: (3)辅助开发支持:Demo.模板.VsPackage制作. 框架源码如下所示. 本文介绍第(1) ...
最新文章
- 【Linux 内核】Linux 内核源码目录说明 ③ ( lib 目录 | LICENSES 目录 | mm 目录 | net 目录 | samples 目录 | scripts 目录 )
- C语言中, 有些时候数值名并不与指针等价.
- 特斯拉提升安全监控等级,推出“哨兵模式”
- matlab cat函数_如何用Matlab编写贪吃蛇游戏?(持续更新)
- 使用 iTextSharp 生成 PDF 表格
- Convolutional Neural Networks
- apper安卓×××
- windows安装MobaXterm
- 计算机二级机试题型,计算机二级机试题库
- 人工智能自动驾驶的意义,人工智能自动驾驶汽车
- matlab不能radon变换,Radon变换的理解
- Faster-RCNN理论
- Request method ‘GET‘ not supported 405错误辨析总结
- Android手机的USB
- iOS开发创建App内购买项目发现元数据丢失
- 向左转移测试需要整个团队的努力
- 汽车制造行业工厂数据、互联数据和移动数据存储解决方案
- ShareSDK移动APP社会化分享组件
- 【计算机网络】——通信协议综述(网络协议、网络分层、ifconfige命令行、DHCP与PXE)
- INTERVAL '1' MONTH TO MONTH
热门文章
- File转化为MultipartFile
- 支付宝(即时到账批量退款业务错误码)
- 正确的线程中止-标志位
- QT教程2:QT5的体系构架
- js解析url query_js如何解析url
- 卷组删除pv_Linux LVM(逻辑卷管理)的删除
- 如何获取握手包_白话详解TCP的三次握手到底做了些什么
- android studio放置在函数上面看_Android中用Kotlin协程和Retrofit进行网络请求和取消请求...
- 往有序链表的插入元素使原链表依旧有序
- git reset --hard、git reset --sort及git revert区别