news 2026/4/27 18:15:57

WPF 进阶特性详解:依赖属性、附加属性、Transform、Effect 与路由事件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WPF 进阶特性详解:依赖属性、附加属性、Transform、Effect 与路由事件

大家在学习 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 步:

  1. 定义DependencyProperty静态字段

  2. 使用DependencyProperty.Register()完成注册

  3. 用普通属性外壳包装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 支持绑定

PasswordBoxPassword并不是依赖属性,所以不能像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>

常用属性:

  • ScaleX

  • ScaleY

  • CenterX

  • CenterY

2.3 SkewTransform 倾斜
<Border Width="120" Height="120" Background="LightBlue"> <Border.RenderTransform> <SkewTransform AngleX="20" AngleY="10" CenterX="60" CenterY="60"/> </Border.RenderTransform> </Border>

常用属性:

  • AngleX

  • AngleY

  • CenterX

  • CenterY

2.4 TranslateTransform 平移
<Border Width="120" Height="120" Background="LightGreen"> <Border.RenderTransform> <TranslateTransform X="80" Y="30"/> </Border.RenderTransform> </Border>

常用属性:

  • X

  • Y

3. 实战:TransformGroup 做图片查看器

如果我们想同时支持图片拖拽和平滑缩放,就不能只靠单一变换,而是要把ScaleTransformTranslateTransform组合起来。

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:隧道事件,从根节点到事件源,常见前缀是Preview

  • Bubble:冒泡事件,从事件源往父级一路传播

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

这里有一个很容易忽略的细节: 像CanvasBorder这种控件,如果没有背景色,哪怕是透明色,也可能收不到鼠标事件。

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 架构,这些知识几乎都会变成必修课。

最后给大家一个学习建议:

  1. 先掌握依赖属性和附加属性,因为这两个是 WPF 属性系统的核心。

  2. 再练习 Transform 和 Effect,把界面“做出来”。

  3. 最后重点理解路由事件,把控件之间的通信“串起来”。

当你把这 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(); } } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/27 18:14:13

AI学习路线图:从机器学习基础到深度学习实战的完整指南

1. 项目概述&#xff1a;一个面向实践者的AI学习资源库最近几年&#xff0c;AI领域的热度居高不下&#xff0c;从大语言模型到图像生成&#xff0c;各种新概念、新工具层出不穷。对于很多想入行或者想提升技能的开发者、学生甚至业务人员来说&#xff0c;一个最直接的问题就是&…

作者头像 李华
网站建设 2026/4/27 18:11:08

自进化学习框架Dr. Zero的设计与优化实践

1. 自进化学习框架Dr. Zero的核心设计自进化学习&#xff08;Self-Evolution Learning&#xff09;是当前AI领域的前沿方向&#xff0c;其核心挑战在于如何让模型在缺乏标注数据的情况下持续提升性能。Dr. Zero框架通过创新的交替优化机制解决了这一难题。我在实际部署中发现&a…

作者头像 李华
网站建设 2026/4/27 18:11:07

产品经理AI工具productskills实战:从机会发现到PRD落地的全流程指南

1. 产品经理的AI副驾&#xff1a;productskills深度体验与实战指南最近在探索如何将AI更深度地融入产品工作流时&#xff0c;我遇到了一个名为productskills的工具。作为一名在产品一线摸爬滚打了十年的老兵&#xff0c;我对任何号称能提升效率的工具都抱有审慎的好奇心。produ…

作者头像 李华
网站建设 2026/4/27 18:09:21

如何将DeepSeek V4 1M上下文 接入你的Claude Code

文章信息预计字数&#xff1a;3200 字 | 阅读时间&#xff1a;8 分钟 | 难度等级&#xff1a;⭐⭐ 入门 核心价值&#xff1a;解锁 Claude Code 在国内的零门槛使用方案 我一直在用 Claude Code&#xff0c;好用到离谱。 而是 Claude Code 不是我吹&#xff0c;才是龙虾们真正的…

作者头像 李华
网站建设 2026/4/27 18:06:31

c语言字符数组与字符串的使用详解

1、字符数组的定义与初始化 字符数组的初始化&#xff0c;最容易理解的方式就是逐个字符赋给数组中各元素。 char str[10]{ I, ,a,m, ,‘h,a,p,p,y}; 即把10个字符分别赋给str[0]到str[9]10个元素 如果花括号中提供的字符个数大于数组长度&#xff0c;则按语法错误处理&#xf…

作者头像 李华