大家在学习 WPF 的时候,前期最容易接触到的是控件、布局和数据绑定;但真正把这些能力串起来的,其实是 WPF 自己的一整套机制。 比如为什么有些属性能绑定、有些属性能做动画、为什么Grid.Row能写在Button上、为什么一个按钮点击后父级也能收到事件,这些问题的答案都藏在 WPF 的“底层特性”里。
目录
一、WPF 依赖属性
1. 什么是依赖属性
2. 依赖属性和普通属性有什么区别
3. 自定义依赖属性的 3 个步骤
4. 实战:给自定义控件定义依赖属性
5. 依赖属性的回调函数
二、WPF 附加属性
1. 什么是附加属性
2. 附加属性怎么定义
3. 实战:让 PasswordBox 支持绑定
三、WPF Transform 转换
1. Transform 是什么
2. 四种常见变换
2.1 RotateTransform 旋转
2.2 ScaleTransform 缩放
2.3 SkewTransform 倾斜
2.4 TranslateTransform 平移
3. 实战:TransformGroup 做图片查看器
四、WPF Effect 特效
1. Effect 是什么
2. DropShadowEffect 阴影效果
3. BlurEffect 模糊效果
五、WPF 路由事件
1. 什么是路由事件
2. 隧道事件和冒泡事件怎么理解
3. 自定义路由事件实战
六、总结
这篇文章结合实战案例,系统梳理 5 个非常重要的知识点:
依赖属性
附加属性
Transform 转换
Effect 特效
路由事件
如果你正在做自定义控件、MVVM 绑定、交互动画,或者面试里经常被问到 WPF 原理,这篇内容基本都绕不开。
一、WPF 依赖属性
1. 什么是依赖属性
依赖属性(DependencyProperty)是 WPF 属性系统的核心。它不是普通的 .NET 属性包装私有字段,而是一种由 WPF 属性系统统一管理的属性机制。
它最大的价值在于:一个属性的值不再只来自字段本身,而是可以同时受到本地值、样式、动画、数据绑定、资源、默认值等多种来源的影响。
也正因为如此,依赖属性天然支持这些 WPF 高频能力:
数据绑定
样式与模板
动画
默认值
属性变更回调
值继承
2. 依赖属性和普通属性有什么区别
| 对比项 | 普通 .NET 属性 | WPF 依赖属性 |
|---|---|---|
| 存储方式 | 一般存储在私有字段中 | 由 WPF 属性系统统一管理 |
| 是否支持绑定 | 不支持 | 支持 |
| 是否支持动画 | 不支持 | 支持 |
| 是否支持样式 | 不支持 | 支持 |
| 变更通知 | 需要手动写 | 可以通过回调处理 |
| 使用场景 | 普通业务类 | WPF 控件、可视化对象 |
普通属性写法如下:
private int length; public int Length { get { return length; } set { length = value; } }依赖属性写法如下:
public int MyProperty { get { return (int)GetValue(MyPropertyProperty); } set { SetValue(MyPropertyProperty, value); } } public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( "MyProperty", typeof(int), typeof(OwnerClass), new PropertyMetadata(0));3. 自定义依赖属性的 3 个步骤
定义一个依赖属性,一般分成 3 步:
定义
DependencyProperty静态字段使用
DependencyProperty.Register()完成注册用普通属性外壳包装
GetValue()和SetValue()
其中最关键的一句就是:
public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( "MyProperty", typeof(int), typeof(OwnerClass), new PropertyMetadata(0));这 4 个参数分别表示:
属性名
属性类型
所属类型
元数据(默认值、回调函数等)
4. 实战:给自定义控件定义依赖属性
下面我们定义一个Widget用户控件,用来显示图标、标题和数值。
前端 XAML:
<UserControl x:Class="Demo.Widget" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="root" FontSize="30" Foreground="#666666" BorderBrush="#8CDDCD"> <Border BorderBrush="{Binding ElementName=root, Path=BorderBrush}"> <Border.Style> <Style TargetType="Border"> <Setter Property="Padding" Value="10"/> <Setter Property="Background" Value="White"/> <Setter Property="BorderThickness" Value="0 3 0 0"/> <Setter Property="Margin" Value="5"/> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="#F7F9F9"/> </Trigger> </Style.Triggers> </Style> </Border.Style> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Value}" Foreground="{Binding ElementName=root, Path=Foreground}" FontSize="{Binding ElementName=root, Path=FontSize}" /> <TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding Title}" Foreground="{Binding ElementName=root, Path=Foreground}" FontSize="14" /> <TextBlock Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Text="{Binding Icon}" Foreground="{Binding ElementName=root, Path=BorderBrush}" FontSize="26" VerticalAlignment="Center"/> </Grid> </Border> </UserControl>后台代码:
public partial class Widget : UserControl { public Widget() { InitializeComponent(); DataContext = this; } public string Icon { get { return (string)GetValue(IconProperty); } set { SetValue(IconProperty, value); } } public static readonly DependencyProperty IconProperty = DependencyProperty.Register( "Icon", typeof(string), typeof(Widget), new PropertyMetadata("😃")); public string Title { get { return (string)GetValue(TitleProperty); } set { SetValue(TitleProperty, value); } } public static readonly DependencyProperty TitleProperty = DependencyProperty.Register( "Title", typeof(string), typeof(Widget), new PropertyMetadata("请输入标题")); public string Value { get { return (string)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( "Value", typeof(string), typeof(Widget), new PropertyMetadata("内容")); }使用时就可以像普通控件一样直接写:
<StackPanel Orientation="Horizontal"> <local:Widget Icon="¥" Title="本年度销售总额" Value="38452.21" Width="215" Height="100"/> <local:Widget Icon="💎" Title="系统访问量" Value="9985" Foreground="#415767" BorderBrush="#87BEE4" Width="225" Height="110"/> </StackPanel>这就是依赖属性最实用的地方:自定义控件既能对外暴露属性,又天然支持绑定、样式和后续扩展。
5. 依赖属性的回调函数
很多时候我们不只是想“存一个值”,而是想在值变化后立即执行逻辑,这时候就要用到PropertyChangedCallback。
例如:
public static readonly DependencyProperty CountProperty = DependencyProperty.Register( "Count", typeof(int), typeof(TrayControl), new PropertyMetadata(0, OnCountPropertyChanged)); private static void OnCountPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = d as TrayControl; control?.Initialize(); }当Count发生变化时,就会自动调用OnCountPropertyChanged。 这类写法非常适合做:
控件刷新
界面重绘
数据校验
联动其它属性
如果你还需要默认值、强制值修正等能力,也可以把这些逻辑写到PropertyMetadata中。
二、WPF 附加属性
1. 什么是附加属性
附加属性(Attached Property)可以理解成:某个属性本来不属于这个控件,但被另一个类型“附加”到了它身上。
最经典的例子就是:
<Grid> <Button Grid.Row="0" Content="按钮1"/> <Button Grid.Row="1" Content="按钮2"/> </Grid>这里的Row并不是Button自己定义的属性,而是Grid定义出来附加给子元素使用的属性,也就是Grid.Row。
所以,附加属性特别适合这种场景:
父容器给子元素打标签
控件间建立额外关系
给原本不支持某种能力的控件补功能
2. 附加属性怎么定义
Visual Studio 中输入propa,按两次Tab就能生成模板。
标准写法如下:
public static int GetMyProperty(DependencyObject obj) { return (int)obj.GetValue(MyPropertyProperty); } public static void SetMyProperty(DependencyObject obj, int value) { obj.SetValue(MyPropertyProperty, value); } public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.RegisterAttached( "MyProperty", typeof(int), typeof(OwnerClass), new PropertyMetadata(0));和依赖属性相比,附加属性最大的区别是:
使用
RegisterAttached()注册通过
GetXxx()/SetXxx()访问
3. 实战:让 PasswordBox 支持绑定
PasswordBox的Password并不是依赖属性,所以不能像TextBox.Text一样直接绑定。 这也是 WPF 初学者经常会踩的坑。
解决思路就是:写一个PasswordBoxHelper,通过附加属性给PasswordBox搭一座“桥”。
先准备一个支持通知的基类:
public class ObservableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }再定义实体类:
public class Person : ObservableObject { private string userName; public string UserName { get { return userName; } set { userName = value; RaisePropertyChanged(); } } private string password; public string Password { get { return password; } set { password = value; RaisePropertyChanged(); } } }核心桥接代码如下:
public class PasswordBoxHelper { public static string GetPassword(DependencyObject obj) { return (string)obj.GetValue(PasswordProperty); } public static void SetPassword(DependencyObject obj, string value) { obj.SetValue(PasswordProperty, value); } public static readonly DependencyProperty PasswordProperty = DependencyProperty.RegisterAttached( "Password", typeof(string), typeof(PasswordBoxHelper), new PropertyMetadata("", OnPasswordPropertyChanged)); private static void OnPasswordPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is PasswordBox passwordBox) { passwordBox.PasswordChanged -= PasswordBox_PasswordChanged; passwordBox.PasswordChanged += PasswordBox_PasswordChanged; } } private static void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e) { if (sender is PasswordBox passwordBox) { SetPassword(passwordBox, passwordBox.Password); } } } 然后在 XAML 中这样使用: <StackPanel Margin="80"> <TextBox Text="{Binding Person.UserName, UpdateSourceTrigger=PropertyChanged}" Width="200" Height="25"/> <PasswordBox local:PasswordBoxHelper.Password="{Binding Person.Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="200" Height="25"/> </StackPanel>这样就实现了:
PasswordBox.Password -> PasswordBoxHelper.Password -> ViewModel.Password
这个案例也是附加属性最经典的面试题之一。
三、WPF Transform 转换
1. Transform 是什么
Transform是 WPF 中负责二维图形变换的抽象基类,常见子类有 4 个:
RotateTransform:旋转ScaleTransform:缩放SkewTransform:倾斜TranslateTransform:平移
如果多个变换要同时使用,就交给TransformGroup组合处理。
2. 四种常见变换
2.1 RotateTransform 旋转
<Button Content="RotateTransform"> <Button.RenderTransform> <RotateTransform Angle="45" CenterX="50" CenterY="12.5"/> </Button.RenderTransform> </Button>常用属性:
Angle:旋转角度CenterX:旋转中心 X 坐标CenterY:旋转中心 Y 坐标
2.2 ScaleTransform 缩放
<Button Content="ScaleTransform"> <Button.RenderTransform> <ScaleTransform ScaleX="1.5" ScaleY="1.5" CenterX="50" CenterY="12.5"/> </Button.RenderTransform> </Button>常用属性:
ScaleXScaleYCenterXCenterY
2.3 SkewTransform 倾斜
<Border Width="120" Height="120" Background="LightBlue"> <Border.RenderTransform> <SkewTransform AngleX="20" AngleY="10" CenterX="60" CenterY="60"/> </Border.RenderTransform> </Border>常用属性:
AngleXAngleYCenterXCenterY
2.4 TranslateTransform 平移
<Border Width="120" Height="120" Background="LightGreen"> <Border.RenderTransform> <TranslateTransform X="80" Y="30"/> </Border.RenderTransform> </Border>常用属性:
XY
3. 实战:TransformGroup 做图片查看器
如果我们想同时支持图片拖拽和平滑缩放,就不能只靠单一变换,而是要把ScaleTransform和TranslateTransform组合起来。
XAML:
<Canvas x:Name="canvas" Background="Transparent" MouseWheel="canvas_MouseWheel" MouseMove="canvas_MouseMove" MouseLeftButtonDown="canvas_MouseLeftButtonDown" MouseLeftButtonUp="canvas_MouseLeftButtonUp"> <Image x:Name="image" Source="/Images/mm.jpg"/> </Canvas>后台代码:
private bool isMouseDown = false; private Point mousePoint = new Point(0, 0); private TranslateTransform translateTransform = new TranslateTransform(); private ScaleTransform scaleTransform = new ScaleTransform(); private TransformGroup group = new TransformGroup(); public MainWindow() { InitializeComponent(); Loaded += (s, e) => { group.Children.Add(scaleTransform); group.Children.Add(translateTransform); image.RenderTransform = group; var scale = Math.Min( canvas.ActualWidth / image.ActualWidth, canvas.ActualHeight / image.ActualHeight); scaleTransform.ScaleX = scale; scaleTransform.ScaleY = scale; translateTransform.X = (canvas.ActualWidth - image.ActualWidth * scale) / 2; }; } private void canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { isMouseDown = true; mousePoint = e.GetPosition(canvas); } private void canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { isMouseDown = false; } private void canvas_MouseMove(object sender, MouseEventArgs e) { var position = e.GetPosition(canvas); if (isMouseDown) { translateTransform.X += position.X - mousePoint.X; translateTransform.Y += position.Y - mousePoint.Y; mousePoint = position; } } private void canvas_MouseWheel(object sender, MouseWheelEventArgs e) { var delta = e.Delta * 0.001; var position = e.GetPosition(canvas); if (scaleTransform.ScaleX + delta < 0.1) return; Point inversePoint = group.Inverse.Transform(position); scaleTransform.ScaleX += delta; scaleTransform.ScaleY += delta; translateTransform.X = -(inversePoint.X * scaleTransform.ScaleX - position.X); translateTransform.Y = -(inversePoint.Y * scaleTransform.ScaleY - position.Y); }这个案例里最值得记住的一点是:缩放不是简单改倍率,还要同步修正平移量,这样才能做到“以鼠标所在位置为中心缩放”。
四、WPF Effect 特效
1. Effect 是什么
Effect是 WPF 里的特效基类,常见的两个子类是:
DropShadowEffect:阴影特效BlurEffect:模糊特效
2. DropShadowEffect 阴影效果
给按钮加阴影非常简单:
<Button Content="按钮1" Width="100" Height="50"> <Button.Effect> <DropShadowEffect ShadowDepth="10" BlurRadius="20" Color="Gray" Direction="-45" Opacity="1"/> </Button.Effect> </Button>常用属性如下:
| 属性 | 作用 |
|---|---|
Color | 阴影颜色 |
ShadowDepth | 阴影偏移距离 |
Direction | 阴影方向 |
BlurRadius | 模糊半径 |
Opacity | 透明度 |
在实际项目中,这个特效非常适合做:
卡片悬浮感
按钮立体感
弹窗层级区分
3. BlurEffect 模糊效果
模糊效果常用于:
毛玻璃背景
聚焦突出
鼠标交互反馈
一个简单示例:
<Ellipse Width="120" Height="120" Fill="SkyBlue"> <Ellipse.Effect> <BlurEffect Radius="8"/> </Ellipse.Effect> </Ellipse>如果想做动态模糊,可以在代码里修改Radius:
private void grid_MouseMove(object sender, MouseEventArgs e) { Point mousePoint = e.GetPosition(this); Point centerPoint = new Point(ActualWidth / 2, ActualHeight / 2); double distance = Math.Sqrt( Math.Pow(mousePoint.X - centerPoint.X, 2) + Math.Pow(mousePoint.Y - centerPoint.Y, 2)); effect.Radius = distance / 5; }这样鼠标越远,模糊越明显,交互感会非常直观。
五、WPF 路由事件
1. 什么是路由事件
WPF 的界面本质上是一棵元素树。 比如下面这段结构:
<Window> <Border> <Canvas> <Button/> <Button/> </Canvas> </Border> </Window>
当按钮触发事件时,这个事件并不一定只在按钮自己身上结束,而是可能沿着整棵树传播。 这种“会沿元素树传播”的事件,就叫做路由事件。
WPF 中常见的路由策略有 3 种:
Tunnel:隧道事件,从根节点到事件源,常见前缀是PreviewBubble:冒泡事件,从事件源往父级一路传播Direct:直接事件,只在事件源本身触发
2. 隧道事件和冒泡事件怎么理解
如果点击按钮:
隧道事件路线:
Window -> Border -> Canvas -> Button冒泡事件路线:
Button -> Canvas -> Border -> Window
看一个隧道事件示例:
<Window PreviewMouseUp="Window_PreviewMouseUp"> <Border PreviewMouseUp="Border_PreviewMouseUp"> <Canvas PreviewMouseUp="Canvas_PreviewMouseUp"> <Button PreviewMouseUp="Button_PreviewMouseUp" Content="确定"/> </Canvas> </Border> </Window>如果点击按钮,输出顺序会是:
Window对象的隧道事件PreviewMouseUp被触发 Border对象的隧道事件PreviewMouseUp被触发 Canvas对象的隧道事件PreviewMouseUp被触发 Button确定按钮的隧道事件PreviewMouseUp被触发
再看冒泡事件:
<Window MouseUp="Window_MouseUp"> <Border MouseUp="Border_MouseUp" Background="Transparent"> <Canvas MouseUp="Canvas_MouseUp" Background="Transparent"> <Button MouseUp="Button_MouseUp" Content="确定"/> </Canvas> </Border> </Window>这里有一个很容易忽略的细节: 像Canvas、Border这种控件,如果没有背景色,哪怕是透明色,也可能收不到鼠标事件。
3. 自定义路由事件实战
除了使用系统自带的路由事件,我们也可以注册自己的路由事件。
比如给Widget自定义一个“销售完成事件”:
public static readonly RoutedEvent CompletedEvent = EventManager.RegisterRoutedEvent( "CompletedEvent", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Widget)); public event RoutedEventHandler Completed { add { AddHandler(CompletedEvent, value); } remove { RemoveHandler(CompletedEvent, value); } } private void RaiseRoutedEvent() { RoutedEventArgs args = new RoutedEventArgs(CompletedEvent, this); RaiseEvent(args); }再结合依赖属性回调做业务判断:
public double Target { get { return (double)GetValue(TargetProperty); } set { SetValue(TargetProperty, value); } } public static readonly DependencyProperty TargetProperty = DependencyProperty.Register( "Target", typeof(double), typeof(Widget), new PropertyMetadata(0.0)); public double Value { get { return (double)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( "Value", typeof(double), typeof(Widget), new PropertyMetadata(0.0, OnValuePropertyChanged)); private static void OnValuePropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is Widget control && e.NewValue is double value) { if (value >= control.Target && control.Target != 0) { control.RaiseRoutedEvent(); } } }前端直接订阅:
<local:Widget Value="{Binding ElementName=slider, Path=Value}" Target="1000000" Title="第四季度华南市场总销售额统计" Completed="Widget_Completed"/>后台处理:
private void Widget_Completed(object sender, RoutedEventArgs e) { Widget widget = sender as Widget; listBox.Items.Insert(0, $"完成目标销售额:{widget.Value}"); }这个例子把两个知识点串起来了:
用依赖属性回调监听值变化
用路由事件向外广播业务完成状态
在实际项目里,这种设计非常适合做:
自定义控件事件通知
业务状态上报
父容器统一监听子控件行为
六、总结
WPF 的很多“高级能力”并不是孤立存在的,而是互相配合的:
依赖属性负责让属性具备绑定、样式、动画和回调能力
附加属性负责把一个类型的能力扩展到另一个控件身上
Transform 负责视觉变换
Effect 负责视觉特效
路由事件负责事件传播和控件通信
如果你只是写界面,可能觉得这些内容有点底层;但只要一旦开始做自定义控件、复杂交互、MVVM 架构,这些知识几乎都会变成必修课。
最后给大家一个学习建议:
先掌握依赖属性和附加属性,因为这两个是 WPF 属性系统的核心。
再练习 Transform 和 Effect,把界面“做出来”。
最后重点理解路由事件,把控件之间的通信“串起来”。
当你把这 5 个知识点真正吃透之后,WPF 里的很多“黑魔法”其实就不神秘了。
课后作业:
简单的后台绑定数据
需要注意绑定数据时对属性的定义与数据匹配问题
运行展示
具体代码:
XAML:
<Window x:Class="WpfApp8.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApp8" mc:Ignorable="d" Title="MainWindow" Height="450" Width="900"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock Text="{Binding Model.TimeText}" Background="SkyBlue" FontSize="24" Foreground="White" Margin="0 0 0 20"/> <StackPanel Grid.Row="1" Orientation="Horizontal"> <Border Background="CadetBlue" CornerRadius="4" Margin="5" Width="150" Height="200"> <StackPanel Margin="20"> <TextBlock Text="⏱" FontSize="20" Foreground="White"/> <TextBlock Text="汇总" FontSize="18" Foreground="White" Margin="0 10 0 20"/> <TextBlock Text="{Binding Model.TotalTasks}" FontSize="48" Foreground="White" FontWeight="Bold"/> </StackPanel> </Border> <Border Background="LightGreen" CornerRadius="4" Margin="5" Width="150" Height="200" > <StackPanel Margin="20"> <TextBlock Text="⏱" FontSize="20" Foreground="White"/> <TextBlock Text="已完成" FontSize="18" Foreground="White" Margin="0 10 0 20"/> <TextBlock Text="{Binding Model.CompletedTasks}" FontSize="48" Foreground="White" FontWeight="Bold"/> </StackPanel> </Border> <Border Background="BlueViolet" CornerRadius="4" Margin="5" Width="150" Height="200"> <StackPanel Margin="20"> <TextBlock Text="📈" FontSize="20" Foreground="White"/> <TextBlock Text="完成比例" FontSize="18" Foreground="White" Margin="0 10 0 20"/> <TextBlock Text="{Binding Model.CompletionRate}" FontSize="48" Foreground="White" FontWeight="Bold"/> </StackPanel> </Border> <Border Background="Orange" CornerRadius="4" Margin="5" Width="150" Height="200"> <StackPanel Margin="20"> <TextBlock Text="📋" FontSize="20" Foreground="White"/> <TextBlock Text="备忘录" FontSize="18" Foreground="White" Margin="0 10 0 20"/> <TextBlock Text="{Binding Model.MemoCount}" FontSize="48" Foreground="White" FontWeight="Bold"/> </StackPanel> </Border> </StackPanel> </Grid> </Window>Model.cs
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; namespace WpfApp8 { public class Model { private string _timeText; private int _totalTasks; private int _completedTasks; private string _completionRate; private int _memoCount; public string TimeText { get => _timeText; set => _timeText = value; } public int TotalTasks { get => _totalTasks; set => _totalTasks = value; } public int CompletedTasks { get => _completedTasks; set => _completedTasks = value; } public string CompletionRate { get => _completionRate; set => _completionRate = value; } public int MemoCount { get => _memoCount; set => _memoCount = value; } } }TaskView.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.ComponentModel; using System.Runtime.CompilerServices; namespace WpfApp8 { public class TaskView { public Model Model { get; set; } public TaskView() { Model = new Model(); Model.TimeText = $"你好,{DateTime.Now:yyyy年M月d日dddd}"; Model.TotalTasks = 27; Model.CompletedTasks = 24; Model.CompletionRate = "89%"; Model.MemoCount = 13; } } }MainWindow.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace WpfApp8 { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.DataContext = new TaskView(); } } }