前言
在用户界面设计中,仪表盘(Gauge)控件是展示关键性能指标(KPI)和实时数据的有力工具。
通过直观的视觉表示,仪表盘能够迅速传达复杂的数据信息,帮助用户做出快速决策。为了满足这一需求,推荐一款基于 WPF 的自定义 Gauge 控件,该控件继承自 RangeBase,支持设置最大值、最小值以及当前值,并且可以显示一个动态的百分比值、标题和刻度。
项目环境
框架支持.NET 4 至 .NET 8;
Visual Studio 2022;
WPFDevelopers 1.1.0.3-preview 版本已正式发布!
该版本为预览版,欢迎各位下载并体验。
逻辑实现
Gauge 继承 RangeBase,支持设置最大值、最小值以及当前值,并且可以显示一个动态的百分比值、标题、刻度。
1、新增 Gauge.cs
依赖属性
Title:仪表盘的标题,默认值为 "WD"。
ValueFormat:定义数值显示的格式( {0:0}%),以百分比形式显示当前值。
Thickness:设置仪表盘的边框,默认值 10。
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public string ValueFormat
{
get { return (string)GetValue(ValueFormatProperty); }
set { SetValue(ValueFormatProperty, value); }
}
public double Thickness
{
get { return (double)GetValue(ThicknessProperty); }
set { SetValue(ThicknessProperty, value); }
}
RangeBase 依赖属性
Value:设置为 0.0,表示当前的仪表盘值(默认 0%)。
Minimum:设置为 0.0,表示仪表盘的最小值。
Maximum:设置为 100.0,表示仪表盘的最大值。
public Gauge()
{
SetValue(ValueProperty, 0.0);
SetValue(MinimumProperty, 0.0);
SetValue(MaximumProperty, 100.0);
}
2、重写OnRender方法绘制控件
背景:使用 Ellipse 作为仪表盘的背景,背景色通过 Background 属性设置,默认为 #293950。
指针:根据当前值(Value),计算出指针的角度,并使用 DrawLine 绘制红色指针。
外边框:使用渐变颜色绘制仪表盘的外边框。
刻度和标签:绘制了从最小值到最大值的刻度线,并在每个刻度绘制了对应的刻度值。
当前值显示:根据 ValueFormat 属性格式化显示当前值,并显示在仪表盘的底部位置。
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (Background == null)
Background = new
SolidColorBrush((Color)ColorConverter.ConvertFromString("#293950"));
var width = ActualWidth;
var height = ActualHeight;
var radius = Math.Min(width, height) / 2;
drawingContext.DrawEllipse(Background, new Pen(Background,
Thickness), new Point(width / 2, height / 2), radius, radius);
var normalizedValue = (Value - Minimum) / (Maximum - Minimum);
var mappedAngle = -220 + normalizedValue * 260;
var angleInRadians = mappedAngle * Math.PI / 180;
var pointerLength = radius * 0.7;
var pointerX = width / 2 + pointerLength * Math.Cos(angleInRadians);
var pointerY = height / 2 + pointerLength * Math.Sin(angleInRadians);
drawingContext.DrawLine(new Pen(Brushes.Red, 2),
new Point(width / 2, height / 2), new Point(pointerX, pointerY));
drawingContext.DrawEllipse(Brushes.White, new Pen(Brushes.Red, 2), new Point(width / 2, height / 2), width / 20, width / 20);
var pathGeometry = new PathGeometry();
var startAngle = -220;
angleInRadians = startAngle * Math.PI / 180;
var startX = width / 2 + radius * Math.Cos(angleInRadians);
var startY = height / 2 + radius * Math.Sin(angleInRadians);
var pathFigure = new PathFigure()
{
StartPoint = new Point(startX, startY),
};
var endAngle = 40;
angleInRadians = endAngle * Math.PI / 180;
var endX = width / 2 + radius * Math.Cos(angleInRadians);
var endY = height / 2 + radius * Math.Sin(angleInRadians);
var isLargeArc = (endAngle - startAngle > 180);
var arcSegment = new ArcSegment()
{
Point = new Point(endX, endY),
Size = new Size(radius, radius),
RotationAngle = 0,
SweepDirection = SweepDirection.Clockwise,
IsLargeArc = isLargeArc,
};
pathFigure.Segments.Add(arcSegment);
pathGeometry.Figures.Add(pathFigure);
if (BorderBrush == null)
{
var gradientBrush = new LinearGradientBrush
{
StartPoint = new Point(0, 0),
EndPoint = new Point(1, 0)
};
gradientBrush.GradientStops.Add(
new GradientStop((Color)ColorConverter.ConvertFromString("#37D2C2"), 0.0));
gradientBrush.GradientStops
.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#5AD2B2"), 0.01));
gradientBrush.GradientStops
.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#B77D29"), 0.49));
gradientBrush.GradientStops
.Add(new GradientStop(Colors.Red, 1.0));
gradientBrush.Freeze();
BorderBrush = gradientBrush;
}
drawingContext.DrawGeometry(null,
new Pen(BorderBrush,
Thickness), pathGeometry);
var tickLength = radius * 0.1;
var step = (Maximum - Minimum) / 10;
for (int i = 0; i <= 10; i++)
{
var angle = startAngle + (i * (endAngle - startAngle) / 10);
var tickStartX = width / 2 + (radius - tickLength) * Math.Cos(angle * Math.PI / 180);
var tickStartY = height / 2 + (radius - tickLength) * Math.Sin(angle * Math.PI / 180);
var tickEndX = width / 2 + (radius + Thickness / 2) * Math.Cos(angle * Math.PI / 180);
var tickEndY = height / 2 + (radius + Thickness / 2) * Math.Sin(angle * Math.PI / 180);
drawingContext.DrawLine(new Pen(Brushes.White, 2),
new Point(tickStartX,
tickStartY), new Point(tickEndX, tickEndY));
var labelValue = Minimum + step * i;
var formattedText = DrawingContextHelper.GetFormattedText(
labelValue.ToString(),Brushes.White,
FlowDirection.LeftToRight,FontSize);
var labelRadius = radius - tickLength * 2;
var labelX = width / 2 + labelRadius * Math.Cos(angle * Math.PI / 180) - formattedText.Width / 2;
var labelY = height / 2 + labelRadius * Math.Sin(angle * Math.PI / 180) - formattedText.Height / 2;
drawingContext.DrawText(
formattedText,
new Point(labelX, labelY));
}
var formattedValue = "{0:0}%";
try
{
formattedValue =
string.Format(ValueFormat, Value);
}
catch (FormatException ex)
{
throw new
InvalidOperationException("Formatting failed ", ex);
}
var currentValueText = DrawingContextHelper.GetFormattedText(formattedValue,
Brushes.White, FlowDirection.LeftToRight, FontSize * 2);
var valueX = width / 2 - currentValueText.Width / 2;
var valueY = height / 2 + radius * 0.4;
drawingContext.DrawText(currentValueText,
new Point(valueX, valueY));
var titleValue = DrawingContextHelper.GetFormattedText(Title,
Brushes.White,
FlowDirection.LeftToRight, FontSize);
valueX = width / 2 - titleValue.Width / 2;
valueY = height / 2 + radius * 0.8;
drawingContext.DrawText
(titleValue, new Point(valueX, valueY));
}
XAML 示例
示例引入WPFDevelopers 1.1.0.3-preview的 Nuget 正式包
<WrapPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel VerticalAlignment="Bottom">
<wd:Gauge
Title="Min"
Width="100"
Height="100"
Margin="10"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="Black"
BorderBrush="Red"
FontSize="8"
Maximum="90"
Minimum="30"
Thickness="3"
ValueFormat="{}{0:0}值"
Value="{Binding ElementName=MySlider2,
Path=Value}" />
<Slider
Name="MySlider2"
Width="200"
Margin="0,0,0,20"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Maximum="90"
Minimum="30" />
</StackPanel>
<StackPanel>
<wd:Gauge
Title="反对率"
Width="200"
Height="200"
Margin="10"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ValueFormat="{}{0:0}%"
Value="{Binding ElementName=MySlider,
Path=Value}" />
<Slider
Name="MySlider"
Width="200"
Margin="0,0,0,20"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Maximum="100"
Minimum="0" />
</StackPanel>
<StackPanel VerticalAlignment="Bottom">
<wd:Gauge
Title="Max"
Width="100"
Height="100"
Margin="10"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="Black"
BorderBrush="DodgerBlue"
FontSize="8"
Maximum="90"
Minimum="30"
Thickness="3"
ValueFormat="{}{0:0}值"
Value="{Binding ElementName=MySlider3,
Path=Value}" />
<Slider
Name="MySlider3"
Width="200"
Margin="0,0,0,20"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Maximum="90"
Minimum="30" />
</StackPanel>
</WrapPanel>
功能丰富的 Gauge 控件助力高效数据展示
通过继承 RangeBase 并扩展其功能,我们成功创建了一个功能强大的 Gauge 控件,它不仅支持设置最大值、最小值以及当前值,还能够显示动态的百分比值、标题和刻度。
总结
以下是该控件的主要优势:
灵活性与可定制性:通过继承 RangeBase,控件具备了良好的灵活性和可扩展性,开发者可以根据具体需求调整其行为和外观。
直观的数据呈现:动态百分比值和刻度线的加入,使得数据展示更加直观和易于理解,提升了用户体验。
广泛的应用场景:无论是用于监控系统状态、展示业务指标,还是其他需要实时数据可视化的场景,Gauge 控件都能提供出色的解决方案。
简化开发流程:高度封装的功能减少了开发者的工作量,使他们能够更专注于业务逻辑的实现,从而加快项目的开发进度。
总之,这款 Gauge 控件为 WPF 开发者提供了一个强大而灵活的数据可视化工具,极大地提升了应用程序的数据展示能力。通过使用该控件,开发者可以更轻松地构建出美观且实用的用户界面,满足不同项目的需求。希望这篇文章能为您的开发工作提供有价值的参考和启发。
最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。
也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!
作者:WPF开发者
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!