侧边栏壁纸
  • 累计撰写 86 篇文章
  • 累计创建 11 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录
WPF

WPF绘制曲线

祈安千
2026-01-29 / 0 评论 / 0 点赞 / 6 阅读 / 0 字

Why

  • 最近正在研究绘制曲线,发现好像也没想象中的那么难,做了个简单的测试的。

How

  • 新建一个控件名为CurveChartDrawingVisual,以下是类的所有内容
 public class CurveChartDrawingVisual : FrameworkElement
 {


     public ObservableCollection<double> Points
     {
         get { return (ObservableCollection<double>)GetValue(PointsProperty); }
         set { SetValue(PointsProperty, value); }
     }

     public static readonly DependencyProperty PointsProperty =
         DependencyProperty.Register(nameof(Points), typeof(ObservableCollection<double>), typeof(CurveChartDrawingVisual), new PropertyMetadata(new ObservableCollection<double>(), OnPointsChanged));




     private static void OnPointsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
     {
         var control = d as CurveChartDrawingVisual;
         if (e.OldValue is ObservableCollection<double> oldPoints)
         {
             oldPoints.CollectionChanged -= control.OnPointsCollectionChanged;
         }

         if (e.NewValue is ObservableCollection<double> newPoints)
         {
             newPoints.CollectionChanged += control.OnPointsCollectionChanged;
         }

     }

     private void OnPointsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
     {

         Application.Current.Dispatcher.Invoke(() =>
         {
             Redraw();
         });
     }

     public CurveChartDrawingVisual()
     {

         AddVisualChild(layer);
     }

     private DrawingVisual layer = new();

     private void Redraw()
     {
         using var dc = layer.RenderOpen();
         DrawBackgGround(dc);
         DrawAxis(dc);
         DrawSmoothCurve(dc);
     }

     //X轴、Y轴的偏移量
     private double xOffset = 5;
     private double yOffset = 5;
     //自定义,仅用作测试
     private double maxValue = 850;

     private void DrawBackgGround(DrawingContext dc)
     {
         dc.DrawRectangle(Brushes.Black, null, new Rect(0, 0, ActualWidth, ActualHeight));
     }

     private void DrawAxis(DrawingContext dc)
     {
         var axisPen = new Pen(Brushes.Gray, 1);

         double width = ActualWidth - xOffset;
         double height = ActualHeight - yOffset;

         Point origin = new(xOffset, height);

         // X 轴
         dc.DrawLine(
             axisPen,
             origin,
             new Point(width, origin.Y)
         );

         // Y 轴
         dc.DrawLine(
             axisPen,
             origin,
             new Point(origin.X, 0)
         );

         DrawTicks(dc, origin, width, height);

     }

     private void DrawTicks(DrawingContext dc, Point origin, double width, double height)
     {
         var tickPen = new Pen(Brushes.White, 1);

         int xTickCount = 10;
         int yTickCount = 10;

         double xStep = width / xTickCount;
         double yStep = height / yTickCount;

         // X 轴刻度
         for (int i = 1; i <= xTickCount; i++)
         {
             double x = i * xStep;
             dc.DrawLine(
                 tickPen,
                 new Point(x, origin.Y),
                 new Point(x, 0)
             );
             dc.DrawText(new FormattedText((i * 10).ToString("F0"), CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, new Typeface("Arial"), 18, Brushes.Red, VisualTreeHelper.GetDpi(this).PixelsPerDip), new Point(x - 40, origin.Y - 20));
         }

         // Y 轴刻度
         for (int i = 1; i <= yTickCount; i++)
         {
             double y = origin.Y - i * yStep;
             dc.DrawLine(
                 tickPen,
                 new Point(origin.X, y),
                 new Point(ActualWidth, y)
             );

             dc.DrawText(new FormattedText((i * 85).ToString("F0"), CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, new Typeface("Arial"), 18, Brushes.Red, VisualTreeHelper.GetDpi(this).PixelsPerDip), new Point(origin.X, y));
         }
     }


     private void DrawSmoothCurve(DrawingContext dc)
     {
         var points = Points.ToList();
         if (points.Count < 2)
         {
             return;
         }
         double width = ActualWidth - xOffset;
         double height = ActualHeight - yOffset;
         var yScale = height / (maxValue - 1);
         double stepX = width / (100 - 1);
         var geometry = new StreamGeometry();
         using var ctx = geometry.Open();
         for (int i = 0; i < points.Count; i++)
         {
             var x = i * stepX + xOffset;
             var y = height - points[i] * yScale - yOffset;

             var p = new Point(x, y);
             if (i == 0)
             {
                 ctx.BeginFigure(p, false, false);
                 continue;
             }
             //var lastX = (i - 1) * stepX + xOffset;
             //var lastY = ActualHeight - points[i - 1] * yScale - yOffset;
             //var lastPoint = new Point((lastX + x) / 2, lastY);
             //ctx.QuadraticBezierTo(lastPoint, p, true, false);
             ctx.LineTo(p, true, false);

         }
         geometry.Freeze();


         dc.DrawGeometry(null, new Pen(Brushes.Lime, 2), geometry);
     }




     protected override int VisualChildrenCount => 1;
     protected override Visual GetVisualChild(int index) => layer;



 }
  • 其中最主要的逻辑是使用DrawingVisual+StreamGeometry绘制,基本上是原生最快的绘制曲线了。
  • 注意绘制的Y轴坐标系是左上角而不是一般认为的左下角。
  • 以下是如何添加数据内容(ViewModel
 public MainViewModel()
 {

     dataTimer = new Timer(RefreshData, null, 0, 200);
     dataTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
 }



 [ObservableProperty]
 private ObservableCollection<double> _buffers = [];

 private readonly Timer dataTimer;
 private readonly Random random = new();

 [RelayCommand]
 private void Start()
 {
     dataTimer.Change(0, 100);

 }

 private void RefreshData(object? state)
 {
     if (Buffers.Count >= 100)
     {
         //删除第一个
         Buffers.RemoveAt(0);
     }

     //数据从0-850
     var data = random.NextDouble() * 850;
     //var data = 250;
     Buffers.Add(data);

 }

 [RelayCommand]
 private void Stop()
 {
     dataTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
 }
  • 已下是如何调用控件
 <Grid d:ShowGridLines="True">
     <Grid.RowDefinitions>
         <RowDefinition Height="Auto" />
         <RowDefinition Height="*" />
     </Grid.RowDefinitions>
     <WrapPanel Orientation="Horizontal">
         <PQButton
             Width="80"
             Height="80"
             Command="{Binding StartCommand}"
             Content="Hello" />
         <Button
             Width="80"
             Command="{Binding StopCommand}"
             Content="暂停" />
     </WrapPanel>


     <Grid Grid.Row="1">
         <lc:CurveChartDrawingVisual x:Name="MyCurve" Points="{Binding Buffers, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
             <i:Interaction.Triggers>
                 <i:EventTrigger EventName="MouseEnter">
                     <i:InvokeCommandAction Command="{Binding ShowPointCommand}" />
                 </i:EventTrigger>
             </i:Interaction.Triggers>
         </lc:CurveChartDrawingVisual>
     </Grid>

 </Grid>

Tips

  • 注意这种方法不仅简单而且速度很快,不占用内存,基本满足大部分需求。
  • 下方两个方法一定需要重写,不然会有异常。
0
博主关闭了所有页面的评论