WPF 自绘 Gauge 仪表盘:实现个性化数据展示

724 阅读6分钟

前言

在用户界面设计中,仪表盘(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(00),  
            EndPoint = new Point(10)  
        };  
        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(nullnew 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开发者

出处:github.com/WPFDevelope…

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!