GUI相关知识
1.附加属性:不属于自身类,由父容器设置,例Grid.Row、Canvas.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
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的区别
| TemplatePart | TemplateBinding | |
|---|---|---|
| 作用 | 用来获取模板内部控件实例对象,做事件订阅、调用方法 | 在模板内部绑定控件的依赖属性值,只传数据,拿不到控件对象 |
| 使用位置 | C# 类上面写 Attribute | XAML 的 ControlTemplate 内部 |
| 例子 | [TemplatePart(Name="PART_Btn",Type=typeof(Button))] |
Text="{TemplateBinding Title}" |
浙公网安备 33010602011771号