用户控件中的WPF高效绘图

Posted

技术标签:

【中文标题】用户控件中的WPF高效绘图【英文标题】:WPF efficient drawing in usercontrol 【发布时间】:2015-04-12 16:17:34 【问题描述】:

我在 WPF 中绘制用户控件时遇到性能问题。这就是我所做的。 基本上,该应用程序播放音频文件。播放音频文件的类每秒钟发送一次有关其位置的信息。

我有一个呈现文件位置的用户控件。我是这样做的:

    protected override void OnRender(DrawingContext drawingContext)
    

        // lots of drawing happens here like drawingContext.DrawRectangle,
        // drawingContext.DrawLine ....

        base.OnRender(drawingContext);
    

现在我在应用程序中有另一个线程进行一些处理(主要是读取音频数据)并通过委托每秒向用户控件发送几次信息。第二个进程像这样声明一个委托:

    protected virtual void OnBytesPlayed(BytesPlayedEventArgs e)
    

        if (playheadCallback != null)
            playheadCallback(fileStream.CurrentTime.TotalSeconds, fileStream.TotalTime.TotalSeconds);

    

用户控件注册到该事件并重新绘制自身:

    public void PlayheadPositionUpdate(double currentFrame, double allFrames)
    

        Application.Current.Dispatcher.BeginInvoke(
           DispatcherPriority.Background,
           new System.Action(() =>
           
               this.InvalidateVisual();
           ));
    

这一切都有效,但是一旦我启用回调并且它每秒绘制几次用户控件,一切都会变得缓慢并且性能下降。所以我想我处理绘图或重绘的方式效率不高。有人可以指出正确的方向如何重绘用户控件,使其不会影响播放性能或应用程序的整体性能吗?

如果我需要解释更多或提供更多代码,请告诉我。我想尽可能简单地概述问题。

谢谢。

编辑:

调用NAudio的代码是

            var inputStream = new AudioFileReader(filename);

            fileStream = inputStream;

            var aggregator = new SampleAggregator(inputStream);

            aggregator.NotificationCount = inputStream.WaveFormat.SampleRate;

            aggregator.BytesPLayed += (s, a) => OnBytesPlayed(a);

            playbackDevice.Init(aggregator);

【问题讨论】:

你在哪个线程上播放音频? 我正在使用一个名为 naudio 的库。不确定它是如何详细实现的,但假设是单独的线程,因为主屏幕的其余部分减去用户控件会正确反应。只有当我每秒重绘用户控件几次时才会出现问题。 据我所知,您如何使用 NAudio 并不重要。只需搜索 NAudio 和线程,您就会找到关于此的讨论。能否添加初始化和启动 NAuio 的代码? 绘制到 DrawingVisual 的一种可能更有效的方法是使用它,如 MSDN 上的 Using DrawingVisual Objects 文章中所示(使用 DrawingVisual 主机容器)。它将消除调用 InvalidateVisual 的需要,根据 MSDN 的说法,它“强制执行一个全新的布局传递”。 你也可以在一个单独的线程中绘制你的位图......并且只有在绘制完成后才会失效......然后失效应该直接将位图推到你的画布上......如果你有一些基本的绘图然后你甚至可以将它们缓存在另一个位图中,并且只重绘动态数据:=) 【参考方案1】:

您应该改用数据绑定和 INotifyPropertyChanged 并让框架控制呈现。我有一个简单的例子来展示如何显示一个简单的图表。

GraphControl.xaml

  <Border Height="50" Width="200" BorderBrush="Black" CornerRadius="1" BorderThickness="0.1">
    <Canvas x:Name="canvas" Background="White" Margin="0" ClipToBounds="True" />
  </Border>

