1

同志们)我在多线程应用程序中发现了 Invalidate 方法的一些有趣行为。我希望你能帮我解决一个问题...

我在尝试一次使不同的控件无效时遇到了麻烦:虽然它们是相同的,但一个成功地重新绘制了自己,但另一个 - 没有。

这是一个示例:我有一个表单(MysticForm),上面有两个面板(SlowRenderPanel)。每个面板都有一个计时器,并以 50 毫秒为周期调用 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) insted of System.Threading.Timer (true)。在第一种情况下(System.Windows.Forms.Timer)一切正常。删除 OnPaint 中的 System.Threading.Sleep 调用也可以使执行正常。

2) 将计时器间隔设置为 25 毫秒或更短,完全可以防止第二个面板重绘(而用户不调整表单大小)。

3)使用 System.Windows.Forms.Timer 导致速度增加

4)强制控制进入同步上下文(调用)没有意义。我的意思是 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有趣的行为不是吗?=)

4

2 回答 2

1

Invalidate() 使客户区或矩形 ( InvalidateRect() ) 无效并“告诉” Windows下次Windows 绘制;刷新我,画我。但它不会导致或调用绘制消息。要强制绘制事件,您必须在 Invalidate 调用之后强制窗口绘制。这并不总是需要,但有时这是必须要做的。

要强制绘制,您必须使用Update()函数。“导致控件重绘其客户区域内的无效区域。”

在这种情况下,您必须同时使用两者。


编辑:避免此类问题的一种常用技术是将所有绘画例程和任何相关的内容保存在单个(通常是主)线程或计时器中。逻辑可以在其他地方运行,但实际调用绘制的地方应该都在一个线程或计时器中。

这是在游戏和 3D 模拟中完成的。

高温高压

于 2010-08-19T20:37:55.737 回答
1

很好地演示了在后台线程上使用控件或表单的成员时出现的问题。Winforms 通常会捕捉到这一点,但 Invalidate() 方法代码中有一个错误。像这样改变它:

 timerUnsafe = new System.Threading.Timer(state => { Invalidate(true); }, null, 1000, 50);

跳闸异常。

另一个面板速度较慢,因为它的许多 Invalidate() 调用都被绘制事件取消了。这样做的速度足够慢。经典的穿线比赛。您不能从工作线程调用 Invalidate(),同步计时器是一个明显的解决方案。

于 2010-08-19T21:04:01.253 回答