WPF里的进度条(ProgressBar)大概是所有控件里最容易被低估的一个。拖进来,设个Value,填充条就动了;但真正到了实际项目里,你会发现它身上能翻车的点比想象中多得多:绑定不刷新、跨线程崩溃、样式丑到没法看、不确定状态怎么调都不动……这篇文章我会从最基础的属性讲起,一路聊到ControlTemplate、VisualState、数据绑定、动画和自定义样式,最后再用一个批量文件处理的示例把这些内容串起来。适合刚接触WPF的开发者,也适合写过几个项目但没认真啃过进度条模板的人。
1. 基础用法:进度条看似简单,其实有两个隐藏雷区
1.1 Minimum、Maximum、Value 不是随便填的
ProgressBar 脱离不了这三个基础属性。默认情况下 Minimum 是 0,Maximum 是 100,Value 是 0。你只需要在代码里把 Value 改大,填充条就会往前走,这确实是它最基础的工作方式。
但很多初学者会忽略一个重要规则:Value 必须在 Minimum 和 Maximum 之间。如果你给 Value 赋一个超过 Maximum 的值,进度条的行为会变得不可控,某些版本里甚至会直接抛出异常,或者填充条超出边界显示得很奇怪。我建议在代码里显式做一次钳制:
csharp复制var percent = Math.Max(0, Math.Min(100, currentPercent));
myProgressBar.Value = percent;
另一个容易踩的点是:如果你把 Minimum 设成 10、Maximum 设成 20,那么 Value 只能在 10 到 20 之间取值。别小看这个,当你后面做数据绑定时,如果后台算出来的百分比是 0 到 100,但控件范围被改成了别的值,进度条就可能一步都不走。
所以我的习惯是:如果某个 ProgressBar 显示的是“固定总量下的完成度”,我会把 Minimum 固定为 0,Maximum 固定为 100,然后只改 Value;如果进度条要显示的是一个真实的数量范围,比如“已下载 0KB 到 500MB”,我会把 Maximum 设为 500,并把 Value 设成等效的 MB 数。方法本身没有优劣,关键是“口径一致”。
1.2 IsIndeterminate:进度未知时不要硬套百分比
有些场景你压根无法计算百分比。比如正在连接远程服务、扫描一堆不知道数量的文件、等待另一个模块返回结果。这时候你如果把进度条当成确定进度来用,就得不断猜一个不准确的百分比,看起来非常业余。
正确做法是把 IsIndeterminate 设为 true:
xml复制<ProgressBar IsIndeterminate="True" Width="200" Height="16" />
这种模式下控件内部会显示一条来回滑动的方块,表达“正在工作,但不知道进度”。这个属性用起来很简单,但有一点要提醒:它和 Value 是有互斥关系的。当 IsIndeterminate 为 true 时,Value 的变化不会反映在界面上。很多人遇到“进度条完全不动”的问题,最后发现就是忘了把 IsIndeterminate 改回 false。
还有一个细节:不确定状态下视觉效果是依赖模板里的动画实现的。如果你后面自定义了样式模板,却完全没有处理 Indeterminate 的状态动画,那即使 IsIndeterminate 为 true,进度条也可能纹丝不动。这个坑我在后文讲 VisualState 时会单独展开。
1.3 跨线程更新:为什么界面会卡死或者不刷新
这是所有 WPF 新手都会撞上的问题。你在后台线程里跑一个循环,每到循环末尾就写一句:
csharp复制myProgressBar.Value = i;
然后运行起来,界面要么直接抛一个异常,提示“调用线程无法访问此对象,因为另一个线程拥有该对象”,要么程序不报错但进度条像被冻结了一样。
原因很简单:WPF 的 UI 元素不是线程安全的,更新界面必须回到 UI 线程。后台线程改 Value 实际上是在“隔壁房间”动 UI 元素,WPF 拒绝接受。
早期常见的解决方案是 Dispatcher:
csharp复制Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
myProgressBar.Value = i;
}));
这样确实能跑,但在高频循环里,每循环一次就往 UI 线程丢一个委托,界面很容易被消息队列塞满,反而更卡。更现代的做法是用 IProgress<T>,或者用 async/await 配合上下文切换。我在下一章会详细讲绑定和上报,那才是值得长期使用的姿势。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据绑定与进度上报:MVVM 场景下的标准姿势
2.1 在 ViewModel 里暴露一个 ProgressValue
如果你还在写“把 ProgressBar 拖到窗口上,然后直接在代码后台操作它的 Value”,在有业务逻辑的项目里很快会乱套。合理做法是把它作为 ViewModel 的一个属性来绑定。
ViewModel 需要实现 INotifyPropertyChanged,最简单的手写版本是这样:
csharp复制public class MainViewModel : INotifyPropertyChanged
{
private double _progressValue;
public double ProgressValue
{
get => _progressValue;
set
{
_progressValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProgressValue)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
XAML 里只需要写:
xml复制<ProgressBar Minimum="0" Maximum="100" Value="{Binding ProgressValue}" />
这样你在任何地方修改 ViewModel 的 ProgressValue,界面上都会同步。很多人绑定后发现界面不动,99% 是忘了实现 INotifyPropertyChanged,或者在 setter 里没有触发 PropertyChanged,还有一些是把 DataContext 设在了一个全局但没传给控件。排查顺序建议是:先看 DataContext 是否绑定正确,再看属性名是否拼错,最后在 setter 里打一个断点,看后台有没有调用。
2.2 用 IProgress 彻底告别 Dispatcher 焦虑
我前文说 Dispatcher.BeginInvoke 会显得很啰嗦,主要是因为它把“业务计算”和“界面线程调度”耦合在一起了。更好的办法是利用 Progress<T>。
Progress<T> 在构造函数里会捕获当前同步上下文。如果你在 UI 线程里创建它,那么之后调用它的 Report 方法时,回调会自动被投递回 UI 线程执行。也就是说,你只需要在后台任务里调用 Report,完全不需要显式使用 Dispatcher。
看一个典型例子:
csharp复制public async Task StartAsync()
{
var progress = new Progress<double>(value =>
{
ProgressValue = value;
});
await Task.Run(() => SimulateWork(progress));
}
private void SimulateWork(IProgress<double> progress)
{
for (int i = 0; i <= 100; i++)
{
Thread.Sleep(50);
progress.Report(i);
}
}
这段代码里,progess.Report(i) 是在后台线程调用的,但回调中的 ProgressValue = value 一定会回到 UI 线程执行,于是进度条自然刷新。这个方案比 Dispatcher 更干净,而且不容易出现跨线程访问的异常。我后来做项目基本都是这个套路。
2.3 更新频率与节流,别让 UI 线程被刷爆
后台循环如果每 1 毫秒就 Report 一次,UI 线程会非常痛苦。比如你在复制文件流的时候,底层可能几百毫秒就回调一次进度,这频率其实还好;但如果你在处理几十万条数据,每个数据项都 Report,那 UI 线程必定卡顿。
处理办法有两个方向。第一个方向是“按比例节流”:只在百分比的整数位发生变化时才上报。
csharp复制private void SimulateWork(IProgress<double> progress)
{
int lastReport = -1;
for (int i = 0; i <= 1000; i++)
{
Thread.Sleep(10);
double percent = i / 1000.0 * 100;
if ((int)percent != lastReport)
{
lastReport = (int)percent;
progress.Report(percent);
}
}
}
第二个方向是“按时间节流”:记录上次上报时间,间隔超过 100 毫秒才再上报一次。根据我的经验,UI 上的进度条每秒钟刷新 10 到 20 次就已经非常顺畅了,再高的频率根本看不出来,反而白白消耗 CPU。做大数据量循环时,请养成节流习惯。
3. 自定义样式:从控制模板里重新认识 ProgressBar
3.1 默认模板到底由哪几块拼出来的
想自定义进度条样式,第一步得知道 ProgressBar 的默认模板里有什么。WPF 控件的“皮肤”不是画在一个 Draw 方法里的,而是通过 ControlTemplate 重新组装出来的。ProgressBar 最核心的部分有两个:
PART_Track:进度条底部的轨道,用来承载背景。PART_Indicator:实际表示进度的填充块。
ProgressBar 的内部逻辑会通过这个名字去找这两个元素,然后根据 Value、Minimum、Maximum 去设置 PART_Indicator 的宽度或高度。如果你的自定义模板里没有这两个名字,那控件即使能显示背景,填充条也不会动。这是我见过最多的自定义样式翻车原因。
一个最简单的自定义模板长这样:
xml复制<Style TargetType="ProgressBar" x:Key="RoundedProgressBarStyle">
<Setter Property="Height" Value="24"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ProgressBar">
<Grid>
<Border x:Name="PART_Track"
Background="#FFE3E3E3"
CornerRadius="12"/>
<Border x:Name="PART_Indicator"
HorizontalAlignment="Left"
Background="#FF2D8CF0"
CornerRadius="12"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
这里我把轨道和填充块都设置成了圆角,高度 24,视觉上就是一个胶囊形状的进度条。由于 ProgressBar 内部逻辑会调整 PART_Indicator 的宽度,所以填充条从左边开始向右伸展,整体效果非常接近现代客户端的风格。
3.2 做一个圆角渐变并且带百分比文字的自定义样式
光有圆角还不够,很多设计稿里要求填充条带渐变色,同时中心位置显示当前百分比文字。渐变很容易,把 PART_Indicator 的 Background 设成一个 LinearGradientBrush 就行。百分比文字则需要额外放一个 TextBlock,并且把它绑定到当前 Value 上。
麻烦点在于:TextBlock 拿到的是 Value,而你要显示的是百分比。如果 Maximum 固定是 100,那 Value 本身就是百分比;如果 Maximum 是动态的,就必须把 Value 和 Maximum 一起参与计算。最稳妥的做法是写一个多值转换器。
转换器代码:
csharp复制public class ProgressToPercentConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length < 2) return "0%";
if (values[0] is double value && values[1] is double maximum)
{
double percent = maximum <= 0 ? 0 : value * 100.0 / maximum;
percent = Math.Clamp(percent, 0, 100);
return $"{percent:F0}%";
}
return "0%";
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
XAML 里通过 MultiBinding 把两个属性传进去:
xml复制<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="12"
Foreground="White">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ProgressToPercentConverter}">
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Value"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Maximum"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
把上面的 TextBlock 加到 3.1 的模板里,就得到了一个带渐变、带圆角、带百分比文字的进度条。这里有一个视觉细节:百分比文字如果一直是纯白色,在填充条宽度很窄的时候可能盖不住或者看不清。你可以给文字加阴影,或者把文字放在一个半透明的深色背景上,具体根据设计稿调整。我的经验是:文字不要依赖填充块的宽度,让它始终居中显示在整根进度条上,可读性最好。
3.3 不确定状态(IsIndeterminate)的自定义视觉状态
自定义模板最容易遗漏的就是 VisualState。默认模板里包含了很多视觉状态,其中和进度条关系最大的是 Determinate 和 Indeterminate。
如果你在自定义模板里完全不写 VisualStateManager,那么当 IsIndeterminate 为 true 时,控件状态切换不到“不确定”的视觉分支,于是看起来还是静止的。想要让自定义模板支持不确定状态,你需要主动添加状态组:
xml复制<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Determinate"/>
<VisualState Name="Indeterminate">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="PART_Indicator"
Storyboard.TargetProperty="Opacity"
To="0.4"
AutoReverse="True"
RepeatBehavior="Forever"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
上面的写法让不确定状态下的填充块在半透明和不透明之间来回变化,视觉上明显区别于确定状态。你也可以让 PART_Indicator 的宽度或位移做动画,效果更丰富。但要注意:不确定状态的核心是“让用户知道事情在进行中”,动画幅度不要太大,否则容易让人烦躁。
3.4 让填充过程平滑动画的两种实现
默认情况下,Value 从 10 变成 90,填充条会直接跳过去,没有过渡。在实际项目里,这种“瞬移”在视觉上很生硬。如果希望填充过程平滑,有两个方向。
方向一是做依赖属性动画。继承 ProgressBar,在 Value 变化时用 DoubleAnimation 让它从旧值滑到新值:
csharp复制public class SmoothProgressBar : ProgressBar
{
protected override void OnValueChanged(double oldValue, double newValue)
{
var animation = new DoubleAnimation
{
From = oldValue,
To = newValue,
Duration = TimeSpan.FromMilliseconds(300)
};
BeginAnimation(ValueProperty, animation);
}
}
这个类可以直接替换原来的 ProgressBar,绑定也不受影响。需要注意的是:动画会让 Value 的更新变得“迟钝”,如果你后台每秒上报几十次进度,动画反而会增加卡顿感。所以它只适合低频进度更新,比如每个文件复制结束后更新一次。
方向二是在模板里做动画,这种方式灵活但复杂。比如给 PART_Indicator 的 RenderTransform 加上 ScaleTransform,然后通过更新 ScaleX 来控制填充宽度。这种做法的好处是动画逻辑和控件逻辑解耦,但坏处是绑定关系会绕,维护成本高。我个人的选择是:简单的平滑效果用派生类,复杂交互才去动模板里的动画。
4. 进阶玩法:多段进度、环形进度和自定义控件
4.1 叠加两个进度条实现“已下载+正在处理”效果
有时候业务需要在一个区域同时显示两层进度。比如一个下载工具,外层显示整个任务列表的总体进度,内层显示当前正在下载的这个文件的进度。如果产品要求“两层进度同时显示”,最粗暴的做法是在同一个 Grid 里叠两个 ProgressBar。
上层的进度条背景设为透明,只显示填充条;下层的进度条显示轨道。两层绑定不同的 ViewModel 属性:
xml复制<Grid>
<ProgressBar Value="{Binding OverallProgress}" />
<ProgressBar Value="{Binding CurrentFileProgress}"
Background="Transparent"
Foreground="#FFFFA500" />
</Grid>
不过这种叠加方式高度依赖样式,稍不小心就会让轨道漏出来。更可靠的方案是封装一个用户控件,对外暴露两个依赖属性:OverallValue 和 CurrentValue。在控件内部再放两个 ProgressBar,一个作为背景进度,一个作为前景进度。业务层只需要绑定这两个属性,视觉逻辑全部封装在控件里。这类场景我不太建议写一堆样式去硬凑,用户控件的维护成本更低。
4.2 环形进度条其实没那么难
环形进度条也可以做到,只是比直线进度条绕一些。WPF 没有现成的环形 ProgressBar,常见的做法是用 Arc 或者 Path 画圆弧,然后通过 StrokeDashArray 来控制显示比例。
核心思路是:一个圆的周长是固定的,你只要让实线部分的长度等于“周长 × 百分比”,就得到了一个环形进度条。XAML 里大致是这样:
xml复制<Path StrokeThickness="8" Stroke="Gray">
<Path.Data>
<EllipseGeometry Center="60,60" RadiusX="50" RadiusY="50"/>
</Path.Data>
</Path>
<Path StrokeThickness="8" Stroke="#FF2D8CF0" StrokeStartLineCap="Round">
<Path.Data>
<EllipseGeometry Center="60,60" RadiusX="50" RadiusY="50"/>
</Path.Data>
</Path>
第二层 Path 的 StrokeDashArray 通过一个转换器,把百分比转换成“周长 × 百分比”的实线长度。比如圆半径 50,周长就是 2 × π × 50 ≈ 314,那么 40% 就是 125.6。这里有一个很容易忽略的问题:需要把 Path 的角度方向调好,并且考虑起点要从 12 点方向开始而不是 3 点方向。实际项目里我会在转换器里加上一个 90 度的偏移,保证填充从正上方开始。
如果你需要更复杂的环形交互,建议直接搜一下开源的 Arc 控件,自己写也能写但边际成本偏高。从学习角度,自己动手写一遍会更理解依赖属性的机制。
4.3 什么时候值得写一个自定义控件,而不是堆样式
写 WPF 时间长了,你会遇到一个判断问题:这个需求该用 Style、UserControl 还是 CustomControl?
我的经验是:如果只是改颜色、圆角、模板,用 Style 就够了;如果要做复合结构,比如两层进度条、带取消按钮的进度条,用 UserControl 更合适;如果要在内部重写逻辑,比如平滑动画、特殊布局计算,才值得用继承 ProgressBar 的自定义控件。
不要一上来就继承控件。WPF 的样式系统已经解决掉 80% 的视觉问题,剩下 20% 才是真正的逻辑扩展。自定义控件一旦写出去,就要考虑模板绑定、默认样式、依赖属性回调等一系列问题,而且调试成本明显上升。很多新手在进度条上花了一整天做“自定义控件”,最后发现其实一个 ControlTemplate 就能解决,这是很可惜的。
5. 实战:做一个批量文件处理的进度封装
5.1 需求定义与进度计算口径
前面把知识拆开讲了,这一章我用一个真实的模拟项目把内容串起来。假设你要做一个批量文件处理工具,界面里有一个进度条,要能显示“当前已经处理到第几个文件”,并且处理完整个列表后自动归零。
第一件事不是写代码,而是定进度口径。有两种常见算法:
- 按文件个数算:已处理文件数 / 总文件数。优点是简单,缺点是每个文件大小差异大的时候,进度跳跃感明显。
- 按文件字节数算:已处理字节数 / 总字节数。优点是比较符合用户的体感,缺点是需要额外统计总大小和已处理大小,逻辑更复杂。
我建议尽量用字节数口径,特别是文件处理任务。因为用户看到“处理了一个大文件之后进度猛涨,再处理一堆小文件进度几乎不动”会非常困惑。按字节数算的话,大文件占的权重自然就大,进度也更平滑。
5.2 完整代码:任务执行、进度上报与界面绑定
ViewModel 里需要一个总进度属性,同时还要一个状态文案。结构类似这样:
csharp复制public class FileProcessViewModel : INotifyPropertyChanged
{
private double _progressValue;
public double ProgressValue
{
get => _progressValue;
set
{
_progressValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProgressValue)));
}
}
public async Task RunAsync()
{
var files = GetFiles();
long totalBytes = files.Sum(f => f.Length);
long processedBytes = 0;
var progress = new Progress<double>(v =>
{
ProgressValue = v;
});
await Task.Run(() =>
{
foreach (var file in files)
{
ProcessFile(file);
processedBytes += file.Length;
double percent = totalBytes == 0
? 100
: processedBytes * 100.0 / totalBytes;
progress.Report(percent);
}
});
ProgressValue = 100;
}
}
XAML 只写一句绑定:
xml复制<ProgressBar Height="20"
Value="{Binding ProgressValue, Mode=OneWay}" />
这段代码能跑,但还有一个实际问题:如果你在 UI 线程里调用 RunAsync,Task.Run 里的循环就不会阻塞 UI,所以界面能正常刷新。如果你用的是老式的 BackgroundWorker,需要自己处理 ReportProgress 的事件,效果一样。
5.3 再加一个取消和剩余时间提示
生产环境下的批量任务不能没有取消功能。加上 CancellationTokenSource 后,循环里每次检查 token 是否取消,一旦取消就退出循环,并把进度停在当前值。
剩余时间提示更有意思。你可以用一个 Stopwatch 记录开始时间,再根据当前百分比估算总耗时,剩余时间 = 已用时间 / 百分比 - 已用时间。公式很简单:
code复制剩余秒数 = 已用秒数 / (当前百分比 / 100) - 已用秒数
这个公式在百分比很小的时候会非常不稳定,比如刚开始处理,0.5% 时估算剩余时间可能跳得吓人。我的处理是:加一个最小分母保护,只有当百分比超过 3% 时才显示估算时间,否则只显示“正在计算剩余时间……”。这类小细节虽然不影响功能,但很影响用户体验。
界面上的状态文案可以绑定到另一个属性:
csharp复制public string StatusText { get; set; }
每处理完一个文件就更新一次,比如“正在处理 3/20,已完成 15.5%”。这样用户不仅能看进度条,还能知道他到底卡在哪个文件上。
6. 常见问题排查与避坑速查表
6.1 进度条就是不走
遇到“进度条完全不动”的时候,我一般按这个顺序排查:
先确认 IsIndeterminate。如果是 true,那 Value 怎么变都没用,这是最常见的原因。第二步检查 Maximum 和 Minimum,确保 Value 落在有效区间里。第三步检查绑定,看 ViewModel 的 PropertyChanged 事件有没有触发。第四步检查线程,如果是在后台线程直接改 Value,界面不一定不报错,但可能一直保持旧值。最后检查模板里有没有 PART_Indicator,如果自定义模板忘了这个名字,进度条会变成一张静态图。
6.2 进度条乱跳或者回退
出现乱跳,常见原因是多个任务在同时更新同一个 ProgressValue,后完成的覆盖先完成的,导致进度条倒着走。解决方式是合并进度,或者给不同的子任务分配不同的进度条。如果进度条“回退”而不是倒着走,那大概率是绑定的属性被别的地方重新赋值了。你可以在 setter 里加一个限制:只允许增长,不允许下降:
csharp复制if (value < _progressValue) return;
不过这个限制只适用于单向进度场景;如果需要展示“重新加载”的反向进度,就不适合这样写。
6.3 自定义模板后不确定状态失效
前文说过,这是 VisualState 缺失导致的。确认模板里有没有添加 CommonStates 状态组,并且状态组里是否定义了 Indeterminate 状态对应的动画。注意 VisualState 的名称必须匹配 ProgressBar 内部调用的状态名,Determinate 和 Indeterminate 都不能写成别的。
还有一个细节:VisualStateManager 在模板中是写在 ControlTemplate 的根元素内部的,不是写在 Style 的 Setter 里。位置写错了也一样不生效。
6.4 性能优化:大数据量下的刷新策略
最后聊一下大数据量场景。假设你要显示“处理 100 万条数据”的进度,如果每条数据都上报一次,ProgressBar 的刷新频率会猛增,UI 线程被各种属性变更通知淹没。我实测过的经验是:把上报频率控制在每隔 10 毫秒到 50 毫秒上报一次,或者只在百分比变化超过 0.1% 时上报,界面都不会卡。如果还觉得卡,可以把 ProgressBar 的内容放入单独的容器,避免整页布局反复重排。
下面这张速查表是我自己平时排查问题用的:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 进度条完全不动 | IsIndeterminate 为 true 或模板缺 PART_Indicator | 改为 determinate 或补上模板元素 |
| 绑定值变了但界面不变 | 没触发 PropertyChanged 或 DataContext 错误 | 在 setter 打断点,检查绑定 |
| 跨线程赋值报错 | 后台线程直接操作 UI 元素 | 使用 IProgress<T> 或 Dispatcher |
| 进度条突然回退 | 多个任务写同一个进度值 | 合并进度或改用多个进度条 |
| 自定义样式后填充条不显示 | 模板里少了 PART_Track/PART_Indicator | 补上指定名称的模板元素 |
| 不确定状态静止 | 缺少 VisualState 动画 | 添加 CommonStates 状态组 |
| 界面卡顿 | 上报频率过高 | 按百分比或时间节流 |
写到这里,我把一个基础的 WPF 进度条从“拖个控件填个值”扩展到了“可绑定的业务组件、自绘模板、平滑动画、不确定状态、实战封装”的完整链路。最后再分享几个我自己的习惯:进度条的颜色和形状永远优先走样式模板,不要在主界面代码里硬写颜色;任何耗时的后台任务都用 IProgress 来上报进度,不要在业务代码里塞 Dispatcher;所有自定义模板保留 PART_Track 和 PART_Indicator 这两个名字,给未来维护留余地。如果以后遇到特别复杂的进度交互,我多半会选择先画一个用户控件草图,再一步步添加依赖属性,而不是一上来就大改默认模板。这套思路帮我避开了很多进度条相关的坑,希望对你也有用。
