很多人第一次在 WPF 里做“带水波动画的圆形进度条”时,卡点往往不在单个控件,而是在动画、裁剪和数据绑定怎么配合。本文按实现路径把这个控件拆开说明:先看它由哪些视觉元素组成,再看 `Storyboard` 如何驱动波浪,最后梳理依赖属性与常见细节问题,方便你判断这套方案是否适合直接落到自己的项目里。
这个圆形水波纹进度条要解决什么问题
本文要实现的是一个典型的圆形进度条:外层是圆形边框,中间显示百分比,内部则用波浪和填充高度来表达当前进度。
原始案例来自一次 WPF 复现实践。作者在看到 Web 前端版本后,尝试用 WPF 做出同类效果,并参考了《WPF实现三星手机充电界面》的思路,最终整理出一个可用的简化版本。
虽然样式不算复杂,但它已经覆盖了这类控件最核心的几个点:
- 圆形外观而不是普通矩形进度条;
- 内部波浪动画持续播放;
- 文字、颜色、边框、进度高度可动态绑定;
- 可封装成独立的 `UserControl` 使用。
XAML 结构:圆形外观、波浪区域和文字如何拼起来
实现这类控件时,重点不是单独某个元素,而是布局层级的组合。原始代码里,控件本体用 `UserControl` 承载,内部通过资源、容器和绑定属性把几个视觉层拼到一起。
核心代码如下:
<UserControl x:Class="Wa veProgress.UserControl.Wa veProgressControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wa veProgress.UserControl"
mc:Ignorable="d"
Height="150" Width="150" x:Name="wa ve_control">
<!-- Storyboard 定义 -->
<UserControl.Resources>
<Storyboard x:Key="WaterStoryboard">
<PointAnimation ... />
<PointAnimation ... />
</Storyboard>
</UserControl.Resources>
<!-- 控件布局与属性绑定 -->
<Grid>
...
<StackPanel>
<Path Fill="{Binding Wa vePorgressBarColor}">
...
</Path>
<Rectangle Height="{Binding Wa veProgressHeight}" Fill="{Binding Wa vePorgressBarColor}"/>
</StackPanel>
<Ellipse Stroke="{Binding Wa veBorderBrush}" Fill="Transparent" StrokeThickness="{Binding Wa veBorderThickness}"/>
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="22" Foreground="{Binding TextColor}">
<Run Text="{Binding DisPlayValue}"></Run>
<Run Text="%"></Run>
</TextBlock>
</Grid>
</UserControl>
从结构上看,这个控件至少有三层
- 波浪层:由 `Path` 绘制水波形状,并用 `Rectangle` 补足下方填充区域;
- 边框层:通过 `Ellipse` 画出圆形边界;
- 文本层:用 `TextBlock` 在中心显示 `DisPlayValue` 和 `%`。
其中 `Path` 和 `Rectangle` 使用同一个 `Fill` 绑定到 `Wa vePorgressBarColor`,这样既能保持波浪顶部和水体主体颜色一致,也便于统一换肤。
圆形效果的关键不只是画圆,还要做裁剪
这类效果的关键难点在于:波浪本身通常是横向展开的图形,但最终显示区域必须被限制在圆形内部。这里原文特别提到,实际做法是利用 EllipseGeometry 配合 Clip 属性,对控件内容进行圆形剪裁。
也就是说,波浪动画本身可以按普通路径去绘制,但显示时只保留圆形区域内的部分。这样做的好处是实现成本相对低,也更符合 WPF 现有图形系统的用法。
水波动画是怎么动起来的
在动画部分,原始实现使用了 `Storyboard`,并在资源中声明了 `WaterStoryboard`。其中最关键的是两个 `PointAnimation`,它们通常用于驱动路径控制点的位置变化,从而让贝塞尔曲线形成“上下起伏、左右流动”的波浪效果。
原文没有展开完整动画参数,但结合给出的结构,可以明确几个实现重点:
- `Storyboard` 放在 `UserControl.Resources` 中,便于统一管理和调用;
- 波浪形状并不是一张图片,而是由路径点动态变化生成;
- 动画的本质是不断修改曲线控制点,让水面保持流动感。
这也是为什么原文在总结里特别提到“学到了如何使用贝塞尔曲线绘制复杂的形状以及为其添加动画效果”。对于 WPF 来说,这种做法比逐帧贴图更贴近框架自身的矢量绘制能力。
后台逻辑:为什么要用依赖属性
如果这个圆形水波纹进度条只做一次静态演示,直接写死颜色和数值也能跑起来。但只要你希望它成为可复用控件,依赖属性基本就是绕不过去的一步。
原文给出的后台代码如下:
public partial class Wa veProgressControl : UserControl
{
public Wa veProgressControl()
{
InitializeComponent();
this.DataContext = this;
}
// 定义依赖属性并实现相应的get/set方法
public static readonly DependencyProperty Wa veProgressBackgroundProperty = DependencyProperty.Register(...);
public SolidColorBrush Wa veProgressBackground { get; set; }
// 更多依赖属性...
}
`DataContext = this` 的作用
构造函数中的 this.DataContext = this;,目的是让 XAML 中的绑定能够直接访问当前控件实例上的属性,例如:
- `Wa veProgressBackground`
- `Wa veBorderBrush`
- `Wa veBorderThickness`
- `Wa vePorgressBarColor`
- `Wa veProgressHeight`
- `TextColor`
- `DisPlayValue`
这样一来,颜色、边框粗细、进度高度和文字内容都可以由外部动态设置,而不是写死在模板里。
依赖属性解决的是“可配置”和“可刷新”
原文明确提到,后台定义了多个依赖属性,用来支持 UI 元素动态更新。对于这种控件来说,依赖属性至少有两层价值:
- 便于外部绑定:可以直接从窗口、页面或 ViewModel 传入颜色和进度值;
- 便于样式复用:控件在不同主题或不同场景下,只改属性即可复用同一套 XAML 结构。
如果后续还要把尺寸做成可变、把动画速度做成可调,这一套依赖属性模式也可以继续扩展下去。
实现过程中的收获与已知问题
这次实现带来的知识点主要集中在三个方面:
- 使用贝塞尔曲线绘制复杂波浪形状;
- 使用 `Storyboard` 为路径控制点添加动画;
- 使用 `Clip` 做圆形图形裁剪。
同时,原文也提到一个很具体的显示问题:实现中曾出现一条不希望存在的白色线条。这个问题可以通过设置 StrokeThickness="0" 解决。
这类细节很常见。原因通常不是布局完全错误,而是路径或边框在默认描边状态下被渲染出来,叠加到填充区域之后,就会形成视觉上的细线。
这版实现目前的局限
原始文章最后也给出了一个现实判断:当前版本的大小还是固定的。也就是说,这个控件已经能够满足基本需求,但在可伸缩性和通用性上还有继续打磨的空间。
如果你准备把它用于正式项目,下一步通常会优先关注这几件事:
- 尺寸是否能跟随容器自适应;
- 波浪幅度和速度是否能参数化;
- 不同 DPI 和缩放比例下是否仍能保持边缘平滑;
- 是否需要补充完整的模板化和样式资源支持。
适合什么场景,能从这篇实现里学到什么
如果你的目标是快速做出一个可展示、可绑定、带动画的 WPF 圆形进度条,这套方案是能落地的。它没有走特别重的自定义渲染路线,而是基于 `UserControl`、`Storyboard`、`Path`、`Ellipse`、`Clip` 和依赖属性,把视觉效果拆成了 WPF 原生就能处理的几个部分。
更重要的是,这个案例适合拿来理解一类通用思路:当你要在 WPF 中实现不规则动画控件时,通常可以先用路径描述形状,再用动画驱动关键点变化,最后通过裁剪和绑定把它封装成可复用组件。这比单纯照着效果图堆控件,更接近真正可维护的实现方式。












