GUI相关知识

1.附加属性:不属于自身类,由父容器设置,例Grid.RowCanvas.Left

<Button Grid.Row="0" Canvas.Left="20"/>

  

2.依赖属性 DependencyProperty

普通属性:就是 C# 类的普通属性,后台存一个私有字段。 

依赖属性:WPF 特有,不存私有字段,值存到DependencyObject 内部哈希表,支持绑定、样式、动画、值继承。

对比维度普通 CLR 属性WPF 依赖属性 DependencyProperty
存储位置 存于类的私有_xxx字段,对象实例内存里 不使用私有字段,存储在DependencyObject内部的哈希表集合
基类要求 无,任意 class 都可以写 必须继承 DependencyObject(Control、UIElement 都继承它)
读写方式 直接 get/set 访问私有字段 通过 GetValue() / SetValue()读写;包装一层 CLR 属性仅为方便 C# 代码调用
XAML 支持 不支持样式、触发器、绑定、动画 ✅支持数据绑定、Style、Trigger、Storyboard 动画
属性值继承 不能,对象之间互不影响 ✅支持属性值继承,父容器设置,子控件自动继承(FontSize 等)
值优先级 只有一个值,赋值就覆盖旧值 多层值优先级:本地赋值 > 触发器 > 样式 > 主题默认值,按优先级生效
内存开销 每个实例都占一份内存;大量实例时内存开销大 静态唯一DependencyProperty对象是 static 共享;仅当值≠默认值时才存入哈希表;大量控件更省内存
CLR 包装器行为 get/set 一定会执行 WPF 内部会绕过 get/set 直接调用 GetValue/SetValue;不要在 get、set 里写业务逻辑!
变更通知 需要手动实现 INotifyPropertyChanged,自己触发事件 内置变更回调PropertyChangedCallback,属性变化自动回调
验证 自己写逻辑 内置ValidateValueCallback值验证回调

 

//普通属性:私有字段存储
private string _title;
public string Title
{
    get { return _title; }
    set { _title = value; }
}

