ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WPF自定义控件实战:用ProgressBar打造垂直温度计

WPF自定义控件实战:用ProgressBar打造垂直温度计 简介这份资源面向具备一定 WPF 基础的开发者聚焦如何把默认水平的 ProgressBar 改造成垂直温度计效果解决自定义进度控件外观与交互的常见需求。内容围绕 Orientation 属性设置、ControlTemplate 模板重写、Path 与 ScaleTransform 绘制温度指针、Storyboard 动画过渡以及温度值与单位绑定等知识点展开适合希望在仪表盘、监控面板等场景中打造专业 UI 的读者。压缩包共 41 个文件约 215KB以 cs 源码、xaml 界面、resources 资源、exe 与 config 配置等为主另含 sln 解决方案与 resx 资源文件结构完整可直接运行调试。目前已有 758 人学习下载。通过 Temperature_Demo 示例读者可对照完整工程理解模板、动画与数据绑定的整合方式并据此扩展出更多自定义控件。1. 垂直温度计一个被低估的 WPF 自定义控件练手场景工业上位机、环境监控看板、设备状态面板里垂直温度计出现的频率远比想象中高。它看起来只是把 ProgressBar 转 90 度但真动手就会发现WPF 自带的 ProgressBar 模板是横向布局直接旋转会连文字一起转刻度、水银柱圆角、渐变填充、报警区间全都要自己接管。这也是为什么很多 WPF 开发在搜「wpf 自定义控件」「wpf ProgressBar 垂直」时翻到的答案要么是LayoutTransform硬转要么是贴一段看不懂的 ControlTemplate 就没了。这篇笔记锁定的就是这件事用 ProgressBar 作为数据与状态载体通过重写模板和附加属性把它改造成一个可复用、可绑定、带刻度和报警色的垂直温度计。适合已经能写 WPF 基础绑定、想跨过「只会拖控件」这道坎的开发者也适合手上有监控类项目、需要快速落地一个仪表控件的熟手直接抄参数和模板结构。下面从选型理由讲到模板拆解再到避坑和进阶全部是可复现的路径。2. 为什么不用旋转ProgressBar 垂直化的三种路线对比2.1 三条路线的真实成本把横向进度条变成垂直温度计业内常见做法有三条我按落地成本从低到高排一下。第一条是LayoutTransform或RenderTransform旋转。写起来最快RenderTransformOrigin设成0.5,0.5再RotateTransform Angle270视觉上确实竖起来了。但问题立刻来文字跟着转、鼠标命中区域错位、刻度线方向全乱而且父容器测量时用的还是旋转前的宽高布局会留出莫名其妙的空白。做 Demo 可以进项目就是血泪经验。第二条是换控件用Slider设OrientationVertical或者用第三方图表库的仪表盘。Slider 天生支持垂直模板也好改但它语义是「可交互输入」用在只读监控场景要额外禁用交互、去掉 Thumb反而绕。图表库则引入外部依赖一个温度计不值得。第三条是重写 ProgressBar 的ControlTemplate用Grid手动排布轨道、填充条、刻度。这是我最推荐的路ProgressBar 本身有Minimum、Maximum、Value三个依赖属性Value变化时模板里的PART_Indicator会自动调整尺寸数据层完全不用自己写。我们只需要把「横向拉伸」改成「纵向拉伸」再补上刻度和报警色。路线开发耗时可维护性布局副作用推荐场景Transform 旋转10 分钟差明显临时演示换 Slider/图表库30 分钟中依赖库需要交互重写 ControlTemplate1-2 小时好无生产监控2.2 ProgressBar 模板里到底有什么要改模板先得知道默认模板长什么样。WPF 的 ProgressBar 默认模板核心就两个部分一个叫PART_Track的边框底槽一个叫PART_Indicator的矩形填充。控件内部逻辑是当Value变化它去模板里找名为PART_Indicator的元素按(Value-Minimum)/(Maximum-Minimum)算出比例然后设置这个元素的宽度。关键点来了它设置的是 Width不是 Height。这就是为什么单纯旋转没用、必须重写模板——我们要让填充条的高度随 Value 变化而不是宽度。理解这一点后面模板怎么写就顺了。!-- 默认模板的简化结构理解 PART_Indicator 的宽度驱动逻辑 -- ControlTemplate TargetTypeProgressBar Border x:NamePART_Track Background#EEE CornerRadius4 Grid Rectangle x:NamePART_Indicator HorizontalAlignmentLeft Fill#4CAF50/ /Grid /Border /ControlTemplate上面这段是默认逻辑的抽象。HorizontalAlignmentLeft配合内部设置的 Width填充条从左往右长。我们要做的是把它换成VerticalAlignmentBottom并让内部逻辑去改 Height。但 ProgressBar 的内部逻辑写死了改 Width所以真正落地时我们不用它的自动计算而是自己用Value和Maximum算高度通过IValueConverter或附加属性绑定。这是整个方案最核心的一步下一章展开。提示不要试图去反射改 ProgressBar 的内部计算逻辑版本升级会翻车。用绑定自己算高度稳定且可控。3. 手写垂直温度计模板从轨道到水银柱的完整 XAML3.1 用 IValueConverter 把 Value 换算成高度既然 ProgressBar 内部改的是 Width我们就绕开它自己把Value映射成填充条的高度。写一个转换器输入当前值和最大最小值输出像素高度。// PercentToHeightConverter.cs // 把 Value 按比例换算成填充条高度TotalHeight 由外部传入 public class PercentToHeightConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { // values[0]Value, values[1]Minimum, values[2]Maximum, values[3]轨道总高 double value System.Convert.ToDouble(values[0]); double min System.Convert.ToDouble(values[1]); double max System.Convert.ToDouble(values[2]); double total System.Convert.ToDouble(values[3]); if (max min) return 0d; double ratio (value - min) / (max - min); ratio Math.Max(0, Math.Min(1, ratio)); // 夹紧防止越界 return ratio * total; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) throw new NotSupportedException(); }逻辑说明IMultiValueConverter允许一次绑定多个源。这里绑了四个当前值、最小值、最大值、轨道高度。ratio算出百分比后夹紧到 0~1避免传感器异常值把水银柱顶出轨道。返回的是像素高度直接给填充条的Height。参数说明TotalHeight建议不要写死而是绑定轨道的ActualHeight这样窗口缩放时温度计自适应。如果轨道高度是固定值比如 300也可以直接传常量但自适应更稳。3.2 完整 ControlTemplate轨道、水银柱、刻度三件套下面是可以直接抄的模板。结构是三层Grid叠加底层轨道、中层水银柱、顶层刻度。Style x:KeyVerticalThermometerStyle TargetTypeProgressBar Setter PropertyTemplate Setter.Value ControlTemplate TargetTypeProgressBar Grid Width60 !-- 轨道总高固定 300实际项目可绑定 -- Border x:NameTrack Height300 Width24 CornerRadius12 Background#E8E8E8 HorizontalAlignmentCenter/ !-- 水银柱底部对齐高度由转换器算出 -- Border x:NameMercury Width24 CornerRadius12 HorizontalAlignmentCenter VerticalAlignmentBottom Height300 Border.Background LinearGradientBrush StartPoint0,1 EndPoint0,0 GradientStop Color#FF5722 Offset0/ GradientStop Color#FFC107 Offset1/ /LinearGradientBrush /Border.Background Border.Height MultiBinding Converter{StaticResource PercentToHeight} Binding PathValue RelativeSource{RelativeSource TemplatedParent}/ Binding PathMinimum RelativeSource{RelativeSource TemplatedParent}/ Binding PathMaximum RelativeSource{RelativeSource TemplatedParent}/ Binding PathActualHeight ElementNameTrack/ /MultiBinding /Border.Height /Border !-- 刻度用 ItemsControl 生成见 3.3 -- ItemsControl x:NameScale Width60 ItemsSource{Binding Ticks, RelativeSource{RelativeSource TemplatedParent}}/ /Grid /ControlTemplate /Setter.Value /Setter /Style逻辑说明Track是底槽固定 300 高、24 宽、圆角 12看起来像玻璃管。Mercury是水银柱VerticalAlignmentBottom保证从底部往上长Height通过MultiBinding绑定到转换器。渐变从底部橙红到顶部琥珀模拟温度升高。Scale是刻度层数据源Ticks是自定义依赖属性下一节讲。参数说明Width24是管径改大改小要同步改CornerRadius一般取宽度一半。Height300是轨道高度如果做自适应把它去掉并让外层容器约束。渐变两个GradientStop的颜色就是温度色报警场景可以换成蓝到红。3.3 刻度线用 ItemsControl 生成别手写刻度如果一条条手写Rectangle改量程时全要重来。正确做法是暴露一个Ticks集合用ItemsControl加Canvas定位。// 在自定义控件里加依赖属性 public static readonly DependencyProperty TicksProperty DependencyProperty.Register(Ticks, typeof(ObservableCollectiondouble), typeof(ThermometerBar), new PropertyMetadata(new ObservableCollectiondouble())); public ObservableCollectiondouble Ticks { get (ObservableCollectiondouble)GetValue(TicksProperty); set SetValue(TicksProperty, value); }// 初始化时按量程生成刻度比如 0~100 每 10 度一条 for (int i 0; i 100; i 10) Ticks.Add(i);逻辑说明Ticks存的是刻度对应的数值。模板里ItemsControl的ItemTemplate用转换器把数值换算成Canvas.Top再画一条短横线加文字。这样改量程只改集合模板不动。参数说明刻度间隔按业务定温度计一般 5 或 10。刻度文字建议用TextBlock加FontSize10太大会挤。定位时注意Canvas.Top是从上往下算而温度是从下往上换算公式是Top 轨道高 - (值-最小)/(最大-最小)*轨道高别搞反这是新手最容易翻车的地方。4. 数据绑定与实时刷新让温度计动起来4.1 用 MVVM 把温度值推给控件温度计要动靠的是Value变化。生产项目里温度来自串口、Modbus 或 MQTT最终落到一个 ViewModel 属性上。用INotifyPropertyChanged通知界面。public class MonitorViewModel : INotifyPropertyChanged { private double _temperature; public double Temperature { get _temperature; set { if (Math.Abs(_temperature - value) 0.01) return; // 死区过滤 _temperature value; OnPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string name null) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }逻辑说明Temperature的 setter 里加了死区过滤差值小于 0.01 不通知。传感器常有微小抖动不去抖界面会疯狂重绘CPU 飙升这是监控项目的经典坑。参数说明死区阈值按传感器精度定PT100 一般 0.1数字传感器可以 0.01。OnPropertyChanged用CallerMemberName省去手写属性名减少拼写错误。4.2 XAML 绑定与量程设置local:ThermometerBar Minimum0 Maximum100 Value{Binding Temperature, ModeOneWay} Width60 Height320/逻辑说明Minimum、Maximum设量程Value单向绑定到 ViewModel。温度计是只读展示用OneWay就够别用TwoWay徒增复杂度。参数说明量程要和传感器一致。如果传感器是 -20~80Minimum-20刻度生成逻辑也要跟着改。量程和刻度集合必须同步否则刻度对不上水银柱这是排查时第一个要看的点。4.3 报警区间用触发器改水银柱颜色温度超过阈值要变红用DataTrigger最干净。Border.Style Style TargetTypeBorder Setter PropertyBackground Value#4CAF50/ Style.Triggers DataTrigger Binding{Binding Temperature, Converter{StaticResource OverThresholdConverter}} ValueTrue Setter PropertyBackground Value#F44336/ /DataTrigger /Style.Triggers /Style /Border.Style逻辑说明OverThresholdConverter判断温度是否超过报警值返回布尔。触发器命中就把水银柱背景换成红色。比在代码里手动改颜色更符合 MVVM也更好维护。参数说明报警阈值建议做成依赖属性暴露出去别写死在转换器里。多级报警预警黄、报警红就加多个触发器按优先级从低到高排后面的覆盖前面的。5. 避坑与排查垂直温度计最容易翻车的五个点5.1 水银柱从顶部往下长现象Value增大填充条却从上面往下延伸。原因填充条VerticalAlignment设成了Top或默认Stretch。解决改成VerticalAlignmentBottom让底部固定、高度向上增长。这是最高频的错误没有之一。5.2 刻度数值和实际位置对不上现象刻度标 50 度但水银柱到 50 时和刻度线错位。原因刻度定位公式用了Top ratio * 高度忘了温度是从下往上。解决Top 轨道高 - ratio * 轨道高。另外确认刻度和水银柱用的是同一个Minimum/Maximum量程不一致也会错位。5.3 窗口缩放后水银柱高度不更新现象拖动窗口轨道变高了水银柱还是老高度。原因Height绑定的是固定值没绑ActualHeight。解决MultiBinding里第四个参数绑到Track的ActualHeight并确保Track的高度由布局决定而非写死。如果Track写死 300那缩放本来就不该变先确认需求。5.4 高频刷新导致界面卡顿现象温度每秒更新几十次界面掉帧。原因每次Value变化都触发模板重绘加上没做死区过滤。解决setter 里加死区刷新频率限制在 10Hz 以内如果还卡把Value绑定改成DispatcherTimer定时拉取而非事件推送。5.5 圆角在低高度时变形现象温度接近下限水银柱很矮圆角变成椭圆甚至消失。原因CornerRadius是固定值高度小于两倍圆角半径时圆角被压扁。解决给水银柱设MinHeight或者用Clip配合RectangleGeometry做圆角裁剪高度小时自动退化。简单做法是MinHeight设为CornerRadius * 2。注意这五个坑里5.1 和 5.2 占了新手问题的八成。先检查对齐方式和定位公式再去看绑定。6. 进阶把温度计做成可复用控件与性能验证做到这里温度计已经能跑。但要进生产项目还得解决两件事复用和性能。复用方面把前面所有东西——依赖属性Minimum、Maximum、Value、Ticks、报警阈值——打包成一个ThermometerBar : ProgressBar的自定义控件模板放进Themes/Generic.xaml。这样别的项目引用 DLL 就能用不用每次复制 XAML。我一般会把管径、轨道高、刻度间隔也做成依赖属性默认值给一套工业场景常用的管径 24、高 300、间隔 10需要时再覆盖。性能验证有个简单办法在 ViewModel 里用一个DispatcherTimer以 50Hz 推送随机温度跑十分钟看任务管理器的 CPU 和内存。如果 CPU 稳定在 1% 以下、内存无增长说明没有泄漏和过度重绘。如果 CPU 偏高八成是死区没做或者绑定用了TwoWay。我自己的习惯是任何实时控件上线前都跑一遍这个压力测试比事后查卡顿省事得多。还有一个容易被忽略的点Ticks集合如果是ObservableCollection每次Add都会触发ItemsControl重建。刻度是静态的初始化时一次性填好之后别动它。如果量程要动态改先Clear再批量Add或者干脆用普通List配合手动刷新。这个细节不注意量程切换时会看到明显闪烁。最后说个我踩过的坑温度计的Value绑定如果直接绑到传感器原始值而传感器偶尔返回NaN转换器里Convert.ToDouble(NaN)会算出NaNHeight设成NaN会让整个控件布局崩掉。所以转换器里一定要加double.IsNaN判断返回 0 或上一次的有效值。这种边界情况平时测不出来现场一跑就翻车后悔药都没得吃。希望帮到你。本文还有配套的精品资源点击获取
返回列表