【问题标题】:How to achieve smooth UI updates every 16 ms?如何实现每 16 毫秒流畅的 UI 更新?
【发布时间】:2014-05-27 12:20:36
【问题描述】:

我正在尝试创建一种雷达。 Radar 是由 360 DrawingVisual(代表雷达波束)组成的 VisualCollection。雷达放置在 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 为空,画笔是具有大约 500 个 GradientStops 的 LinearGradientBrush。画笔每隔几毫秒更新一次,在这个例子中假设是 16 毫秒。这就是变得滞后的原因。这是整体逻辑。

在 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()并设置16ms的延迟:

    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 个波束。这令人不安。我真的很想实现流畅的运动。我需要一个光束来绘制每个确切的时间段。不是这种随机的东西。这是我迄今为止尝试过的列表(没有任何帮助):

  • 在 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 多边形对象而不是 360 绘图视觉对象。这样我可以避免使用 Invoke() 方法。每个多边形的 Polygon.FillProperty 都绑定到 ObservableCollection,并实现了 INotifyPropertyChanged。如此简单的代码行 {brushCollection[beamIndex] = (new created and freeze brush)} 导致多边形 FillProperty 更新并且 UI 被重绘。但还是没有流畅的动作;
  • 可能还有一些我可以忘记的小解决方法。

我没有尝试的:

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

所以,就是这样。我正在寻求帮助。

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

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

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

【问题讨论】:

    标签: wpf


    【解决方案1】:

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

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

    如果更新您的 WPF 场景,您就完成了!

    在 MSDN 上有一个complete example

    【讨论】:

    • 这个!这就是我需要的。如此简单......很棒的解决方案,非常感谢。
    【解决方案2】:

    您可以使用 WPF 性能套件检查一些图形瓶颈:

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

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

    【讨论】:

    • 这些滞后很可能与 VGA 卡无关,因为如果我消除延迟,程序运行速度非常快,每秒大约 5 个整圈。我也在不同的电脑上用完全不同的硬件试过它。不过,我感谢您的帮助,并将检查 WPF 性能套件。
    • WPF Performance Suite 显示我的应用程序以稳定的 61-63 FPS、124-126 rect/s、零软件和硬件 IRT 每帧和 4.68 MB 视频内存使用率运行。
    【解决方案3】:
    while (Environment.TickCount - beginTime < delay) { }
            delay += deltaDelay;
    

    上面的序列阻塞了线程。改用“await Task.Delay(...)”,它不会像对应的 Thread.Sleep(...) 那样阻塞线程。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-03-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多