【WPF學習】第六十章 建立控件模板

  通過數十天的忙碌,今天終於有時間寫博客。框架

  前面一章經過介紹有關模板工做方式相關的內容,同時介紹了FrameWorkElement下全部控件的模板。接下來將介紹如何構建一個簡單的自定義按鈕,並在該過程當中學習有關控件模板的一些細節。工具

  經過上一章內容,基本Button控件使用ButtonChrome類繪製其特殊的背景和邊框。Button類使用ButtonChrome類而不使用WPF繪圖圖元的一個緣由是,標準按鈕的外觀依賴於幾個明顯的特徵(是否被禁用、是否具備焦點以及是否正在被單擊)和其餘一些更微妙的因素(如當前Windows主題)。只使用觸發器實現這類邏輯是笨拙的。學習

  然而,當構建自定義控件時,能夠不用擔憂標準化和主題集成(實際上,WPF不像之前的用戶界面技術那樣強調用戶界面標準化)。反而能更須要關注如何建立富有吸引力的新穎控件,並將他們混合到用戶界面的其餘部分。所以,可能不須要建立諸如ButtonChrome的類,而可以使用以及學過的元素,設計自給自足的不使用代碼的控件模板。測試

1、簡單按鈕動畫

  爲應用自定義控件模板,只須要設置控件的Template屬性。儘管可定義內聯模板(經過在控件標籤內部嵌入控件模板標籤),但這種方法基本沒有意義。這是由於幾乎老是但願爲同一控件的多個皮膚實例重用模板。爲適應這種設計,須要將控件模板定義爲資源,並使用StaticResource引用該資源,以下所示:spa

<Button  Margin="10" Padding="5" Template="{StaticResource ButtonTemplate}"  >
            A Simple Button with a Custom Template
</Button>

  經過這種方法,不只能夠較容易地建立許多自定義按鈕,在之後還能夠很靈活地修改控件模板,而不會擾亂應用程序用戶界面的其他部分。設計

  在這個特定示例中,ButtonTemplate資源放在包含窗口的Resource集合中。然而,在實際應用程序中,可能更喜歡使用應用程序資源,具體緣由在下一章介紹的「組織模板資源」中進行討論。code

  下面是控件模板的基本框架:orm

<Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            ...
        </ControlTemplate>
</Window.Resources>

  在上面的控件模板中設置了TargetType屬性,以明確指示該模板是爲按鈕設計的。與樣式相似,這老是一個能夠遵循的好約定。在內容控件(如按鈕)中也須要使用該約定,不然ContentPresenter元素就不能工做。對象

  要爲基本按鈕建立模板,須要本身繪製邊框和背景,而後在按鈕中放置內容。繪製邊框的兩種可能的候選方法是使用Rectangle類和Border類。下面的示例使用Border類,將具備圓角的桔色輪廓與引入注目的紅色背景和白色文本結合在一塊兒:

 <Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
            ...
            </Border>
        </ControlTemplate>
</Window.Resources>

  在此主要關注背景,但仍須要一種方法顯示按鈕內容。在之前的學習中,可能還記得Button類在其餘控件模板中包含了一個ContentPresenter元素。全部內容控件都須要ContentPresenter元素——它是標示「在此插入內容」的標記器,告訴WPF在何處保存內容:

<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
                  <ContentPresenter RecognizesAccessKey="True"></ContentPresenter>
            </Border>
</ControlTemplate>

  在ContentPresenter元素將RecognizesAccessKey屬性設置爲true。儘管這不是必需的,但可確保按鈕支持訪問鍵——具備下劃線的字母,可使用該字母快速觸發按鈕。對於這種狀況,若是按鈕具備文本Click_Me,那麼當用戶按下Alt+M組合鍵時會觸發按鈕(在標準的Windows設置中,下劃線是隱藏的,而且只要按下Alt鍵,訪問鍵(在此是M鍵)就會具備下劃線)。若是爲將RecongnizesAccessKey屬性設置爲true,就會忽略該細節,而且任何下劃線都將被視爲普通的下劃線,並做爲按鈕內容的一部分進行顯示。

2、模板綁定

  該例還存在一個小問題。如今爲按鈕添加的標籤將Margin屬性的值指定爲10,並將Padding屬性的值指定爲5。StackPanel控件關注的是按鈕的Margin屬性,但忽略了Padding屬性,使按鈕的內容和側邊擠壓在一塊兒。此處的問題是Padding屬性不起做用,除非在模板中特別注意它。換句話說,模板負責檢索內邊距值並使用該值在內容周圍插入額外的空白。

  幸運的是,WPF專門針對該目的設計了一個工具:模板綁定。頭能改過使用綁定模板,模板可從應用模板的控件中提取一個值。在本例中,可以使用模板綁定檢索Padding屬性的值,並使用該屬性值在ContentPresenter元素周圍建立外邊距:

<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
                  <ContentPresenter RecognizesAccessKey="True"  Margin="{TemplateBinding Padding}"></ContentPresenter>
            </Border>
