如何实现每 16 毫秒平滑的 UI 更新? [英] How to achieve smooth UI updates every 16 ms?

查看:25
本文介绍了如何实现每 16 毫秒平滑的 UI 更新?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试创建某种雷达.雷达是由 360 个 DrawingVisual(代表雷达波束)组成的 VisualCollection.雷达放置在 Viewbox 上.

I am trying to create sort of a radar. Radar is VisualCollection that consists of 360 DrawingVisual's (which represent radar beams). Radar is placed on 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(brush, pen, geometry) 预先计算了几何图形.Pen 为 null,brush 是一个 LinearGradientBrush,大约有 500 GradientStops.画笔每隔几毫秒更新一次,在本例中假设为 16 毫秒.这就是变得滞后的原因.这是整体逻辑.

Each DrawingVisual has precalculated geometry for DrawingContext.DrawGeometry(brush, pen, geometry). Pen is null and brush is a LinearGradientBrush with about 500 GradientStops. The brush gets updated every few milliseconds, lets say 16 ms for this example. And that is what gets laggy. Here goes the overall logic.

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

In MainWindow() constructor I create the radar and start a background thread:

    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 毫秒的延迟:

In Run() method there is an infinite loop, where random brush is generated, Dispatcher.Invoke() is called and a delay for 16 ms is set:

    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 更新之前,radar.Draw() 被调用了几次,而不是每 16 毫秒绘制 1 个波束,而是每 32-64 毫秒绘制 2-4 个波束.这令人不安.我真的很想实现流畅的运动.我需要在每个确切的时间段内绘制一根光束.不是这种随机的东西.这是我迄今为止尝试过的列表(没有任何帮助):

Every Invoke() call it performs one simple thing: dc.DrawGeometry(), which redraws the beam under current beamIndex. However, sometimes it seems, like before UI updates, radar.Draw() is called few times and instead of drawing 1 beam per 16 ms, it draws 2-4 beams per 32-64 ms. And it is disturbing. I really want to achieve smooth movement. I need one beam to get drawn per exact period of time. Not this random stuff. This is the list of what I have tried so far (nothing helped):

  • 在 Canvas 中放置雷达;
  • 使用Task、BackgroundWorker、Timer、自定义Microtimer.dll并设置不同的线程优先级;
  • 使用不同的延迟实现方式:Environment.TickCount、DateTime.Now.Ticks、Stopwatch.ElapsedMilliseconds;
  • 将 LinearGradientBrush 更改为预定义的 SolidColorBrush;
  • 使用 BeginInvoke() 代替 Invoke() 并更改调度程序优先级;
  • 使用 InvalidateVisuals() 和丑陋的 DoEvents();
  • 使用 BitmapCache、WriteableBitmap 和 RenderTargetBitmap(使用 DrawingContext.DrawImage(bitmap);
  • 使用 360 Polygon 对象而不是 360 DrawingVisuals.这样我就可以避免使用 Invoke() 方法.每个多边形的 Polygon.FillProperty 绑定到 ObservableCollection,并实现了 INotifyPropertyChanged.如此简单的代码行 {brushCollection[beamIndex] = (new created and freeze Brush)} 导致多边形 FillProperty 更新和 UI 被重绘.但还是没有流畅的动作;
  • 可能还有一些我可以忘记的小变通方法.

我没有尝试过的:

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

原来如此.我在乞求帮助.

So, this is it. I am begging for help.

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

These lags are not about PC resources - without delay radar can do about 5 full circles per second (moving pretty fast). Most likely it is something about multithread/UI/Dispatcher or something else that I am yet to understand.

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

Attaching an .exe file so you could see what is actually going on: https://dl.dropboxusercontent.com/u/8761356/Radar.exe

DispatcherTimer(DispatcherPriority.Render) 也没有帮助.

DispatcherTimer(DispatcherPriority.Render) did not help aswell.

推荐答案

要获得流畅的 WPF 动画,您应该使用CompositionTarget.Rendering 事件.

For smooth WPF animations you should make use of the CompositionTarget.Rendering event.

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

No need for a thread or messing with the dispatcher. The event will automatically be fired before each new frame, similar to HTML's requestAnimationFrame().

如果更新您的 WPF 场景,您就大功告成了!

In the event update your WPF scene and you're done!

有一个完整示例 可在 MSDN 上找到.

There is a complete example available on MSDN.

这篇关于如何实现每 16 毫秒平滑的 UI 更新?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