1. 程式人生 > >【WPF學習】第六十一章 組織模板資源

【WPF學習】第六十一章 組織模板資源

  為表達全國各族人民對抗擊新冠肺炎疫情鬥爭犧牲烈士和逝世同胞的深切哀悼,國務院今天釋出公告,決定2020年4月4日舉行全國性哀悼活動。

  當使用控制元件模板時,需要決定如何更廣泛地共享模板,以及是否希望自動地或明確地位用模板。

  第一個問題是關於希望在何處使用模板的問題。例如,是將它們限制在特定視窗中嗎?大多數情況下,控制元件模板應用於多個視窗,甚至可能應用於整個應用程式。為避免多次定義模板,可在Application類的Resources集合中定義模板資源。

  然而,為此需要考慮另一個事項。通常,控制元件模板在多個應用程式之間共享。單個應用程式很有可能使用單獨開發的模板。然而,一個應用程式只能有一個App.xaml檔案和一個Application.Resources集合。因此,在單獨資源字典中定義資源是一個更好的主意。這樣,可靈活地再特定視窗或在整個應用程式中使用資源。而且還可以結合使用樣式,因為任何應用程式都可以包含多個資源字典。為在Visual Studio中新增資源字典,在Solution Explorer視窗中右擊專案,選擇Add|New Item選單項,然後選擇Resources Dictionary(WPF)模板。

  在前面章節中介紹了資源字典,使用它們很容易,只需要為應用程式新增一個新的具有如下內容的XAML檔案即可:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    >
    <ControlTemplate x:Key="GradientButtonTemplate" TargetType="{x:Type Button}">
        ...
    </ControlTemplate>
</ResourceDictionary>

  雖然可將所有模板組合到單個資源字典檔案中,但富有經驗的開發人員更願意為每個控制元件模板建立單獨的資源字典。這是因為控制元件模板可能很快地變得過於複雜,並可能需要使用其他相關資源。將它們儲存在一個單獨的地方,並與其他控制元件相隔離,是一種很好的組織方式。

  為使用資源字典,只需要將他們新增到特定視窗或應用程式(這種情況更常見)的Resources集合中。可使用MergedDictionaries集合完成該工作。例如,如果按鈕模板在專案資料夾的Resources子資料夾下的Button.xaml檔案中,就可以在App.xaml檔案中使用以下標記:

<Application x:Class="ControlTemplates.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Menu.xaml"
    >
    <Application.Resources>
            <ResourceDictionary>
               <ResourceDictionary.MergedDictionaries>
                  <ResourceDictionary Source="Resources\Button.xaml"/>
               </ResourceDictionary.MergedDictionaries>
             </ResourceDictionary>
    </Application.Resources>
</Application> 

一、分解按鈕控制元件模板

  當完善或擴充套件控制元件模板時,可發現其中封裝了大量的不同細節,包括特定的形狀、幾何圖形和畫刷。從控制元件模板中提取這些細節並將他們定義為單獨的資源是一個好主意。一個原因是通過該步驟,可以更方便地再一組相關的控制元件中重用這些畫刷。例如,可能會決定建立使用相同的自定義Button、CheckBox和RadioButton控制元件。為使該工作更加容易,可為畫刷(名為Brushes.xaml)建立一個單獨的資源字典,並將該資源字典合併到每個控制元件(如Button.xaml、CheckBox.xaml和RadioButton.xaml)的資源字典中。

  為檢視這種技術的工作情況,分析下嗎的標記。這些標記代表了一個按鈕的完整資源字典,包括控制元件模板使用的資源、控制元件模板,以及為應用程式中每個按鈕應用控制元件模板的樣式規則。始終需要遵循這一順序,因為資源需要在使用之前先定義(如果在模板之後定義畫刷,將收到錯誤資訊,因為模板找不到所需的畫刷)。

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <!-- Resources used by the template. -->
    <RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3"
                         x:Key="HighlightBackground">
        <GradientStop Color="White" Offset="0"/>
        <GradientStop Color="Blue" Offset="0.4"/>
    </RadialGradientBrush>
    <RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3"
                         x:Key="PressedBackground">
        <GradientStop Color="White" Offset="0"/>
        <GradientStop Color="Blue" Offset="1"/>
    </RadialGradientBrush>

    <SolidColorBrush Color="Blue" x:Key="DefaultBackground"></SolidColorBrush>
    <SolidColorBrush Color="Gray" x:Key="DisabledBackground"></SolidColorBrush>

    <RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3"
                         x:Key="Border">
        <GradientStop Color="White" Offset="0"/>
        <GradientStop Color="Blue" Offset="1"/>
    </RadialGradientBrush>

    <!-- The button control template. -->
    <ControlTemplate x:Key="GradientButtonTemplate" TargetType="{x:Type Button}">
        <Border Name="Border" BorderBrush="{StaticResource Border}" BorderThickness="2"
       CornerRadius="2" Background="{StaticResource DefaultBackground}"
       TextBlock.Foreground="White">
            <Grid>
                <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
           StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True">
                </Rectangle>
                <ContentPresenter Margin="{TemplateBinding Padding}"
           RecognizesAccessKey="True"></ContentPresenter>
            </Grid>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter TargetName="Border" Property="Background"
           Value="{StaticResource HighlightBackground}" />
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter TargetName="Border" Property="Background"
           Value="{StaticResource PressedBackground}" />
            </Trigger>
            <Trigger Property="IsKeyboardFocused" Value="True">
                <Setter TargetName="FocusCue" Property="Visibility"
            Value="Visible"></Setter>
            </Trigger>
            <Trigger Property="IsEnabled" Value="False">
                <Setter TargetName="Border" Property="Background"
           Value="{StaticResource DisabledBackground}"></Setter>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</ResourceDictionary>

  下圖顯示了該模板定義的按鈕。在該例中,當用戶滑鼠移到按鈕上時,使用漸變填充。然而,漸變中心位於按鈕中央。如果希望建立更新穎的效果,例如跟隨滑鼠位置的漸變,就需要使用動畫或者編寫程式碼。

二、通過樣式應用模板

  這種設計存在侷限性,控制元件模板本質上硬編碼了一些細節,如顏色方案。這意味著如果希望在按鈕中使用相同的元素組合(Border、Grid、Rectangle和ContentPresenter)並採用相同的方式安排它們,但希望提供不同的顏色方案,就必須建立應用不同畫刷資源的新模板副本。

  這未必是個問題(畢竟,佈局和格式化細節可能緊密相關,以至於不希望以任何方式隔離它們)。但這確實限制了重用控制元件模板的能力。如果模板使用了元素的複合排列方式,並且希望重用這些具有各種不同格式化細節(通常是顏色和字型)的元素,可從模板中將這些細節提取出來,並將它們放到樣式中。

  為此,需要重新編寫模板。這次不能使用硬編碼的顏色,而需要使用模板繫結從控制元件屬性中提取出資訊。下面的示例為前面看到的特殊按鈕定義了一個精簡模板。控制元件模板將一些細節作為基礎的固定要素——焦點框和兩個單位寬的圓角邊框。背景和邊框畫刷是可配置的。唯一需要保留的觸發器是顯示焦點框的那個觸發器:

<ControlTemplate x:Key="GradientButtonTemplate" TargetType="{x:Type Button}">
        <Border Name="Border" BorderThickness="2" CornerRadius="2"
              Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}">
            <Grid>
                <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
           StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True">
                </Rectangle>
                <ContentPresenter Margin="{TemplateBinding Padding}"
           RecognizesAccessKey="True"></ContentPresenter>
            </Grid>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsKeyboardFocused" Value="True">
                <Setter TargetName="FocusCue" Property="Visibility"
            Value="Visible"></Setter>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>

  關聯的樣式應用這個控制元件模板,設定邊框和背景顏色,並新增觸發器以便根據按鈕的狀態改變背景色:

