首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

Windows 8 Store Apps 開發實戰(16):控件核心基石 — 依賴屬性、附加屬性、繼承體系與路由事件

     編輯:關於.NET

前言

在 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)
    {
        // 同依賴屬性回呼
    }
}

在 XAML 中使用

<myControls:MyControl
    Title="{Binding Value, ElementName=slider}"
    myControls:MyAttachedProperty.SubTitle="{Binding Value, ElementName=slider}" />

三、控件繼承體系 — 從 DependencyObject 到具體控件

理解繼承鏈能幫助你快速定位功能來源:

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 的冒泡與命中測試

<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

  • IsHitTestVisible="true"(預設):元素可接收觸控/滑鼠輸入,參與命中測試
  • IsHitTestVisible="false":元素及其所有子元素對輸入系統「隱形」,事件直接穿透到下層

範例中 borderPurple 設為 False,導致它與內部的 borderYellow 皆無法接收 Tapped 事件。

六、完整自定義控件範例結構

MyControls/themes/generic.xaml

<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>

MyControls/themes/MyControl.xaml

<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>

MyControls/MyControl.cs

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) { }
    }
}

七、依賴屬性與附加屬性的實際應用頁面

DependencyPropertyDemo.xaml

<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>

AttachedPropertyDemo.xaml

<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 控件開發的四大核心基石:

  1. 依賴屬性:賦予屬性樣式、綁定、動畫、繼承、變更通知等超能力
  2. 附加屬性:跨類別的全域屬性機制,實現佈局與行為解耦
  3. 繼承體系:DependencyObject → UIElement → FrameworkElement → Control → 具體控件,各層職責清晰
  4. 路由事件與命中測試:冒泡機制、Handled 控制、AddHandler(handledEventsToo)、IsHitTestVisible 穿透

掌握這些概念,你就擁有了開發專業級自定義控件、深度客製化既有控件、排查疑難事件/佈局問題的鑰匙。


原始碼下載:https://files.cnblogs.com/webabcd/Windows8.rar

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved