如何实现每 16 毫秒平滑的 UI 更新? [英] How to achieve smooth UI updates every 16 ms?
问题描述
我正在尝试创建某种雷达.雷达是由 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屋!