public string Title { //这个get/set只是给C#代码调用,WPF绑定时不跑这里! get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); } //静态、全局唯一,所有控件实例共用这一个对象 public static readonly DependencyProperty TitleProperty = DependencyProperty.Register( nameof(Title), typeof(string), typeof(MyControl), new PropertyMetadata("默认值", OnTitleChanged)); //属性变更回调 private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { //属性发生变化在这里处理业务 }

  

 

 

3.路由事件RouteEvent

事件沿着可视化树传递,分 3 种策略

Bubble 冒泡:子控件向上传到父容器(Click)

Tunnel 隧道:父先触发,向下传递,前缀Preview,如PreviewMouseDown

Direct 直接:仅自身触发

e.Handled=true 终止事件传递

 

4.资源Resource

资源可以存放颜色、画刷、样式、模板、转换器,可复用,减少重复代码。 

查找顺序:控件自身 Resources →父容器 →Window →App.xaml 全局资源

<!--控件内资源-->
<Window.Resources>
    <SolidColorBrush x:Key="MainColor" Color="#2196F3"/>
</Window.Resources>
<!--使用-->
<Border Background="{StaticResource MainColor}"/>

{StaticResource Key}:加载一次,编译期查找

{DynamicResource Key}:运行时动态更新资源,适合主题切换

 

5.样式和触发器

Style批量设置控件属性,TargetType指定控件类型,Setter设置属性。

<Style x:Key="BtnStyle" TargetType="Button">
    <Setter Property="Width" Value="120"/>
    <Setter Property="Height" Value="35"/>
</Style>
<!--使用-->
<Button Style="{StaticResource BtnStyle}"/>

  

6.触发器

PropertyTrigger属性触发器

<Trigger Property="IsMouseOver" Value="True">
    <Setter Property="Background" Value="Red"/>
</Trigger>

DataTrigger数据触发器:绑定的数据满足条件触发

<Window.Resources>
    <Style x:Key="DataTriggerDemoStyle" TargetType="Border">
        <Setter Property="Width" Value="200"/>
        <Setter Property="Height" Value="80"/>
        <Setter Property="Background" Value="Gray"/>

        <Style.Triggers>
            <!-- DataTrigger:绑定ViewModel的Status,当Status=1时触发 -->
            <DataTrigger Binding="{Binding Status}" Value="1">
                <Setter Property="Background" Value="Green"/>
                <Setter Property="CornerRadius" Value="8"/>
            </DataTrigger>

            <!-- 绑定ViewModel布尔属性,IsEnable = false触发 -->
            <DataTrigger Binding="{Binding IsEnable}" Value="False">
                <Setter Property="Opacity" Value="0.4"/>
            </DataTrigger>
        </Style.Triggers>
    </Style>
</Window.Resources>

<!-- 使用 -->
<Border Style="{StaticResource DataTriggerDemoStyle}"/>

MultiTrigger多条件触发器

<Style x:Key="MultiTriggerDemoStyle" TargetType="Button">
    <Setter Property="Width" Value="150"/>
    <Setter Property="Height" Value="40"/>

    <Style.Triggers>
        <!-- MultiTrigger:所有条件同时成立才执行 -->
        <MultiTrigger>
            <MultiTrigger.Conditions>
                <!--条件1:鼠标悬浮上面-->
                <Condition Property="IsMouseOver" Value="True"/>
                <!--条件2:按钮IsEnabled为true-->
                <Condition Property="IsEnabled" Value="True"/>
            </MultiTrigger.Conditions>
            <!--全部条件满足才执行下面Setter-->
            <MultiTrigger.Setters>
                <Setter Property="Background" Value="#4488dd"/>
                <Setter Property="Foreground" Value="White"/>
            </MultiTrigger.Setters>
        </MultiTrigger>
    </Style.Triggers>
</Style>

<Button Style="{StaticResource MultiTriggerDemoStyle}" Content="Multi测试"/>

 

注意MultiTrigger和MultiDataTrigger的区别:MultiTrigger判断控件自身属性(Property),MultiDataTrigger判断绑定的数据(Binding)

例如MultiDataTrigger的使用如下:

<Style.Triggers>
    <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
            <Condition Binding="{Binding Status}" Value="2"/>
            <Condition Binding="{Binding IsEnable}" Value="True"/>
        </MultiDataTrigger.Conditions>
        <MultiDataTrigger.Setters>
            <Setter Property="Background" Value="Orange"/>
        </MultiDataTrigger.Setters>
    </MultiDataTrigger>
</Style>

  

EventTrigger事件触发器,专门用来执行动画StoryBoard

 

<Style x:Key="EventTriggerDemoStyle" TargetType="Rectangle">
    <Setter Property="Width" Value="120"/>
    <Setter Property="Height" Value="60"/>
    <Setter Property="Fill" Value="LightBlue"/>
    <Style.Triggers>
        <EventTrigger RoutedEvent="MouseEnter">
            <EventTrigger.Actions>
                <!--鼠标进入,执行动画,宽度放大-->
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetProperty="Width"
                                         To="200" Duration="0:0:0.3"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger.Actions>
        </EventTrigger>

        <EventTrigger RoutedEvent="MouseLeave">
            <EventTrigger.Actions>
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetProperty="Width"
                                         To="120" Duration="0:0:0.3"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger.Actions>
        </EventTrigger>
    </Style.Triggers>
</Style>

<Rectangle Style="{StaticResource EventTriggerDemoStyle}"/>

  

注意事项:

  • EventTrigger 监听路由事件 RoutedEvent
  • 内部只能放操作,最常用BeginStoryboard播放动画
  • 不能写 Setter,不能改普通属性,只能动画
  • 可以写在 ControlTemplate.Triggers,CustomControl 模板内部大量使用

总结上述:触发器可以写在Style.Triggers,也可以写在模版内部ControlTemplate.Trigger内部

<ControlTemplate TargetType="local:MyCustomControl">
    <Border x:Name="MainBorder">
        <ContentPresenter/>
    </Border>
    <ControlTemplate.Triggers>
        <!--属性触发器-->
        <Trigger Property="IsMouseOver" Value="True">
            <Setter TargetName="MainBorder" Property="Background" Value="SkyBlue"/>
        </Trigger>
        <!--DataTrigger也可以写在这里-->
        <DataTrigger Binding="{Binding IsOk}" Value="True">
            <Setter TargetName="MainBorder" Property="CornerRadius" Value="10"/>
        </DataTrigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

  

7.模版系统

WPF 控件逻辑与视觉分离,控件只负责行为,外观交给模板。

7.1ControlTemplate 控件模板:重写控件整体外观

<ControlTemplate x:Key="CustomBtnTemplate" TargetType="Button">
    <Border Background="{TemplateBinding Background}">
        <ContentPresenter/>
    </Border>
</ControlTemplate>

  

7.2 DataTemplate数据模版

<ListBox.ItemTemplate>
    <DataTemplate>
        <TextBlock Text="{Binding Name}"/>
    </DataTemplate>
</ListBox.ItemTemplate>

  

7.3ItemPanel:修改集合控件内部布局容器,例如将ListBox改为横向排列

<ListBox.ItemsPanel>
    <ItemsPanelTemplate>
        <StackPanel Orientation="Horizontal"/>
    </ItemsPanelTemplate>
</ListBox.ItemsPanel>

  

8.数据绑定Binding

  • OneWay单向:数据→UI,数据变界面更新(默认)
  • TwoWay双向:UI 修改同步回数据源(TextBox.Text)
  • OneTime:初始化读一次,之后不再更新
  • OneWayToSource:UI 写入数据,不回显

9.数据元通知INotifyPropertyChange,属性变更通知界面刷新。集合用ObservableCollection<T>,集合增删自动通知 UI

public class User : INotifyPropertyChanged
{
    private string _name;
    public string Name
    {
        get => _name;
        set { _name = value; OnPropertyChanged(); }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    void OnPropertyChanged([CallerMemberName]string prop=null)
    {
        PropertyChanged?.Invoke(this,new PropertyChangedEventArgs(prop));
    }
}

  

10.绑定转换器IValueConverter

public class BoolToVisConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (bool)value ? Visibility.Visible : Visibility.Collapsed;
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        => throw new NotImplementedException();
}

XAML 注册资源后使用Converter={StaticResource xxxConverter}

 

11.自定义控件和用户控件

对比项CustomControl 自定义控件UserControl 用户控件
基类 继承 Control 继承 UserControl(ContentControl)
XAML 文件 没有自带 XAML,UI 全部靠Generic.xaml里的ControlTemplate实现 自带.xaml + .cs,直接在 XAML 里拖拽组合控件
模板位置 强制约定:Themes/Generic.xaml,主题资源字典 自身 XAML 文件内
核心机制 重写DefaultStyleKey,WPF 自动从 Generic.xaml 读取控件模板 直接实例化 XAML 定义的控件树
修改外观 外部可以完全重写ControlTemplate,彻底改变控件结构 外部只能 Style 修改属性,很难彻底改写内部控件结构
依赖属性 需要手动注册 DependencyProperty 可以直接用,也可以自定义依赖属性
事件 可自定义 RoutedEvent 路由事件 一般用现有控件事件,很少自定义路由事件
适用场景 通用控件库、可复用组件,允许使用者重写模板(如第三方 UI 库控件) 业务页面组件,把一堆控件打包复用,不需要外部改内部结构
可视化树 运行时由 ControlTemplate 生成 XAML 编译直接生成可视化树
主题支持 原生支持系统主题切换 支持,但需要手动写资源
代码复杂度 高,需要写模板、默认样式 低,快速拼装 UI
实例文件结构 MyControl.cs + Themes/Generic.xaml MyView.xaml + MyView.xaml.cs

 

 11.1TemplatePart作用详解

TemplatePart是一个特性【Attribute】,专门用于CustomControl(自定义控件), 如果设计的CustomControl 模板只是纯展示,不需要操作模板内部控件、不需要订阅内部控件事件,则完全不用写[TemplatePart]

 背景:CustomControl 的 UI 写在 ControlTemplate(Generic.xaml)里,C# 后台代码拿不到模板里面的控件实例,因为模板是运行时才生成可视化树。

概念核心
[TemplatePart(Name = "PART_InnerBtn", Type = typeof(Button))]
public class MyCustomControl : Control
{
    //……
}
Name:必须以 PART_ 为前缀,这是 WPF 约定规范。
Type:指定这个部件是什么控件类型。 

完整代码示例:关键是OnApplyTemplate()+GetTemplateChild的使用。

using System.Windows;
using System.Windows.Controls;

// 约定契约:模板中需要有名为 PART_OkBtn 的 Button
[TemplatePart(Name = "PART_OkBtn", Type = typeof(Button))]
public class MyCustomControl : Control
{
    private Button _okBtn;

    static MyCustomControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCustomControl),
            new FrameworkPropertyMetadata(typeof(MyCustomControl)));
    }

    // 模板应用完毕回调
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        // 获取模板内部的PART部件
        _okBtn = GetTemplateChild("PART_OkBtn") as Button;

        // 一定要判空!使用者重写模板时可能删掉这个PART
        if (_okBtn != null)
        {
            _okBtn.Click += InnerBtn_Click;
        }
    }

    private void InnerBtn_Click(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("模板内部按钮被点击");
    }
} 

对应Generic.xaml模板,必须定义同名x:Name="PART_OkBtn" 

<Style TargetType="local:MyCustomControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:MyCustomControl">
                <Border Background="LightBlue">
                    <!-- 和TemplatePart约定的名字完全对应 -->
                    <Button x:Name="PART_OkBtn" Content="确定"/>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style> 

综上所述:TemplatePart要解决的问题:1.控件逻辑与UI模版分离;2.允许外部使用者重写模板,但保留交互逻辑

 TemplatePart和TemplateBinding的区别

 TemplatePartTemplateBinding
作用 用来获取模板内部控件实例对象,做事件订阅、调用方法 在模板内部绑定控件的依赖属性值,只传数据,拿不到控件对象
使用位置 C# 类上面写 Attribute XAML 的 ControlTemplate 内部
例子 [TemplatePart(Name="PART_Btn",Type=typeof(Button))] Text="{TemplateBinding Title}"

 

posted @ 2026-08-08 00:57  HelloWorld庄先生  阅读(10)  评论(0)    收藏  举报