win10 uwp 自定義控制元件入門
本文告訴大家如何在 UWP 使用 CustomControl 自定義控制元件,在 UWP 的自定義控制元件的中文翻譯是模板化控制元件,通過自定義控制元件可以完全控制整個控制元件的佈局和渲染。
預設建立的自定義控制元件是沒有帶 xaml 的,如果想要讓 CustomControl 可以使用 xaml 就需要引入主題的方法
下面就來告訴大家如何使用 xaml 來做介面
在 CustomControl 使用 xaml 寫介面
在 UWP 主要的元素就是控制元件,可以說,整個 UWP 的介面都依靠控制元件畫出來的。使用 xaml 可以快速畫出好看的介面,而預設建立的 自定義控制元件和使用者控制元件不一樣,使用者控制元件會帶一個 xaml 直接修改就可以在設計器看到介面。
通過建立一個類繼承 Control 類,我這裡建立的是一個 Board 類
public sealed class Board : Control
然後在相同的資料夾,建立一個資源字典 Board.xaml 這樣可以對應資源字典和建立的控制元件
在資源字典先引用命名控制元件,我這裡建立 Board 是在 lindexi.UWP.Framework 名稱空間,就需要在資源字典引用xmlns:local="using:lindexi.UWP.Framework"
這樣才可以拿到對應的控制元件
namespace lindexi.UWP.Framework { public sealed class Board : Control { } }
新增一個 Style 指定為剛才建立的 Board 控制元件
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:lindexi.UWP.Framework"> <Style TargetType="local:Board"> </Style> </ResourceDictionary>
在這裡不新增 Key 就是預設所有的 Board 控制元件都使用這個樣式,通過修改 Template 的方法新增控制元件
<Style TargetType="local:Board"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:Board"> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>
通過 ControlTemplate 的方法裡面就和使用者控制元件一樣可以使用 xaml 寫出介面,我這裡就放一個 ContentControl 可以來定製
可以使用 ContentControl 的 Content 屬性放入任意的 UIElement 都可以加入視覺樹
<Style TargetType="local:Board"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:Board"> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <ContentControl x:Name="ContentControl" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>
但是現在的程式碼還沒完成,還需要在專案建立一個 Theme 資料夾,然後在這個資料夾裡面新增 Generic.xaml 資源字典,從這個字典引用剛才建立的 Board 資源字典,才可以在使用的時候找到
在 Generic.xaml 資源字典只需要新增下面的程式碼
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:lindexi.github.io"> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="ms-appx:///lindexi/Framework/Board.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary>
需要注意 ResourceDictionary 的路徑,修改為自己實際的控制元件的 xaml 檔案的路徑,注意這裡必須使用ms-appx:///
開頭,檔案使用的是相對於專案的路徑,如果使用的是相對於這個檔案的路徑,就會在執行的時候,在某個類的建構函式告訴
Failed to assign to property 'Windows.UI.Xaml.ResourceDictionary.Source' because the type 'Windows.Foundation.String' cannot be assigned to the type 'Windows.Foundation.Uri'.
雖然現在設定好了控制元件的 xaml 但是現在的 xaml 沒有內容,需要在 Board 類新增一些程式碼,讓大家可以看到自己的 xaml 是否可以在 Board 使用
首先是新增 TemplatePart 在 Board 類,這樣是在約定在 xaml 介面需要新增一個對應的控制元件,指定了控制元件的 Name 和這是一個什麼控制元件
[TemplatePart(Name = "ContentControl", Type = typeof(ContentControl))] public sealed class Board : Control
是否記得在 Board 的資源字典就寫了一個 ContentControl 類,雖然添加了約定但是還是需要將這個控制元件拿出來,通過重寫 OnApplyTemplate 方法就可以使用 GetTemplateChild 方法拿到 xaml 裡寫的控制元件
protected override void OnApplyTemplate() { Content = (ContentControl) GetTemplateChild("ContentControl"); }
這樣就可以拿到對應的 xaml 的控制元件,雖然介面都在不斷變化,但是這裡拿到的控制元件是需要使用強轉的方式,一旦找不到控制元件就給一個異常。
如果在 xaml 忘記寫了一個控制元件,通過 GetTemplateChild 方法會返回 null 而不是拋異常,但是建議在這個方法下面判斷拿到的如果是空,就丟擲異常
protected override void OnApplyTemplate() { var foo = GetTemplateChild("不存在的控制元件"); if (foo == null) { throw new ArgumentException("使用的模板不包含"); } }
我通過去拿一個不存在的控制元件,拿到的是空判斷是空就丟擲異常
通過這個方法就可以拿到在 xaml 定義的控制元件,拿到了之後就可以在程式碼修改,如何修改請看下面
佈局
如果已經寫了 xaml 在程式碼拿到了 xaml 的控制元件,自定義控制元件還可以修改佈局的方式
先在介面新增一些元素
public ContentControl Content { get; set; } private Grid _grid; protected override void OnApplyTemplate() { Content = (ContentControl) GetTemplateChild("ContentControl"); _grid = new Grid() { Children = { new TextBlock() { Text = "歡迎訪問我部落格 lindexi.github.io 裡面有很多 UWP WPF 部落格", HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center } } }; Content.Content = _grid; base.OnApplyTemplate(); }
通過重寫 MeasureOverride 方法可以拿到測量的值,在 UWP 的佈局和 WPF 的一樣,都是先進過測量再進行控制每個控制元件的座標和大小。
測量是什麼?在 UWP 的佈局過程,這裡提高了佈局過程,還需要繼續解釋一下什麼是佈局過程。在 UWP 會將所有的控制元件按照控制元件所在的容器,作為視覺樹,視覺樹的意思很簡單,我有一個 Grid 在裡面放在兩個 Grid 同時又在第一個 Grid 裡面新增一個文字,這時的控制元件可以使用樹這個資料結構表示。第一個節點是最上面的,也是最外層的 Grid 這個 Grid 有兩個子節點,分別就是放在 Grid 裡面的兩個 Grid 而這裡的兩個 Grid 的第一個 Grid 裡面也有一個節點就是文字。
在 UWP 通過 xaml 介面就可以知道控制元件的樹結構,如果熟悉樹這個結構就知道,可以使用遞迴的方式處理。也就是一個節點只處理這個節點的子節點,而不處理子節點的子節點,所以 UWP 的佈局就依賴這個視覺樹,通過佈局子節點的方式,然子節點自己遞迴這個佈局方法,佈局子節點的子節點。
那麼佈局是什麼?佈局就是讓子節點控制元件放在該放的地方,雖然定義了視覺樹,知道了一個控制元件的裡面包含了哪些控制元件,但是這個控制元件還沒準備好裡面的控制元件的座標和大小。例如我有一個容器是 StackPanel 這個容器需要讓裡面的控制元件按照垂直或水平的方式佈局,也就是在 StackPanel 垂直佈局裡面的控制元件,第二個控制元件的座標的 Y 點是第一個控制元件的座標的 Y 點加上控制元件的高度。假如第一個控制元件也是一個容器,那麼如何知道這個容器的的高度是多少?因為容器的大小可以是容器裡面的元素決定的,需要讓這個容器先知道他裡面的控制元件的大小才可以知道容器的大小。
這就是測量的過程,測量的過程就是讓每個控制元件知道子節點的大小,從而計算出控制元件的大小,然後將控制元件的大小返回給上一層,讓上一層可以知道子節點的大小。有了測量的過程,在進行 StackPenel 佈局的時候,就可以在測量的過程知道了控制元件大小,從而在可以安排每個控制元件座標。
這裡自定義的控制元件也是這樣,通過重寫 MeasureOverride 可以修改計算自定義控制元件的大小的方法,從而報告給上一層一個特殊的值。
如我這裡的控制元件是想要上一層給我多大的空間,我就要多大的空間,我可以通過重寫 MeasureOverride 方法,返回引數
protected override Size MeasureOverride(Size availableSize) { base.MeasureOverride(availableSize); return availableSize; }
因為我這個控制元件裡面有一些控制元件是需要在測量的過程重新給他一個值,我就可以這樣寫
protected override Size MeasureOverride(Size availableSize) { _grid.Height = availableSize.Height; _grid.Width = availableSize.Width; base.MeasureOverride(availableSize); return availableSize; }
處理測量的方法可以重寫,佈局的方法也可以重寫
通過重寫 ArrangeOverride 的方法可以做到實際的佈局,從測量的方法傳入的引數也許不是最外層控制元件在佈局的時候傳入的大小,假如我有一個 StackPanel 他的高度 100 寬度也是 100 在測量的過程就會傳入大小是 100x100 但是在佈局的過程就依賴當前的控制元件是在 StackPanel 的第幾個控制元件,減去前面控制元件用的地方在是這個控制元件可以用的。
本文的控制元件是不需要重新佈局的方法,現在看起來的控制元件的程式碼請看下面
[TemplatePart(Name = "ContentControl", Type = typeof(ContentControl))] public sealed class Board : Control { public Board() { this.DefaultStyleKey = typeof(Board); } protected override Size MeasureOverride(Size availableSize) { _grid.Height = availableSize.Height; _grid.Width = availableSize.Width; base.MeasureOverride(availableSize); return availableSize; } public ContentControl Content { get; set; } private Grid _grid; protected override void OnApplyTemplate() { Content = (ContentControl) GetTemplateChild("ContentControl"); _grid = new Grid() { Children = { new TextBlock() { Text = "歡迎訪問我部落格 lindexi.github.io 裡面有很多 UWP WPF 部落格", HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center } } }; Content.Content = _grid; base.OnApplyTemplate(); } }
在介面新增這個控制元件然後執行一下,可以看到介面居中顯示了這個控制元件