我们开发了一个简单的 WPF UserControl,它是一个 ChartLine,通常应该显示 -100 到 100 范围内的 512 个值。 该图表可以工作,但是,该图表需要每 1 秒清除和更新一次其值,并且需要花费一秒(1.4~~秒)来简单地呈现其所有值。 在这次失败的尝试之后,我尝试使用 Microsoft 的旧 DynamicDataDisplay (D3),它应该更快,但性能影响完全相同,更新屏幕上的 512 值也花费了一秒多的时间。
下面是我的代码,我相信可能有一些缓存技术、较低的位图分辨率或有助于实现我的目标的东西。
XAML:
<UserControl x:Class="IHM.OsciloscopeGraphic"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:IHM"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="740" Loaded="UserControl_Loaded">
<Grid x:Name="gdMain">
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Label Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:OsciloscopeGraphic}}, Path=TitleGreen}" HorizontalAlignment="Right" HorizontalContentAlignment="Center" Margin="10, 0" FontSize="18" Width="115" Background="Green"/>
<Label Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:OsciloscopeGraphic}}, Path=TitleLightBlue}" FontSize="18" Margin="10, 0" HorizontalAlignment="Left" HorizontalContentAlignment="Center" Background="LightBlue" Grid.Column="1" Width="115"/>
<Grid Name="gdChartArea" Grid.Row="1" Grid.ColumnSpan="2" >
<Border BorderBrush="Black" BorderThickness="1" Margin="30, 10, 10, 30"/>
<Canvas x:Name="cnvChart" Margin="30, 10, 10, 30">
<Canvas.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#4C000080" Offset="1"/>
<GradientStop Color="#4C7F7FFF"/>
</LinearGradientBrush>
</Canvas.Background>
</Canvas>
</Grid>
</Grid>
C# 代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Shapes;
namespace IHM
{
public partial class OsciloscopeGraphic : UserControl
{
#region Properties
/// <summary>
/// If steps Lines are 0, will divide the grid equally by the number in lines grid
/// </summary>
public int LinesGrid { get; set; }
/// <summary>
/// If steps Columns are 0, will divide the grid equally by the number in lines grid
/// </summary>
public int ColumnsGrid { get; set; }
public int StepsLines { get; set; }
public int StepsColumns { get; set; }
public int MaxHorizontal { get; set; }
public int MaxVertical { get; set; }
public int MinHorizontal { get; set; }
public int MinVertical { get; set; }
public static readonly DependencyProperty TitleGreenProperty =
DependencyProperty.Register("TitleGreen", typeof(string), typeof(BarGraphicSplitted), new UIPropertyMetadata("TRS"));
[Bindable(true)]
public string TitleGreen
{
get { return (string)GetValue(TitleGreenProperty); }
set { SetValue(TitleGreenProperty, value); }
}
public static readonly DependencyProperty TitleLightBlueProperty =
DependencyProperty.Register("TitleLightBlue", typeof(string), typeof(BarGraphicSplitted), new UIPropertyMetadata("FRT"));
[Bindable(true)]
public string TitleLightBlue
{
get { return (string)GetValue(TitleLightBlueProperty); }
set { SetValue(TitleLightBlueProperty, value); }
}
#endregion Properties
#region Local Fields/Variables
private bool initialized = false;
private int Quantidade
{
get { return (Math.Abs(this.MaxHorizontal - this.MinHorizontal) + 1); }
}
#endregion Local Fields/Variables
public OsciloscopeGraphic()
{
InitializeComponent();
this.MaxHorizontal = 255;
this.MinHorizontal = 0;
this.MaxVertical = 100;
this.MinVertical = -100;
this.LinesGrid = 0;
this.ColumnsGrid = 0;
this.StepsColumns = 10;
this.StepsLines = 10;
}
#region Private Local/Methods
private Line CreateGridLine()
{
Line lm = new Line();
lm.Stroke = Brushes.Black;
lm.StrokeThickness = 1;
lm.StrokeDashArray = new DoubleCollection() { 1, 4 };
lm.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
return lm;
}
private Line CreateHorizontalGridLine(Point start, double length)
{
Line ln = CreateGridLine();
//It has the same value because the line will be a vertical line
ln.X1 = start.X;
ln.X2 = start.X + length;
ln.Y1 = start.Y;
ln.Y2 = start.Y;
return ln;
}
private Line CreateHorizontalScaleLine(Point start)
{
Line l = CreateScaleLine();
l.X1 = start.X;
l.X2 = start.X - 5;
l.Y1 = start.Y;
l.Y2 = start.Y;
return l;
}
private Line CreateScaleLine()
{
Line l = new Line();
l.Stroke = Brushes.Black;
l.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
return l;
}
private Line CreateVerticalGridLine(Point start, double length)
{
Line ln = CreateGridLine();
//It has the same value because the line will be a vertical line
ln.X1 = start.X;
ln.X2 = start.X;
ln.Y1 = start.Y;
ln.Y2 = start.Y + length;
return ln;
}
private Line CreateVerticalScaleLine(Point start)
{
Line l = CreateScaleLine();
l.X1 = start.X;
l.X2 = start.X;
l.Y1 = start.Y;
l.Y2 = start.Y + 5;
return l;
}
private void DrawGrid(Grid grid, Canvas chart)
{
bool makeBySteps = true;
if ((this.StepsColumns == 0) || (this.StepsLines == 0))
{
makeBySteps = false;
if ((this.LinesGrid == 0) || (this.ColumnsGrid == 0))
throw new DivideByZeroException();
}
//get canvas absolute position
var getPos = chart.TransformToVisual(grid);
Point XYpos = getPos.Transform(new Point(0, 0));
//draw the lines
double actualWidth = (chart.ActualWidth);
double initialPosition = (XYpos.X + 1);
double length = this.MaxHorizontal - this.MinHorizontal + 1;
double stepLegend = (makeBySteps) ? this.StepsColumns : length / Convert.ToDouble(this.ColumnsGrid);
int counter = (makeBySteps) ? ((int)length) / this.StepsColumns : this.ColumnsGrid;
double step = (makeBySteps) ? (actualWidth / length) * this.StepsColumns : (actualWidth / this.ColumnsGrid);
length = Math.Abs(length);
double remainder = 0d;
for (int i = 0; i <= counter; i++)
{
//vertical gridlines
double steps = i * step;
Point start = new Point(initialPosition + steps, XYpos.Y);
Line Lm = CreateVerticalGridLine(start, chart.ActualHeight);
grid.Children.Add(Lm);
//vertical scale lines
Point startScale = new Point(initialPosition + steps, XYpos.Y + chart.ActualHeight);
Line LineScale = CreateVerticalScaleLine(startScale);
grid.Children.Add(LineScale);
//bottom labels
Label lb = new Label();
lb.Width = 20;
lb.Height = 20;
lb.Padding = new Thickness(0);
lb.HorizontalContentAlignment = HorizontalAlignment.Center;
lb.ClipToBounds = false;
//this garantes that it will consider the reminder of divisions
double numero = this.MinHorizontal + (i * stepLegend);
remainder += numero - Math.Round(numero);
numero = Math.Round(numero);
if (remainder > 1)
{
remainder -= 1;
numero += 1;
}
else if (remainder < -1)
{
remainder += 1;
numero -= 1;
}
lb.Content = numero;
grid.Children.Add(lb);
lb.HorizontalAlignment = HorizontalAlignment.Left;
lb.VerticalAlignment = VerticalAlignment.Top;
//TODO: big coment explaining in details the line bellow
lb.Margin = new Thickness((XYpos.X - 10) + steps, XYpos.Y + chart.ActualHeight + 5, 0, 0);
}
initialPosition = XYpos.Y;
double actualHeight = (chart.ActualHeight);
length = this.MaxVertical - this.MinVertical + 1;
stepLegend = (makeBySteps) ? this.StepsLines : length / Convert.ToDouble(this.LinesGrid);
counter = (makeBySteps) ? ((int)length) / this.StepsLines : this.LinesGrid;
step = (makeBySteps) ? (actualHeight / length) * this.StepsLines : (actualHeight / this.LinesGrid);
//initialPosition = (makeBySteps) ? initialPosition + ((actualHeight / length) * (length % this.StepsLines)) : initialPosition;
length = Math.Abs(length);
remainder = 0d;
for (int i = 0; i <= counter; i++)
{
double steps = i * step;
Point start = new Point(XYpos.X, actualHeight + initialPosition - steps);
//horizontal gridlines
Line lm = CreateHorizontalGridLine(start, actualWidth);
grid.Children.Add(lm);
//horizontal scale lines
Line l = CreateHorizontalScaleLine(start);
grid.Children.Add(l);
//side labels
Label lb = new Label();
lb.Width = 30;
lb.Height = 20;
lb.HorizontalContentAlignment = System.Windows.HorizontalAlignment.Right;
lb.Padding = new Thickness(0);
lb.VerticalContentAlignment = VerticalAlignment.Center;
lb.ClipToBounds = false;
//this garantes that it will consider the reminder of divisions
double numero = this.MinVertical + (i * stepLegend);
remainder += numero - Math.Round(numero);
numero = Math.Round(numero);
if (remainder > 1)
{
remainder -= 1;
numero += 1;
}
else if (remainder < -1)
{
remainder += 1;
numero -= 1;
}
lb.Content = numero;
grid.Children.Add(lb);
lb.HorizontalAlignment = HorizontalAlignment.Left;
lb.VerticalAlignment = VerticalAlignment.Top;
//TODO: big coment explaining in details the line bellow
lb.Margin = new Thickness(XYpos.X - 37, start.Y - 10, 0, 0);
}
}
private void DrawGrid()
{
this.DrawGrid(gdChartArea, cnvChart);
}
private void DrawLine(List<int> p_values, SolidColorBrush cor)
{
Polyline cl = new Polyline();
cl.Stroke = cor;
cl.StrokeThickness = 2;
cl.StrokeLineJoin = PenLineJoin.Round;
//cl.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
double stepHorizontal = cnvChart.ActualWidth / ((this.MaxHorizontal - this.MinHorizontal) + 1);
double stepVertical = cnvChart.ActualHeight / ((this.MaxVertical - this.MinVertical) + 1);
for (int i = 0; i < p_values.Count; i++)
{
int val = p_values[i];
double x = (stepHorizontal * i);
double y = cnvChart.ActualHeight - ((val - this.MinVertical) * stepVertical);
cl.Points.Add(new Point(x, y));
}
cnvChart.Children.Add(cl);
}
private void DrawLineGreen(List<int> p_values)
{
DrawLine(p_values, Brushes.Green);
}
private void DrawLineLightBlue(List<int> p_values)
{
DrawLine(p_values, Brushes.LightBlue);
}
private List<int> GetRandomValues()
{
int quantidade = this.Quantidade;
List<int> lsValues = new List<int>(quantidade);
int seed = 0;
long ticks = DateTime.Now.Ticks;
while (ticks > int.MaxValue)
{
ticks -= int.MaxValue;
}
seed = Convert.ToInt32(ticks);
Random ran = new Random(seed);
for (int i = 0; i < quantidade; i++)
{
int randomValue = ran.Next(this.MinVertical, this.MaxVertical);
lsValues.Add(randomValue);
}
return lsValues;
}
#endregion Private Local/Methods
#region Public Methods
public void Clear()
{
this.cnvChart.Children.Clear();
}
public void UpdateGraphValues()
{
UpdateGraphValues(GetRandomValues(), GetRandomValues());
}
public void UpdateGraphValues(List<int> p_frontValues, List<int> p_backValues)
{
//Clear current graphic values.
Clear();
DrawLineGreen(p_frontValues);
DrawLineLightBlue(p_backValues);
}
#endregion Public Methods
#region Window Events
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
if (!initialized)
{
DrawGrid();
UpdateGraphValues();
initialized = true;
}
}
#endregion Window Events
}
}
要在我希望的条件下测试图表,您可以简单地实例化`
private OsciloscopeGraphic graphicOscNormal = new OsciloscopeGraphic()
{
MinHorizontal = 0,
MaxHorizontal = 255,
MinVertical = -100,
MaxVertical = 100
};
在计时器内,您可以调用`graphicOscNormal.UpdateGraphValues()
`,它将用随机值填充图形以用于测试目的。
稍后这些值将来自已实现的串行端口。
注意:我还尝试替换 DrawingVisual 和 DrawingContext.DrawLine 的高级 PolyLine,但性能没有改变!
注意2:我使用的是 C#/WPF 和 .NET 4.0 (VS 2010)。
提前致谢,路易斯。
最佳答案
(最大的)问题是你的随机数生成器 - 它效率极低。尝试:
private Random ran = new Random(0);
private List<int> GetRandomValues()
{
int quantidade = this.Quantidade;
List<int> lsValues = new List<int>(quantidade);
for (int i = 0; i < quantidade; i++)
{
int randomValue = ran.Next(this.MinVertical, this.MaxVertical);
lsValues.Add(randomValue);
}
return lsValues;
}
优化时,分析是值得的。
如果您想要真正、真正快速渲染,那么您几乎必须回到 GDI。例如 - 更新您的 Canvas
(cnvChart) 以使用此 FastCanvas
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows;
using System.Windows.Interop;
using System.Runtime.InteropServices;
using System.Drawing.Drawing2D;
namespace WpfApplication1
{
class FastCanvas : Canvas
{
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateFileMapping(IntPtr hFile,
IntPtr lpFileMappingAttributes,
uint flProtect,
uint dwMaximumSizeHigh,
uint dwMaximumSizeLow,
string lpName);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject,
uint dwDesiredAccess,
uint dwFileOffsetHigh,
uint dwFileOffsetLow,
uint dwNumberOfBytesToMap);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool UnmapViewOfFile(IntPtr lbBaseAddress);
protected System.Drawing.Graphics GDIGraphics;
protected InteropBitmap interopBitmap = null;
protected InteropBitmap buffBitmap = null;
private const uint FILE_MAP_ALL_ACCESS = 0xF001F;
private const uint PAGE_READWRITE = 0x04;
private int bpp = PixelFormats.Bgra32.BitsPerPixel / 8;
protected IntPtr MapViewPointer;
public struct ScopeLine
{
public SolidColorBrush lineBrush;
public List<Point> linePoints;
}
public List<ScopeLine> Lines = new List<ScopeLine>();
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
if (Lines.Count() > 0)
{
ImageSource drIs = null;
if (interopBitmap == null)
{
uint byteCount = (uint)((int)this.ActualWidth * (int)this.ActualHeight * bpp);
var fileMappingPointer = CreateFileMapping(new IntPtr(-1), IntPtr.Zero, PAGE_READWRITE, 0, byteCount, null);
this.MapViewPointer = MapViewOfFile(fileMappingPointer, FILE_MAP_ALL_ACCESS, 0, 0, byteCount);
PixelFormat format = PixelFormats.Bgra32;
var stride = (int)((int)this.ActualWidth * format.BitsPerPixel / 8);
this.interopBitmap = Imaging.CreateBitmapSourceFromMemorySection(fileMappingPointer,
(int)this.ActualWidth,
(int)this.ActualHeight,
format,
stride,
0) as InteropBitmap;
this.GDIGraphics = GetGdiGraphics(MapViewPointer);
}
GDIGraphics.FillRectangle(System.Drawing.Brushes.Transparent,
new System.Drawing.Rectangle(0, 0,
(int)this.ActualWidth,
(int)this.ActualHeight));
foreach (ScopeLine dLine in Lines)
{
var pointCount = dLine.linePoints.Count();
Color lpColour;
lpColour = dLine.lineBrush.Color;
System.Drawing.Color lp2Colour;
lp2Colour = System.Drawing.Color.FromArgb(lpColour.A,
lpColour.R,
lpColour.G,
lpColour.B);
System.Drawing.Pen lpPen = new System.Drawing.Pen(lp2Colour, 1.5f);
System.Drawing.PointF newPoint = new System.Drawing.PointF((float)dLine.linePoints[0].X,
(float)dLine.linePoints[0].Y);
for (int i = 0; i < pointCount - 1; i++)
{
System.Drawing.PointF newPoint1 = new System.Drawing.PointF((float)dLine.linePoints[i + 1].X,
(float)dLine.linePoints[i + 1].Y);
GDIGraphics.DrawLine(lpPen, newPoint, newPoint1);
newPoint = newPoint1;
}
}
var bmpsrc = interopBitmap.GetAsFrozen();
if (bmpsrc == null || bmpsrc.CheckAccess())
{
drIs = (System.Windows.Media.Imaging.BitmapSource)bmpsrc;
}
else
{
//Debug.WriteLine("No access to TheImage");
}
dc.DrawImage(drIs, new Rect(this.RenderSize));
}
}
private System.Drawing.Graphics GetGdiGraphics(IntPtr mapViewPointer)
{
System.Drawing.Graphics gdiGraphics;
System.Drawing.Bitmap gdiBitmap;
gdiBitmap = new System.Drawing.Bitmap((int)this.ActualWidth,
(int)this.ActualHeight,
(int)this.ActualWidth * bpp,
System.Drawing.Imaging.PixelFormat.Format32bppArgb,
mapViewPointer);
gdiGraphics = System.Drawing.Graphics.FromImage(gdiBitmap);
gdiGraphics.CompositingMode = CompositingMode.SourceCopy;
gdiGraphics.CompositingQuality = CompositingQuality.HighSpeed;
gdiGraphics.SmoothingMode = SmoothingMode.HighSpeed;
return gdiGraphics;
}
}
}
并将您的 DrawLine
更改为:
private void DrawLine(List<int> p_values, SolidColorBrush cor)
{
double stepHorizontal = cnvChart.ActualWidth / ((this.MaxHorizontal - this.MinHorizontal) + 1);
double stepVertical = cnvChart.ActualHeight / ((this.MaxVertical - this.MinVertical) + 1);
List<Point> pts = new List<Point>();
for (int i = 0; i < p_values.Count; i++)
{
int val = p_values[i];
double x = (stepHorizontal * i);
double y = cnvChart.ActualHeight - ((val - this.MinVertical) * stepVertical);
pts.Add(new Point(x,y));
}
FastCanvas.ScopeLine newLine;
newLine.lineBrush = cor;
newLine.linePoints = pts;
cnvChart.Lines.Add(newLine);
}
和更新值
到:
public void UpdateGraphValues(List<int> p_frontValues, List<int> p_backValues)
{
cnvChart.Lines.Clear();
DrawLineGreen(p_frontValues);
DrawLineLightBlue(p_backValues);
cnvChart.InvalidateVisual();
}
使用像这样的 GDI,相同的图形可以实时更新(对于 512 点,轻松 > 30fps),而使用 WPF 渲染的速度约为 5-7fps。
关于c# - WPF 自定义 ChartLine 性能问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17969146/