</ControlTemplate>

  這樣就會獲得所指望的效果,在邊框和內容之間添加了一些空白。以下圖顯示了新的簡單按鈕。

   模板綁定和普通的數據綁定相似,但它們的量級更輕,由於它們是專門針對在控件模板中使用而設計的。它們只支持單向數據banding(換句話說,它們是從控件向模板傳遞信息,但不能從模板向控件傳遞信息),而且不能用於從Freezable類的派生類的屬性中提取信息。若是遇到模板綁定不生效的情形,可改用具備完整功能的數據綁定。

  預計須要哪些模板綁定的惟一方法是檢查默認控件模板。若是查看Button類的控件模板,就會發如今模板綁定的使用方法上與自定義模板是徹底相同的——獲取爲按鈕指定的內邊距,並將它轉換成ContentPresenter元素周圍的外邊距。還會發現標準按鈕模板包含另外幾個模板綁定,如HorizontalAlignment、VerticalAlignment以及Background,這個簡單的自定義模板中沒有使用這些模板綁定。這意味着若是爲控件設置了這些屬性,對於這個簡單的自定義模板來講,這些設置是沒有效果。

  在許多狀況下,可不考慮模板綁定。實際上,若是不許備使用屬性或者不但願修改模板,就沒必要綁定屬性。例如,當前得簡單按鈕將用於文本的Foreground屬性設置爲白色並忽略爲Background屬性設置的任何值是合理的,由於前景色和背景色是該該按鈕可視化外觀的固有部分。

  可能選擇避免模板綁定的另外一個緣由是——控件不能橫好地支持它們。例如,若是爲按鈕設置了Background屬性,可能注意到當按鈕被按下時不會連貫地處理該背景色(實際上,這時該背景色消失了,而且被按下的默認外觀替換了)。該例中的自定義模板與此相似,儘管尚未任何鼠標懸停和鼠標單擊行爲,但一旦添加這些細節,就會但願徹底控制按鈕的顏色以及在不一樣狀態下它們的變化。

3、改變屬性的觸發器

  若是測試上一節建立的按鈕,就會發現它使人十分失望。本質上,它不過是一個紅色的圓角矩形——當在它上面移動鼠標或單擊鼠標時,其外觀沒有任何反應。按鈕只是無動於衷,呆在那兒不動。

  可經過爲控件模板添加觸發器來方便地解決這個問題。當一個屬性發生變化時,可以使用觸發器改變另外一個或多個屬性。在按鈕中至少但願響應IsMouseOver和IsPressed屬性。下面的標記是控件模板的修改版本。當這些屬性發生變化時,會改變控件的顏色:

<Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
                <ContentPresenter RecognizesAccessKey="True" 
                                      Margin="{TemplateBinding Padding}"></ContentPresenter>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="DarkRed"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="IndianRed"/>
                    <Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Window.Resources>

  爲使該模板可以工做,還要進行另外一項修改。已爲Border元素指定一個名稱,而且該名稱被用於設置每一個設置其的TargetName屬性。經過這種方法,設置器能更新在模板中指定的Border元素的Background和BorderBrush屬性。使用名稱是確保更新模板特定部分的最容易方法。可建立一條元素類型規則來影響全部Border元素(緣由是已經知道在按鈕模板中只有一個邊框),但若是在之後改變模板,這種方法更清晰,也跟更靈活。

  在全部按鈕(以及其餘大部分控件)中還須要另外一個元素——焦點指示器。雖然沒法改變現有的邊框以天津焦點效果,可是能夠很容易地天津另外一個元素以顯示是否具備焦點,而且能夠簡單地使用觸發器根據Button.IsKeyboardFocused屬性顯示或隱藏該元素。儘管可以使用許多方法建立焦點效果,但下面的示例值只天津了一個具備虛線邊框的透明的Rectangle元素。Rectangle元素不能包含子內容,從而須要確保Rectangle元素和其他內容相互重疊。完成該操做最容易得方法是,使用只有一個單元格的Grid空哦關鍵來封裝Rectangle元素和ContentPresenter元素,這兩個元素位於同一個單元格中。

  下面是修改後的支持焦點的的模板:

<Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
                <Grid>
                    <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
                               StrokeThickness="1" StrokeDashArray="1 2"
                               SnapsToDevicePixels="True"></Rectangle>
                    <ContentPresenter RecognizesAccessKey="True" 
                                      Margin="{TemplateBinding Padding}"></ContentPresenter>
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="DarkRed"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="IndianRed"/>
                    <Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki"/>
                </Trigger>
                <Trigger Property="IsKeyboardFocused" Value="True">
                    <Setter TargetName="FocusCue" Property="Visibility" Value="Visible"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Window.Resources>

  設置器在此使用TargetName屬性茶盅須要改變的元素。

  下圖顯示了使用修改版模板的三個按鈕。第二個按鈕當前具備焦點(經過虛線矩形表示),而鼠標正好懸停在第三個按鈕上。

   爲了潤色該按鈕,還須要另外一個觸發器。當按鈕的IsEnable屬性變爲false是,該觸發器改變按鈕的背景色(也可改變文本的前景色):

<Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
                <Grid>
                    <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
                               StrokeThickness="1" StrokeDashArray="1 2"
                               SnapsToDevicePixels="True"></Rectangle>
                    <ContentPresenter RecognizesAccessKey="True" 
                                      Margin="{TemplateBinding Padding}"></ContentPresenter>
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="DarkRed"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="IndianRed"/>
                    <Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki"/>
                </Trigger>
                <Trigger Property="IsKeyboardFocused" Value="True">
                    <Setter TargetName="FocusCue" Property="Visibility" Value="Visible"/>
                </Trigger>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter TargetName="Border" Property="TextBlock.Foreground" Value="Gray" />
                    <Setter TargetName="Border" Property="Background" Value="MistyRose" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Window.Resources>

  爲確保該規則優先與其餘相沖突的觸發器設置,應當在觸發器列表的末尾定義它。這樣,無論IsMouseOver屬性是否爲true,IsEnabled屬性觸發器都具備優先權,而且按鈕保持未激活狀態的外觀。

  下圖設置按鈕不可用時所示的圖片:

4、使用動畫的觸發器

  觸發器並不是侷限於設置屬性。當特定屬性發生變化時,還可使用事件觸發器運行動畫。

  乍一看,這好像有些曲折,但除了最簡單的WPF控件外,觸發器實際上時其餘全部WPF控件的關鍵要素。例如,考慮到目前位置研究過的按鈕。目前,當鼠標移到按鈕上時,該按鈕當即從一種顏色切換到另外一種顏色。然而,更時髦的按鈕可能使用一個很是短暫的動畫從一種顏色昏倒到其餘顏色,從而建立微妙但優雅的效果。相似地,按鈕可以使用動畫改變焦點提示矩形的透明度,當按鈕獲取焦點時將快速淡入到試圖中,而不是驟然顯示。換句話說,事件觸發器容許控件更通暢地一點點從一個狀態改變到另外一個狀態,從而進一步潤色其外觀。

  下面是從新設計的按鈕模板,當鼠標懸停在按鈕上時,該模板使用觸發器實現按鈕顏色脈衝效果(在紅色和藍色之間不斷切換)。當鼠標離開時,使用一個單獨的持續1秒得動畫,將按鈕背景返回到其正常顏色:

<Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
                <Grid>
                    <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
                               StrokeThickness="1" StrokeDashArray="1 2"
                               SnapsToDevicePixels="True"></Rectangle>
                    <ContentPresenter RecognizesAccessKey="True" 
                                      Margin="{TemplateBinding Padding}"></ContentPresenter>
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <EventTrigger RoutedEvent="MouseEnter">
                    <BeginStoryboard>
                        <Storyboard>
                            <ColorAnimation Storyboard.TargetName="Border"
                                Storyboard.TargetProperty="Background.Color" 
                                  To="Blue" AutoReverse="True" RepeatBehavior="Forever" Duration="0:0:1"></ColorAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <EventTrigger RoutedEvent="MouseLeave">
                    <BeginStoryboard>
                        <Storyboard>
                            <ColorAnimation Storyboard.TargetName="Border"
                                            Storyboard.TargetProperty="Background.Color"
                                            Duration="0:0:0.5"></ColorAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="IndianRed" />
                    <Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki" />
                </Trigger>
                <Trigger Property="IsKeyboardFocused" Value="True">
                    <Setter TargetName="FocusCue" Property="Visibility" Value="Visible" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Window.Resources>

  可以使用兩種等價的方法添加鼠標懸停動畫——建立響應MouseEnter和MouseLeave事件的事件觸發器,或建立當IsMouseOver屬性發生變化時添加進入和退出動做的屬性觸發器。最終效果圖以下所示:

  該例使用兩個ColorAnimation對象來改變按鈕。下面是可能但願使用EventTrigger驅動的動畫只需的其餘一些任務:

  •   顯示或隱藏元素。爲此,須要改變控件模板中元素的Opacity屬性。
  •   改變形狀或位置。可以使用TranslateTransform對象調整元素的位置(例如,稍偏移元素使按鈕具備已被按下的感受)。當用戶將鼠標移到元素上時,可以使用ScaleTransform或RotateTransform對象稍微旋轉元素的外觀。
  •   改變光照或着色。爲此,需使用改變繪製背景色畫刷的動畫。可以使用ColorAnimation動畫改變SolidBrush畫刷中的顏色,也可動態顯示更復雜的畫刷以獲得更高級的效果。例如,使用LinearGradientBrush畫刷中的一種顏色(這是默認按鈕控件模板執行的操做),也可改變RadialGradientBrush畫刷的中心點。
相關文章
相關標籤/搜索