【问题标题】:GDI+ Paint Queue ProblemGDI+ 绘制队列问题
【发布时间】:2011-04-01 08:29:34
【问题描述】:

同志们)我在多线程应用程序中发现了 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) 每个面板都有一个布尔字段 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 事件

我在我的应用程序中使用了类似的代码 sn-p,不幸的是无法使用 System.Windows.Forms.Timer)我使用两个面板的长时间多线程图像渲染,渲染完成后调用 Invalidate 方法在每个面板上...

如果有人可以帮助我了解幕后发生的不同情况以及如何解决问题,那就太好了。

附:有趣的行为不是吗?=)

【问题讨论】:

    标签: c# .net gdi onpaint


    【解决方案1】:

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

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

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


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

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

    HTH

    【讨论】:

    • JustBoo:它会导致更奇怪的事情发生:表单没有响应,第一个面板重绘约 10-20 次,然后另一个等等。我认为对我们来说最重要的是了解两个计时器之间的真正区别,因为在 System.Windows.Forms.Timer 系统工作完美的情况下......使用相同的“Invalidate()”代码......
    • 但是有什么比在两个面板上以一定频率绘制两个位图更容易的...我已经有了位图并且仅在 Graphics.DrawImageUnscaled 方法中发出低性能...所以我该如何更改代码只是为了同时调用两者的重绘? =(
    • IMO,在表单级别有一个计时器,它会根据您想要的间隔引发您自己的“动画事件”。在这一事件/通话中绘制所有内容。如果间隔足够快,每秒 18 到 30 次,看起来应该没问题。这还有一个好处是,您的所有绘图调用现在都集中在一个地方,以便于维护。
    • 查看 cmets 以获得 Hans Passant 的答案。我已经尝试过这个......同样的事情
    • 嘿,等一下。我可能误会了什么。我以为你很想用计时器来制作动画。如果您想显示照片,只需执行我们在 form_load() 中讨论的操作。 :-) 或者我还缺少什么。
    【解决方案2】:

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

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

    触发异常。

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

    【讨论】:

    • 我简化了“任务”...只需禁用面板计时器并将这行代码添加到 MysticForm ctor new System.Threading.Timer(state => { Invoke(new MethodInvoker(() => { 无效(true); })); }, null, 1000, 50);我们不应该看到问题,但它仍然存在。不应该有竞争条件。我说的对吗?=)
    • 呃,你调用的速度比 UI 线程跟得上的快。你最终会死于OOM。当您关闭表单时,还有另一个线程竞赛。尝试完成这项工作有什么意义吗?你真正想做什么。
    • (如果我理解正确的话)我有两个大位图(1000x500),想同时在两个面板上绘制它们(例如 - 2 张照片,宽度绑定),但时间无效调用正在另一个线程中定义......(例如 - 每 50 毫秒它提供照片(新)随机宽度)
    • 好吧,这毫无意义。这很危险,尽管在这种情况下您可能不会陷入僵局陷阱。试图让 UI 线程以比绘制图像更快的速度更新图像是行不通的。你有一个人的眼睛看着这个,一个同步定时器就可以了。通过以 32bppPArgb 格式存储图像来获得更快的图像更新,它的绘制速度比其他任何一种都要快 10 倍。并且预先调整图像大小,在 Paint 事件中重新调整它们的成本很高。
    • 虽然如果你不断改变它们的大小,预先调整它们的大小可能不会有用。
    猜你喜欢
    • 2011-06-01
    • 2011-11-18
    • 1970-01-01
    • 1970-01-01
    • 2011-09-03
    • 1970-01-01
    • 2012-07-30
    • 2017-05-31
    • 1970-01-01
    相关资源
    最近更新 更多