同志们)我在多线程应用程序中发现了一些有趣的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.Whreading.Timer(true)的System.Windows.Forms.Timer(false)。在第一种情况下(System.Windows.Forms.Timer)一切正常。在OnPaint中删除System.Threading.Sleep调用也可以使执行正常。
2)将计时器间隔设置为25毫秒或更短时间会阻止第二个面板重新绘制(而用户不会调整表单大小)。
3)使用System.Windows.Forms.Timer导致速度提升
4)强制控制进入同步上下文(Invoke)没有意义。我的意思是Invalidate(invalidateChildren = false)是“线程安全的”,并且可能在不同的上下文中有不同的行为
5)在这两个计时器的IL比较中没有发现任何有趣的东西......他们只是使用不同的WinAPI函数来设置和删除计时器(AddTimerNative,DeleteTimerNative for Threading.Timer; SetTimer,KillTimer for Windows.Forms.Timer),以及Windows.Forms.Timer使用NativeWindow的WndProc方法来上升Tick事件
我在我的应用程序中使用了类似的代码片段,遗憾的是无法使用System.Windows.Forms.Timer)我使用两个面板的长时间多线程图像渲染,并且在每个面板上完成渲染后调用Invalidate方法面板...
如果有人可以帮助我理解幕后发生的不同以及如何解决问题,那将是很棒的。
P.S。有趣的行为不是吗?=)
答案 0 :(得分:1)
Invalidate()使客户区或矩形无效(InvalidateRect())并“告诉”Windows 下次 Windows绘制;刷新我,画我。但它不会导致或调用绘制消息。要强制执行绘制事件,必须在Invalidate调用后强制窗口绘制。这并不总是需要的,但有时它必须要做的事情。
要强制绘画,您必须使用更新()功能。 “使控件重绘其客户区域内的无效区域。”
在这种情况下你必须同时使用它们。
编辑:避免这类问题的常用技巧是将所有绘制例程和任何相关内容保存在单个(通常是主要的)线程或计时器中。逻辑可以在别处运行,但实际的绘制调用应该在一个线程或计时器中。
这是在游戏和3D模拟中完成的。
HTH
答案 1 :(得分:1)
很好地演示了在后台线程上使用控件或表单的成员时出现的问题。 Winforms通常会捕获这个,但Invalidate()方法代码中存在一个错误。改变它:
timerUnsafe = new System.Threading.Timer(state => { Invalidate(true); }, null, 1000, 50);
绊倒异常。
另一个面板速度较慢,因为很多Invalidate()调用都被paint事件取消了。这样做的速度很慢。经典的穿线比赛。你不能从工作线程调用Invalidate(),同步计时器是一个明显的解决方案。