GDI+油漆队列问题
同志们)我发现多线程应用程序中 Invalidate 方法有一些有趣的行为。我希望你能帮助我解决问题...
我在尝试同时使不同的控件无效时遇到了麻烦:虽然它们是相同的,但一个成功地重新绘制了自己,但另一个则不然。
这是一个示例:我有一个表单(MysticForm),上面有两个面板(SlowRenderPanel)。每个面板都有一个计时器,并以 50ms 为周期调用 Invalidate() 方法。在 OnPaint 方法中,我在面板中央绘制当前 OnPaint 调用的编号。但请注意,在 OnPaint 方法中调用 System.Threading.Thread.Sleep(50) 来模拟长时间绘制过程。
所以问题是,第一个添加的面板比另一个面板更频繁地重新绘制自身。
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace WindowsFormsApplication1 {
static class Program {
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MysticForm());
}
}
public class MysticForm : Form {
public SlowRenderPanel panel1;
public SlowRenderPanel panel2;
public MysticForm() {
// add 2 panels to the form
Controls.Add(new SlowRenderPanel() { Dock = DockStyle.Left, BackColor = Color.Red, Width = ClientRectangle.Width / 2 });
Controls.Add(new SlowRenderPanel() { Dock = DockStyle.Right, BackColor = Color.Blue, Width = ClientRectangle.Width / 2 });
}
}
public class SlowRenderPanel : Panel {
// synchronized timer
private System.Windows.Forms.Timer timerSafe = null;
// simple timer
private System.Threading.Timer timerUnsafe = null;
// OnPaint call counter
private int counter = 0;
// allows to use one of the above timers
bool useUnsafeTimer = true;
protected override void Dispose(bool disposing) {
// active timer disposal
(useUnsafeTimer ? timerUnsafe as IDisposable : timerSafe as IDisposable).Dispose();
base.Dispose(disposing);
}
public SlowRenderPanel() {
// anti-blink
DoubleBuffered = true;
// large font
Font = new Font(Font.FontFamily, 36);
if (useUnsafeTimer) {
// simple timer. starts in a second. calls Invalidate() with period = 50ms
timerUnsafe = new System.Threading.Timer(state => { Invalidate(); }, null, 1000, 50);
} else {
// safe timer. calls Invalidate() with period = 50ms
timerSafe = new System.Windows.Forms.Timer() { Interval = 50, Enabled = true };
timerSafe.Tick += (sender, e) => { Invalidate(); };
}
}
protected override void OnPaint(PaintEventArgs e) {
string text = counter++.ToString();
// simulate large bitmap drawing
System.Threading.Thread.Sleep(50);
SizeF size = e.Graphics.MeasureString(text, Font);
e.Graphics.DrawString(text, Font, Brushes.Black, new PointF(Width / 2f - size.Width / 2f, Height / 2f - size.Height / 2f));
base.OnPaint(e);
}
}
}
调试信息:
1) 每个面板都有一个 bool 字段 useUnsafeTime (默认设置为 true),允许使用 System.Windows.Forms.Timer (false) 代替 System.Threading.Timer (true)。在第一种情况(System.Windows.Forms.Timer)中,一切正常。删除 OnPaint 中的 System.Threading.Sleep 调用也可以使执行正常。
2) 将计时器间隔设置为 25 毫秒或更小,根本可以防止第二个面板重新绘制(当用户不调整表单大小时)。
3)使用System.Windows.Forms.Timer导致速度提高
4)强制控件进入同步上下文(Invoke)没有意义。我的意思是 Invalidate(invalidateChildren = false) 是“线程安全的”,并且在不同的上下文中可能有不同的行为
5) 在这两个计时器的 IL 比较中没有发现任何有趣的东西...它们只是使用不同的 WinAPI 函数来设置和删除计时器(Threading.Timer 的 AddTimerNative、DeleteTimerNative;Windows.Forms.Timer 的 SetTimer、KillTimer)和 Windows.Forms.Timer 使用 NativeWindow 的 WndProc 方法来上升 Tick 事件
我在应用程序中使用了类似的代码片段,不幸的是没有办法使用 System.Windows.Forms.Timer)我使用两个面板的长时间多线程图像渲染,并在每个面板上完成渲染后调用 Invalidate 方法...
如果有人可以帮助我了解背后发生的不同情况,那就太好了场景以及如何解决问题。
PS 有趣的行为不是吗?=)
comrades) I've found some interesting behavior of Invalidate method in multithreaded applications. I hope you could help me with a problem...
I experience troubles while trying to invalidate different controls at one time: while they're identical, one succesfully repaints itself, but another - not.
Here is an example: I have a form (MysticForm) with two panels (SlowRenderPanel) on it. Each panel has a timer and with a period of 50ms Invalidate() method is called. In OnPaint method I draw number of current OnPaint call in the centre of panel. But notice that in OnPaint method System.Threading.Thread.Sleep(50) is called to simulate long time draw procedure.
So the problem is that the panel added first repaints itself much more often than another one.
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace WindowsFormsApplication1 {
static class Program {
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MysticForm());
}
}
public class MysticForm : Form {
public SlowRenderPanel panel1;
public SlowRenderPanel panel2;
public MysticForm() {
// add 2 panels to the form
Controls.Add(new SlowRenderPanel() { Dock = DockStyle.Left, BackColor = Color.Red, Width = ClientRectangle.Width / 2 });
Controls.Add(new SlowRenderPanel() { Dock = DockStyle.Right, BackColor = Color.Blue, Width = ClientRectangle.Width / 2 });
}
}
public class SlowRenderPanel : Panel {
// synchronized timer
private System.Windows.Forms.Timer timerSafe = null;
// simple timer
private System.Threading.Timer timerUnsafe = null;
// OnPaint call counter
private int counter = 0;
// allows to use one of the above timers
bool useUnsafeTimer = true;
protected override void Dispose(bool disposing) {
// active timer disposal
(useUnsafeTimer ? timerUnsafe as IDisposable : timerSafe as IDisposable).Dispose();
base.Dispose(disposing);
}
public SlowRenderPanel() {
// anti-blink
DoubleBuffered = true;
// large font
Font = new Font(Font.FontFamily, 36);
if (useUnsafeTimer) {
// simple timer. starts in a second. calls Invalidate() with period = 50ms
timerUnsafe = new System.Threading.Timer(state => { Invalidate(); }, null, 1000, 50);
} else {
// safe timer. calls Invalidate() with period = 50ms
timerSafe = new System.Windows.Forms.Timer() { Interval = 50, Enabled = true };
timerSafe.Tick += (sender, e) => { Invalidate(); };
}
}
protected override void OnPaint(PaintEventArgs e) {
string text = counter++.ToString();
// simulate large bitmap drawing
System.Threading.Thread.Sleep(50);
SizeF size = e.Graphics.MeasureString(text, Font);
e.Graphics.DrawString(text, Font, Brushes.Black, new PointF(Width / 2f - size.Width / 2f, Height / 2f - size.Height / 2f));
base.OnPaint(e);
}
}
}
Debug info:
1) Each panel has a bool field useUnsafeTime (set to true by default) which allows using System.Windows.Forms.Timer (false) insted of System.Threading.Timer (true). In the first case (System.Windows.Forms.Timer) everything works fine. Removing System.Threading.Sleep call in OnPaint also makes execution fine.
2) Setting timer interval to 25ms or less prevents second panel repainting at all (while user doesn't resize the form).
3) Using System.Windows.Forms.Timer leads to speed increasement
4) Forcing control to enter synchronization context (Invoke) doesn't make sense. I mean that Invalidate(invalidateChildren = false) is "thread-safe" and could possibly have different behavior in diffenent contexts
5) Nothing interesting found in IL comparison of these two timers... They just use different WinAPI functions to set and remove timers (AddTimerNative, DeleteTimerNative for Threading.Timer; SetTimer, KillTimer for Windows.Forms.Timer), and Windows.Forms.Timer uses NativeWindow's WndProc method for rising Tick event
I use a similar code snippet in my application and unfortunately there is no way of using System.Windows.Forms.Timer) I use long-time multithreaded image rendering of two panels and Invalidate method is called after rendering is completed on each panel...
That would be great if someone could help me to understand what's different happening behind the scenes and how to solve the problem.
P.S. Interesting behavior isn't it?=)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
很好地演示了当您在后台线程上使用控件或窗体的成员时会出现什么问题。 Winforms 通常会捕获此问题,但 Invalidate() 方法代码中存在错误。像这样更改它:
引发异常。
另一个面板速度较慢,因为它的许多 Invalidate() 调用都被绘制事件取消了。这样做的速度足够慢。经典的穿线比赛。您不能从工作线程调用 Invalidate(),同步计时器是一个明显的解决方案。
Nice demonstration of what goes wrong when you use members of a control or form on a background thread. Winforms usually catches this but there's a bug in the Invalidate() method code. Change it like this:
to trip the exception.
The other panel is slower because lots of its Invalidate() calls are getting canceled by the paint event. Which is just slow enough to do so. Classic threading race. You cannot call Invalidate() from a worker thread, the synchronous timer is an obvious solution.
Invalidate() 使客户区或矩形 ( InvalidateRect() ) 无效并“告诉”Windows 下次 Windows 进行绘制;刷新我,画我。但它不会引起或调用绘制消息。要强制绘制事件,您必须在 Invalidate 调用后强制窗口进行绘制。这并不总是需要的,但有时这是必须要做的。
要强制绘制,您必须使用 Update() 函数。 “导致控件重新绘制其客户区域内的无效区域。”
在这种情况下你必须同时使用两者。
编辑:避免此类问题的常用技术是将所有绘画例程和任何相关内容保留在单个(通常是主)线程或计时器中。该逻辑可以在其他地方运行,但实际进行绘制调用的地方应该全部在一个线程或计时器中。
这是在游戏和 3D 模拟中完成的。
华泰
Invalidate() invalidates the client area or rectangle ( InvalidateRect() ) and "tells" Windows that next time Windows paints; refresh me, paint me. But it does not cause or invoke a paint message. To force a paint event, you must force windows to paint after an Invalidate call. This is not always needed, but sometimes it's what has to be done.
To force a paint you have to use the Update() function. "Causes the control to redraw the invalidated regions within its client area."
You have to use both in this case.
Edit: A common technique to avoid these kinds of problems is keep all your paint routines and anything related in a single (generally main) thread or timer. The logic can run elsewhere but where the actual paint calls are made should all be in one thread or timer.
This is done in games and 3D simulations.
HTH