<!-- The style that applies the button control template. -->
    <Style TargetType="{x:Type Button}">
        <Setter Property="Control.Template"
       Value="{StaticResource GradientButtonTemplate}"></Setter>
        <Setter Property="BorderBrush"
       Value="{StaticResource Border}"></Setter>
        <Setter Property="Background"
         Value="{StaticResource DefaultBackground}"></Setter>
        <Setter Property="TextBlock.Foreground"
           Value="White"></Setter>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background"
           Value="{StaticResource HighlightBackground}" />
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter Property="Background"
           Value="{StaticResource PressedBackground}" />
            </Trigger>
            <Trigger Property="IsEnabled" Value="False">
                <Setter Property="Background"
           Value="{StaticResource DisabledBackground}"></Setter>
            </Trigger>
        </Style.Triggers>
    </Style>

  理想情況下,應能在控制元件模板中保留所有觸發器,因為它們代表控制元件的行為,並使用樣式簡單設定基本屬性。但在此如果希望樣式能夠設定顏色方案,是不可能實現的。

  為使用這個新模板,需要設定按鈕的Style屬性而不是Template屬性:

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

  現在可建立一些新樣式,這些樣式使用相同的模板,但為了應用新的顏色方案,應將模板繫結到不同的畫刷。

  使用這種方法存在的重要限制。在該樣式中不能使用Setter.TargetName屬性,因為樣式不包含控制元件模板(只是引用模板而已)。因此,樣式和觸發器有一定的限制。它們不能深入到視覺化樹中來改變巢狀的元素的這個方面。反而,樣式需要設定控制元件的屬性,而且控制元件中的元素需要使用模板來繫結來繫結 。

三、自動應用模板

  在當前示例中,每個按鈕負責使用Template或Style屬性將自身關聯到適當模板。如果使用控制元件模板,在應用程式中的特定位置建立特殊效果,這是合理的。但如果希望在具有自定義外觀的整個應用程式中改變每個按鈕的面板,這就不是很方便了。對於這種情況,可能會更希望應用程式中的所有按鈕自動請求新的模板。為實現該功能,需要通過樣式應用控制元件模板。

  技巧是使用型別樣式,這種樣式會自動影響響應的元素型別並設定Template屬性。下面是一個樣式示例,應將該樣式放到資源字典的資源集合中,從而為按鈕提供新外觀:

<Style TargetType="{x:Type Button}">
        <Setter Property="Control.Template"
       Value="{StaticResource GradientButtonTemplate}"></Setter>
    </Style>

  上面的程式碼可以工作,原因是樣式沒有指定鍵名,這意味著改用元素型別(Button)。

  請記住,仍可通過建立一個按鈕並將其Style屬性明確設定為null值,退出該樣式:

<Button Style="{x:Null}" ...></Button>

  包含基於型別的樣式的組合的資源字典通常(非正式地)被稱為主題(theme)。主題能夠實現非凡的效果。通過主題可為已有應用程式的所有控制元件重新應用面板,而根本不需要改使用者介面標記。需要做的全部工作就是為專案新增資源字典,並將其合併到App.xaml檔案的Application.Resources集合中。

  如果在Web上搜索,可找到許多能用於為WPF應用程式換膚的主題,例如,可下載WPF Futures版本中的幾個示例主題。

  為使用主題,為專案新增包含資源字典的.xaml檔案。例如,WPF Futuers提供了一個名為ExpressionDark.xaml的主題檔案。然後,需要在應用程式中啟用樣式。可逐個視窗地完成該工作,但更快捷的方法是通過新增如下所示的標記在應用程式級別匯入他們:

<Application ...>
    <Application.Resources>
        <ResourceDictionary Source="ExpressionDark.xaml"/>
    </Application.Resources>
</Application>

  現在將全面實施資源字典中基於型別的樣式,並將自動改變應用程式每個視窗的每個通用控制元件的外觀。如果是一位正在搜尋熱門使用者介面的應用程式開發人員,但不具備自己構建這類使用者介面的設計技能,那麼使用該技巧幾乎不需要付出努力就能很容易地插入第三方的精彩介面。

四、由使用者選擇的面板

  在一些應用程式中,可能希望動態改變模板,通常是根據使用者的個人愛好加以改變。這很容易發現,但文件沒有對比進行詳細說明。基本技術是在執行時載入新的資源字典,並使用新載入的資源字典代替當前的資源字典(不需要替換所有資源,字需要替換那些勇於面板的資源)。

  訣竅在於檢索ResourceDictionary物件,該物件經過編譯並作為資源嵌入到應用程式中。最簡單的方法是使用ResourceManager類來載入所需資源。

  例如,假定已建立用於定義同一個按鈕控制元件模板的替代版本的兩個資源。其中一個儲存在GradientButton.xaml檔案中,而另一個儲存在GradientButtonVariant.xaml檔案中。為了更好地組織資源,這兩個檔案都位於當前專案的Resources子資料夾中。

  現在可建立一個簡單的視窗,通過Resources集合使用這些資源中的一個,如下所示:

 <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/GradientButton.xaml"></ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>

  現在可通過如下程式碼使用不同的資源字典:

