如何每16毫秒实现流畅的UI更新?

时间:2014-05-27 12:20:37

标签: wpf

我正试图创造一种雷达。雷达是VisualCollection,由360个DrawingVisual(代表雷达波束)组成。雷达放在Viewbox上。

class Radar : FrameworkElement
{
    private VisualCollection visuals;
    private Beam[] beams = new Beam[BEAM_POSITIONS_AMOUNT]; // all geometry calculation goes here

    public Radar()
    {
        visuals = new VisualCollection(this);

        for (int beamIndex = 0; beamIndex < BEAM_POSITIONS_AMOUNT; beamIndex++)
        {
            DrawingVisual dv = new DrawingVisual();
            visuals.Add(dv);
            using (DrawingContext dc = dv.RenderOpen())
            {
                dc.DrawGeometry(Brushes.Black, null, beams[beamIndex].Geometry);
            }
        }

        DrawingVisual line = new DrawingVisual();
        visuals.Add(line);

        // DISCRETES_AMOUNT is about 500
        this.Width = DISCRETES_AMOUNT * 2;
        this.Height = DISCRETES_AMOUNT * 2;
    }

    public void Draw(int beamIndex, Brush brush)
    {
        using (DrawingContext dc = ((DrawingVisual)visuals[beamIndex]).RenderOpen())
        {
            dc.DrawGeometry(brush, null, beams[beamIndex].Geometry);
        }
    }

    protected override Visual GetVisualChild(int index)
    {
        return visuals[index];
    }

    protected override int VisualChildrenCount
    {
        get { return visuals.Count; }
    }
}

每个DrawingVisual都有DrawingContext.DrawGeometry(画笔,笔,几何)的预先计算的几何体。 Pen为null,brush为LinearGradientBrush,大约有500个GradientStop。刷子每隔几毫秒就会更新一次,例如16 ms就可以了。而那就是变得迟钝的东西。这就是整体逻辑。

在MainWindow()构造函数中,我创建雷达并启动后台线程:

    private Radar radar;

    public MainWindow()
    {
        InitializeComponent();

        radar = new Radar();
        viewbox.Child = radar;

        Thread t = new Thread(new ThreadStart(Run));
        t.Start();
    }

在Run()方法中有一个无限循环,其中生成随机画笔,调用Dispatcher.Invoke()并设置16 ms的延迟:

    private int beamIndex = 0;
    private Random r = new Random();
    private const int turnsPerMinute = 20;
    private static long delay = 60 / turnsPerMinute * 1000 / (360 / 2);
    private long deltaDelay = delay;

    public void Run()
    {
        int beginTime = Environment.TickCount;
        while (true)
        {
            GradientStopCollection gsc = new GradientStopCollection(DISCRETES_AMOUNT);
            for (int i = 1; i < Settings.DISCRETES_AMOUNT + 1; i++)
            {
                byte color = (byte)r.Next(255);
                gsc.Add(new GradientStop(Color.FromArgb(255, 0, color, 0), (double)i / (double)DISCRETES_AMOUNT));
            }

            LinearGradientBrush lgb = new LinearGradientBrush(gsc);
            lgb.StartPoint = Beam.GradientStarts[beamIndex];
            lgb.EndPoint = Beam.GradientStops[beamIndex];
            lgb.Freeze();

            viewbox.Dispatcher.Invoke(new Action( () =>
            {
                radar.Draw(beamIndex, lgb);
            }));

            beamIndex++;
            if (beamIndex >= BEAM_POSITIONS_AMOUNT)
            {
                beamIndex = 0;
            }

            while (Environment.TickCount - beginTime < delay) { }
            delay += deltaDelay;
        }
    }

每次Invoke()调用它都会执行一个简单的操作:dc.DrawGeometry(),它在当前beamIndex下重绘光束。但是,有时似乎在UI更新之前,雷达.Draw()被调用几次,而不是每16毫秒绘制1个波束,它每32-64毫秒绘制2-4个波束。这令人不安。我真的想要实现平稳运动。我需要一个光束在每个确切的时间段内绘制。不是这个随机的东西。这是我到目前为止所尝试的清单(没有任何帮助):

  • 将雷达置于画布中;
  • 使用Task,BackgroundWorker,Timer,自定义Microtimer.dll并设置不同的线程优先级;
  • 使用不同的实现延迟的方法:Environment.TickCount,DateTime.Now.Ticks,Stopwatch.ElapsedMilliseconds;
  • 将LinearGradientBrush更改为预定义的SolidColorBrush;
  • 使用BeginInvoke()代替Invoke()并更改Dispatcher Priorities;
  • 使用InvalidateVisuals()和丑陋的DoEvents();
  • 使用BitmapCache,WriteableBitmap和RenderTargetBitmap(使用DrawingContext.DrawImage(位图);
  • 使用360 Polygon对象而不是360 DrawingVisuals。这样我可以避免使用Invoke()方法。每个多边形的Polygon.FillProperty绑定到ObservableCollection,并实现了INotifyPropertyChanged。如此简单的代码行{brushCollection [beamIndex] =(新创建和冻结画笔)}导致多边形FillProperty更新,UI被重绘。但仍然没有顺利的运动;
  • 可能还有一些我无法忘记的变通方法。

我没有尝试:

  • 使用工具绘制3D(视口)以绘制2D雷达;
  • ...

所以,就是这样。我求救。

编辑:这些滞后与PC资源无关 - 没有延迟,雷达每秒可以做大约5个完整的圆圈(移动非常快)。很可能是关于多线程/ UI / Dispatcher或其他我尚未理解的东西。

EDIT2:附加.exe文件,以便您可以看到实际发生的情况:https://dl.dropboxusercontent.com/u/8761356/Radar.exe

EDIT3: DispatcherTimer(DispatcherPriority.Render)也没有帮助。

3 个答案:

答案 0 :(得分:3)

对于流畅的WPF动画,你应该使用 CompositionTarget.Rendering事件。

不需要线程或搞乱调度员。该事件将在每个新帧之前自动触发,类似于HTML的requestAnimationFrame()

在更新你的WPF场景的事件中,你已经完成了!

MSDN上有complete example

答案 1 :(得分:0)

您可以使用WPF Performance Suite检查一些图形瓶颈:

http://msdn.microsoft.com/es-es/library/aa969767(v=vs.110).aspx

Perforator是向您展示性能问题的工具。也许您使用的是低性能VGA卡?

答案 2 :(得分:0)

while (Environment.TickCount - beginTime < delay) { }
        delay += deltaDelay;

上面的序列会阻塞线程。改为使用&#34;等待Task.Delay(...)&#34;它没有像对应的Thread.Sleep(...)那样阻塞线程。