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

标签 wpf

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

  • 我没有尝试的:
  • 使用工具绘制3D(视口(viewport))绘制2D雷达;
  • ...

  • 所以,就是这样。我在乞求帮助。

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

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

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

    最佳答案

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

    无需线程或与调度程序混淆。该事件将在每个新帧之前自动触发,类似于 HTML 的 requestAnimationFrame() .

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

    有一个complete example可在 MSDN 上找到。

    关于wpf - 如何实现每 16 毫秒流畅的 UI 更新?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23889294/

    相关文章:

    c# - 如何隐藏 WPF 网格溢出(如 CSS 溢出 :hidden)

    c# - 字符串的 WPF 本地化 - 在不同项目之间共享?

    c# - WCF 在没有 IIS 的情况下与多个客户端通信

    wpf - Windows 讲述人读取窗口中所有控件的名称(甚至是隐藏的控件)

    c# - 从其他进程向 WPF 单例应用程序发送数据

    c# - CefSharp.wpf Web 浏览器导航事件 (C# .Net)

    wpf - 我的数据绑定(bind)在控件加载之前没有被评估,我在控件加载后得到属性更改事件,为什么?

    wpf - 将文本框的文本属性绑定(bind)到 MainWindow-WPF 上定义的变量

    WPF : InputBindings on a StackPanel

    wpf - 从单独的线程访问 ViewModel 属性