ResourceDictionary resourceDictionary = new ResourceDictionary();
            resourceDictionary.Source = new Uri(
              "Resources/GradientButtonVariant.xaml", UriKind.Relative);
            this.Resources.MergedDictionaries[0] = resourceDictionary;

  上面的程式碼載入GradientButtonVariant資源字典,並將它放置到MergedDictionaries集合的第一個位置。在此沒有清空MergedDictionaries集合或其他任何視窗的資源,因為可能連結到了其他席位繼續使用的資源字典。也沒有為MergedDictionaries集合新增新條目,因為這可能與位於不同集合中的同名資源發生衝突。

  如果正在為整個應用程式改變面板,可使用相同的方法,但應使用應用程式的資源字典,可使用如下程式碼更新這個資源字典:

Application.Current.Resources.MergedDictionaries[0]=newDictionary;

  還可使用pack URI語法載入在另一個程式集合中的資源字典:

ResourceDictionary resourceDictionary = new ResourceDictionary();
            resourceDictionary.Source = new Uri(
              "ControlTemplates;component/GradientButtonVariant.xaml", UriKind.Relative);
            this.Resources.MergedDictionaries[0] = resourceDictionary;

  當載入新的資源字典時,會自動使用新模板更新所有按鈕。如果當修改控制元件時不需要完全改變面板,還可以為面板提供基本樣式。

  該例假定GradientButton.xaml和GradientButtonVariant.xaml資源使用元素型別樣式自動改變按鈕。還有一種方法——通過手動設定Button物件的Template或Style屬性來選用新的模板。如果使用這種方法,務必使用DynamicResource應用,而不能使用StaticResource。如果使用Resource,當切換面板時不會更新按鈕模板。

  還有一種通過編寫程式碼載入資源字典的方法。可使用與為視窗建立帶阿媽隱藏類幾乎相同的方法,為資源字典建立程式碼隱藏類。然後就可以直接例項化這個類,而不是使用ResourceDictionary.Source屬性。這種方法有一個優點,他是強型別的(沒有機會為Source屬性輸入無效的URI),並且可為資源類新增屬性、方法以及其他功能。例如,可以使用這種方法,為自定義視窗模板建立具有事件處理程式碼的資源。

  儘管為資源字典建立程式碼隱藏類很容易,但Visual Studio不能自動完成該工作,需要為繼承自ResourceDictionary的部分類新增程式碼檔案,併為建構函式中呼叫InitializeComponent()方法:

public partial class GradientButtonVariant : ResourceDictionary
    {
        public GradientButtonVariant()
        {
            InitializeComponent();
        }
    }

  這裡使用的類名為GradientButtonVariant,而且該類儲存在GradientButtonVariant.xaml.cs檔案中。包含資源的XAML檔案被命名為GradientButtonVariant.xaml。不是必須使用一致的名稱,但這是一個好主意,並且在建立視窗以及建立頁面時也遵循了Visual Studio使用的這一約定。

  接下來將類連結到資源字典。通過為資源字典的根元素新增Class特性完成該工作,就想為視窗應用Class特性一樣,並且可為任何XAML類應用Class特性。然後提供完全限定的類名。在這個示例中,專案名稱是ControlTemplates,因此這是預設名稱空間,最後的標籤可能如下所示:

<ResourceDictionary x:Class="ControlTemplates.GradientButtonVariant">

  現在可使用該程式碼建立資源字典並將它應用於視窗:

GradientButtonVariant newDict = new GradientButtonVariant();
this.Resources.MergedDictionaries[0] = newDict;

  在Solution Explorer中,如果希望GradientButtonVariant.xaml.cs檔案巢狀在GradientButtonVariant.xaml檔案的下面,需要在文字編輯器中修改.csproj專案檔案。在<ItemGroup>部分,找到程式碼因此檔案,並將下面的程式碼:

<Compile Include="Resources\GradientButtonVariant.xaml.cs"/>

  修改為:

<Compile Include="Resources\GradientButtonVariant.xaml.cs">
    <DependentUpon>Resources\GradientButtonVariant.xaml</DependentUpon>
</Compile>

&n