準備.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。從數據的現象看感受如今技術大牛們博客更新的愈來愈少,固然隨着技術沉澱到必定程度,大牛們可能各自的重點不會放在只研究技術上面。可是,我想說的是,如今咱們能深刻學習的資源確實愈來愈少了。最近幾年各類技術處在飛速發展中,浮躁的程序猿愈來愈多,大部分人都是處在熟練使用各類框架的程度,而能靜下心來研究某些框架的具體實現少之甚少。每每比較大型的公司,真正須要的人倒是那些不只懂框架並且能分析框架的人。題外話就說到這裏吧。。。git

顯示效果圖

    先簡單羅列下此次系統寫的幾個簡單控件。首先聲明展現效果的目的是實現功能,請直接忽略各自的美觀感覺。首先是一個登陸界面,效果以下:github

image

    MessageBox效果:框架

image

    Window、可關閉Item的TabControl、可停靠的DockingPanel效果:ide

image

基礎知識

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元素內嵌樣式很是類似:oop

<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>
            <DataTrigger
            Binding
=」{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系統來講,看看下面的截圖:

image

    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。工程結構以下:

image

    若是咱們新增一個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中值用存放資源引用路徑便可。例如:

<ResourceDictionary
    xmlns="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。工程結構以下:

image

    工程包含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來舉例。先看看實現的效果:

image

    自定義窗口包含兩個部分,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/

相關文章
相關標籤/搜索