GraphControl.xaml.cs

  public partial class GraphControl : UserControl
  
    public IEnumerable<double> Data
    
      get  return (IEnumerable<double>)GetValue(DataProperty); 
      set  SetValue(DataProperty, value); 
    
    public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register("Data", typeof(IEnumerable<double>), typeof(GraphControl), new PropertyMetadata(null, OnDataChanged));

    private static void OnDataChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    
      var control = (GraphControl)dependencyObject;
      control.HandleDataChanged();
    

    public Brush Color
    
      get  return (Brush)GetValue(ColorProperty); 
      set  SetValue(ColorProperty, value); 
    
    public static readonly DependencyProperty ColorProperty =
        DependencyProperty.Register("Color", typeof(Brush), typeof(GraphControl), new PropertyMetadata(Brushes.Green));

    public double Maximum
    
      get  return (double)GetValue(MaximumProperty); 
      set  SetValue(MaximumProperty, value); 
    
    public static readonly DependencyProperty MaximumProperty =
        DependencyProperty.Register("Maximum", typeof(double), typeof(GraphControl), new PropertyMetadata(100.0));

    private void HandleDataChanged()
    
      var points = new PointCollection();
      double x = 0;


      points.Add(new Point(0, canvas.ActualHeight));


      var xScale = canvas.ActualWidth / (Data.Count() -1);
      var yScale = canvas.ActualHeight / Maximum;
      foreach (var dataPoint in Data)
      
        var y = canvas.ActualHeight - dataPoint * yScale;

        points.Add(new Point(x,y));
        x += xScale;

      

      points.Add(new Point(canvas.ActualWidth, canvas.ActualHeight));

      var fill = Color.Clone();
      fill.Opacity = 0.2;
      polygon.Stroke = Color;
      polygon.Fill = fill;

      polygon.Points = points;

    

    Polygon polygon = new Polygon() ;

    public GraphControl()
    
      InitializeComponent();

      var fill = Color.Clone();
      fill.Opacity = 0.2;
      polygon.Stroke = Color;
      polygon.Fill = fill;

      polygon.StrokeThickness = 1;

      canvas.Children.Add(polygon);
    
  

为了测试... MainWindows.xaml

  DataContext="Binding RelativeSource=RelativeSource Self"
  <StackPanel >
    <simpleGraph:GraphControl Data="Binding Data" Margin="4" />
    <simpleGraph:GraphControl Data="Binding Data" Margin="4" />
    <simpleGraph:GraphControl Data="Binding Data" Margin="4" />
    <simpleGraph:GraphControl Data="Binding Data" Margin="4" />
    <simpleGraph:GraphControl Data="Binding Data" Margin="4" />
    <simpleGraph:GraphControl Data="Binding Data" Margin="4" />
    <simpleGraph:GraphControl Data="Binding Data" Margin="4" />
  </StackPanel>

MainWindows.xaml.cs

private Random random = new Random();

public List<double> Data  get; set; 

public MainWindow()

  InitializeComponent();

  var temp = new List<double>();
  for (int i = 0; i < 100; i++)
  
    temp.Add(random.NextDouble() * 100);
  
  Data = new List<double>(temp);
  RaisePropertyChanged("Data");

  Timer timer = new Timer(1);
  timer.Elapsed += (sender, args) =>
  
    UpdateData();
  ;
  timer.Start();


private void UpdateData()

  var data = Data.ToArray();
  ShiftLeft(data, 1);


  var value = random.NextDouble() * 100;
  data[99] = value;

  Data = new List<double>(data); 

  RaisePropertyChanged("Data");


public void ShiftLeft<T>(T[] array, int shifts)

  Array.Copy(array, shifts, array, 0, array.Length - shifts);
  Array.Clear(array, array.Length - shifts, shifts);


private void RaisePropertyChanged(string property)

  if (PropertyChanged != null)
    PropertyChanged(this, new PropertyChangedEventArgs(property));


public event PropertyChangedEventHandler PropertyChanged;

这很粗略,但希望对你有帮助

【讨论】:

【参考方案2】:

您不想调用InvalidateVisual(),因为这会导致 UI 重新布局非常缓慢。但是,您也不需要使用 DataBinding(尽管它是一个很好的解决方案)。

要使您尝试使用的方法更快,请创建一个绘图组“backingStore”来保存您的绘图命令。在您的 OnRender() 方法中,将 DrawingGroup 输出到 DrawingContext。然后,您可以随时更新 backingStore,您的视觉效果也会随之更新。将您现有的渲染代码移动到一个不同的“Render()”方法,该方法只是绘制到 backingStore。

代码如下:

DrawingGroup backingStore = new DrawingGroup();

protected override void OnRender(DrawingContext drawingContext)       
    base.OnRender(drawingContext);            

    Render(); // put content into our backingStore
    drawingContext.DrawDrawing(backingStore);


// I can call this anytime, and it'll update my visual drawing
// without ever triggering layout or OnRender()
private void Render()             
    var drawingContext = backingStore.Open();
    Render(drawingContext);
    drawingContext.Close();            

【讨论】:

以上是关于用户控件中的WPF高效绘图的主要内容,如果未能解决你的问题,请参考以下文章

WPF之InkCanvas控件

WPF 用户控件中的数据绑定

用户控件中的 WPF 将控件模板的内容设置为依赖属性的值

WPF之概述

从用户控件中导航WPF选项卡控件?

WPF 用户控件库中的 WCF 服务引用