在 Windows 8 Store Apps(WinRT/XAML)的開發世界裡,控件系統建立在幾大核心支柱之上:依賴屬性、附加屬性、控件繼承體系、路由事件 與 命中測試。掌握這些概念,不僅能讓你輕鬆閱讀現有控件原始碼,更能開發出高度可複用、可樣式化、支援動畫與綁定的自定義控件。本文將透過完整的程式碼範例,逐層剖析這些基石。
普通 CLR 屬性只是簡單的封裝欄位,而 DependencyProperty 則賦予屬性以下進階能力:
Style.Setter 或 ControlTemplate 中設定{Binding ...}、{TemplateBinding ...}Storyboard 的目標屬性FontSize、Foreground)PropertyMetadata 回呼即時感知值的變化public class MyControl : Control
{
// 1. CLR 屬性包裝器(方便程式碼存取)
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
// 2. 靜態唯讀欄位:註冊依賴屬性
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(
"Title", // 屬性名稱
typeof(string), // 屬性型別
typeof(MyControl), // 擁有者型別
new PropertyMetadata("", OnTitleChanged)); // 預設值與變更回呼
// 3. 靜態回呼:屬性值變更時觸發
private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var newVal = e.NewValue;
var oldVal = e.OldValue;
// 可在此處執行額外邏輯:強制型別轉換、觸發事件、更新視覺狀態等
}
}
附加屬性的特殊之處在於:它定義在某個類別中,卻可被「任何」DependencyObject 衍生類別設定與讀取。典型例子有 Grid.Row、Canvas.Left、ToolTipService.ToolTip。
public class MyAttachedProperty
{
// Get/Set 靜態方法:XAML 解析器會自動呼叫
public static string GetSubTitle(DependencyObject obj)
=> (string)obj.GetValue(SubTitleProperty);
public static void SetSubTitle(DependencyObject obj, string value)
=> obj.SetValue(SubTitleProperty, value);
// 註冊附加屬性:RegisterAttached
public static readonly DependencyProperty SubTitleProperty =
DependencyProperty.RegisterAttached(
"SubTitle",
typeof(string),
typeof(MyAttachedProperty),
new PropertyMetadata("", OnSubTitleChanged));
private static void OnSubTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// 同依賴屬性回呼
}
}
<myControls:MyControl
Title="{Binding Value, ElementName=slider}"
myControls:MyAttachedProperty.SubTitle="{Binding Value, ElementName=slider}" />
理解繼承鏈能幫助你快速定位功能來源:
DependencyObject
└─ UIElement // 視覺樹節點、輸入事件、命中測試、RenderTransform
└─ FrameworkElement // 資料綁定、樣式、佈局、資源字典、命名範圍
├─ Control // 模板、焦點、邊框、背景、前景、字體屬性
│ ├─ ContentControl // 單一 Content(Button、ScrollViewer、AppBar...)
│ │ └─ ButtonBase // 點擊行為
│ │ ├─ ToggleButton (RadioButton、CheckBox)
│ │ └─ RangeBase (ProgressBar、Slider、ScrollBar)
│ └─ ItemsControl // 集合呈現
│ └─ Selector // 可選擇項
│ └─ ListViewBase (GridView、ListView)
├─ Panel // 佈局容器
│ └─ Grid、StackPanel、Canvas、VariableSizedWrapGrid...
├─ TextBlock、WebView、Image...
路由事件允許事件從來源元素沿著視覺樹向上傳遞(冒泡),父元素可選擇處理或讓事件繼續冒泡。
TappedRoutedEventArgs.OriginalSource:真正引發事件的元素TappedRoutedEventArgs.Handled:設為 true 停止冒泡UIElement.AddHandler(routedEvent, handler, handledEventsToo):handledEventsToo=true 即使子元素已處理仍會觸發<Border Name="borderRed" Background="Red" Width="300" Height="300">
<Border Name="borderGreen" Background="Green" Width="250" Height="250" Tapped="borderGreen_Tapped">
<Border Name="borderBlue" Background="Blue" Width="200" Height="200" Tapped="borderBlue_Tapped">
<Border Name="borderOrange" Background="Orange" Width="150" Height="150" Tapped="borderOrange_Tapped">
<Border Name="borderPurple" Background="Purple" Width="100" Height="100"
Tapped="borderPurple_Tapped" IsHitTestVisible="False">
<Border Name="borderYellow" Background="Yellow" Width="50" Height="50"
Tapped="borderYellow_Tapped" />
</Border>
</Border>
</Border>
</Border>
</Border>
public sealed partial class RoutedEventDemo : Page
{
public RoutedEventDemo()
{
this.InitializeComponent();
// handledEventsToo = true:即使子元素 Handled=true,borderRed 仍會收到事件
borderRed.AddHandler(UIElement.TappedEvent, new TappedEventHandler(borderRed_Tapped), true);
}
private void borderBlue_Tapped(object sender, TappedRoutedEventArgs e)
{
lblMsg.Text += $"borderBlue tapped, originalSource: {(e.OriginalSource as FrameworkElement).Name}\n";
e.Handled = true; // 停止冒泡到 borderGreen
}
private void borderPurple_Tapped(object sender, TappedRoutedEventArgs e)
{
// 不會觸發,因為 IsHitTestVisible="False"
}
}
IsHitTestVisible="true"(預設):元素可接收觸控/滑鼠輸入,參與命中測試IsHitTestVisible="false":元素及其所有子元素對輸入系統「隱形」,事件直接穿透到下層範例中 borderPurple 設為 False,導致它與內部的 borderYellow 皆無法接收 Tapped 事件。
<ResourceDictionary
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyControls">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///MyControls/themes/MyControl.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
<ResourceDictionary
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyControls">
<Style TargetType="local:MyControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MyControl">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<StackPanel>
<TextBlock Text="{TemplateBinding Title}"
Foreground="White" FontSize="26.667" />
<TextBlock Text="{TemplateBinding local:MyAttachedProperty.SubTitle}"
Foreground="White" FontSize="14.667" />
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml;
namespace MyControls
{
public class MyControl : Control
{
public MyControl()
{
this.DefaultStyleKey = typeof(MyControl);
}
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(MyControl),
new PropertyMetadata("", OnTitleChanged));
private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// 處理變更邏輯
}
}
public class MyAttachedProperty
{
public static string GetSubTitle(DependencyObject obj)
=> (string)obj.GetValue(SubTitleProperty);
public static void SetSubTitle(DependencyObject obj, string value)
=> obj.SetValue(SubTitleProperty, value);
public static readonly DependencyProperty SubTitleProperty =
DependencyProperty.RegisterAttached("SubTitle", typeof(string), typeof(MyAttachedProperty),
new PropertyMetadata("", OnSubTitleChanged));
private static void OnSubTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { }
}
}
<Page
x:Class="XamlDemo.Controls.Basic.DependencyPropertyDemo"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:myControls="using:MyControls">
<Grid Background="Transparent">
<StackPanel Margin="120 0 0 0">
<myControls:MyControl Background="Blue" BorderBrush="Yellow" BorderThickness="1"
Width="100" HorizontalAlignment="Left"
Title="{Binding Value, ElementName=slider}" />
<Slider Name="slider" Width="100" Minimum="0" Maximum="200"
IsThumbToolTipEnabled="False" HorizontalAlignment="Left" />
</StackPanel>
</Grid>
</Page>
<Page
x:Class="XamlDemo.Controls.Basic.AttachedPropertyDemo"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:myControls="using:MyControls">
<Grid Background="Transparent">
<StackPanel Margin="120 0 0 0">
<myControls:MyControl Background="Blue" BorderBrush="Yellow" BorderThickness="1"
Width="100" HorizontalAlignment="Left"
Title="{Binding Value, ElementName=slider}"
myControls:MyAttachedProperty.SubTitle="{Binding Value, ElementName=slider}" />
<Slider Name="slider" Width="100" Minimum="0" Maximum="200"
IsThumbToolTipEnabled="False" HorizontalAlignment="Left" />
</StackPanel>
</Grid>
</Page>
本文完整涵蓋了 Windows 8 Store Apps 控件開發的四大核心基石:
DependencyObject → UIElement → FrameworkElement → Control → 具體控件,各層職責清晰Handled 控制、AddHandler(handledEventsToo)、IsHitTestVisible 穿透掌握這些概念,你就擁有了開發專業級自定義控件、深度客製化既有控件、排查疑難事件/佈局問題的鑰匙。