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
- 注意这种方法不仅简单而且速度很快,不占用内存,基本满足大部分需求。
- 下方两个方法一定需要重写,不然会有异常。