C#: Windows Forms:什么可能导致Invalidate()无法重新绘制?

14

我正在使用Windows Forms。长期以来,pictureBox.Invalidate();函数可以使屏幕重新绘制。但是现在它不起作用了,我不确定原因。

this.worldBox = new System.Windows.Forms.PictureBox();
this.worldBox.BackColor = System.Drawing.SystemColors.Control;
this.worldBox.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.worldBox.Location = new System.Drawing.Point(170, 82);
this.worldBox.Name = "worldBox";
this.worldBox.Size = new System.Drawing.Size(261, 250);
this.worldBox.TabIndex = 0;
this.worldBox.TabStop = false;
this.worldBox.MouseMove += new 
    System.Windows.Forms.MouseEventHandler(this.worldBox_MouseMove);
this.worldBox.MouseDown += new 
    System.Windows.Forms.MouseEventHandler(this.worldBox_MouseDown);
this.worldBox.MouseUp += new 
    System.Windows.Forms.MouseEventHandler(this.worldBox_MouseUp);

在我的代码中调用以适当地绘制世界:

view.DrawWorldBox(worldBox, canvas, gameEngine.GameObjectManager.Controllers, 
    selectedGameObjects, LevelEditorUtils.PREVIEWS);

View.DrawWorldBox:

public void DrawWorldBox(PictureBox worldBox,
    Panel canvas,
    ICollection<IGameObjectController> controllers,
    ICollection<IGameObjectController> selectedGameObjects,
    IDictionary<string, Image> previews)
{
    int left = Math.Abs(worldBox.Location.X);
    int top = Math.Abs(worldBox.Location.Y);
    Rectangle screenRect = new Rectangle(left, top, canvas.Width, 
        canvas.Height);

    IDictionary<float, ICollection<IGameObjectController>> layers = 
        LevelEditorUtils.LayersOfControllers(controllers);
    IOrderedEnumerable<KeyValuePair<float, 
        ICollection<IGameObjectController>>> sortedLayers 
            = from item in layers
              orderby item.Key descending
              select item;

    using (Graphics g = Graphics.FromImage(worldBox.Image))
    {
        foreach (KeyValuePair<float, ICollection<IGameObjectController>> 
        kv in sortedLayers)
        {
            foreach (IGameObjectController controller in kv.Value)
            {
                // ...

                float scale = controller.View.Scale;
                float width = controller.View.Width;
                float height = controller.View.Height;
                Rectangle controllerRect = new 
                    Rectangle((int)controller.Model.Position.X,
                    (int)controller.Model.Position.Y,
                    (int)(width * scale),
                    (int)(height * scale));

                // cull objects that aren't intersecting with the canvas
                if (controllerRect.IntersectsWith(screenRect))
                {
                    Image img = previews[controller.Model.HumanReadableName];
                    g.DrawImage(img, controllerRect);
                }

                if (selectedGameObjects.Contains(controller))
                {
                    selectionRectangles.Add(controllerRect);
                }
            }
        }
        foreach (Rectangle rect in selectionRectangles)
        {
            g.DrawRectangle(drawingPen, rect);
        }
        selectionRectangles.Clear();
    }
    worldBox.Invalidate();
}

我在这里可能做错了什么?

3个回答

21

想要理解这一点,您需要对操作系统层面的工作原理有一定的了解。

Windows控件是响应WM_PAINT消息绘制的。当它们接收到此消息时,会绘制已被使无效的任何部分。可以使特定的控件或特定区域的控件失效,所有这些都是为了将需要重新绘制的量最小化。

最终,Windows会发现一些控件需要重绘,并向它们发出WM_PAINT消息。但只有在处理完所有其他消息后才会这样做,这意味着Invalidate不会强制立即重新绘制。Refresh从技术上讲应该,但并不总是可靠。(更新:这是因为Refreshvirtual,而且有些控件重写了这个方法并实现不正确)

有一种方法可以通过发出WM_PAINT消息来强制立即绘制,那就是Control.Update。所以如果您想强制立即重新绘制,您可以使用:

control.Invalidate();
control.Update();
无论其他操作如何,即使UI仍在处理消息,这将始终重绘控件。我认为它字面上使用的是SendMessage API而不是PostMessage,这强制进行同步绘画,而不是将其抛到长消息队列的末尾。

你注意到Refresh和Invalidate/Update之间的区别了吗?我甚至从来没有看到Refresh和普通的Invalidate之间有太大的区别。根据MSDN的说法,Update直接将WM_PAINT发送到窗口,而Invalidate则将消息放入应用程序队列中。在实践中,即使在高速动画中,我也从未看到这会产生任何可观察到的差异。 - MusiGenesis
5
我进行了快速测试,并发现使用RefreshInvalidate/Update得到了相同的行为。这使我感到困扰,因为我曾经非常肯定我观察到过不同。于是我用反编译工具打开它,发现 Control.Refresh 方法实际上是先执行一个 Invalidate,再执行一个 Update。然后我恍然大悟:Refresh 方法是“虚拟的”。所以有时候它无法正常工作,是因为控件的作者重写了它并弄乱了它。Refresh 方法应该在大多数情况下能够正常工作,但如果它不能正常工作,通常可以通过显式地调用Invalidate/Update方法来修复它。 - Aaronaught
@MusiGenesis:顺便说一下,如果你想观察InvalidateRefresh(或Invalidate/Update)之间的区别,只需在窗体上添加一个文本框,在事件处理程序中编写textBox1.BackColor = Color.Red,然后是Invalidate,接着是Thread.Sleep(1000) - 你会发现更新直到事件处理程序执行完毕才会发生。如果你正在进行长时间运行的操作并且没有使用BackgroundWorker或工作线程,你需要通过这种方式“强制”更新。 - Aaronaught
1
@Aaronaught:我写了一篇长篇回复,然后意识到我只是在无意义地惹人厌烦(就像我在这里的最初评论)。我只想说那个家伙正在使用一个“PictureBox”,所以他的问题很可能不是Refresh方法实现不正确。此外,我意识到我在动画中没有看到Refresh与Invalidate之间的差异的原因是我没有使用任何一种方法(因为“BitBlt”不需要它)。 - MusiGenesis
1
@MusiGenesis:我并不觉得这很烦人,如果我的回答中有技术错误,那么我想知道;小细节经常意味着一个可行的解决方案和持续的困惑之间的区别。在这种情况下,你是对的,这可能没有什么区别。不过,这里的答案可能会被很多人阅读,而“延迟失效”是一个相当普遍的问题;额外的上下文可能会帮助未来的某个人。 - Aaronaught
嗯,这对我有帮助。我一点都不知道Refresh和Invalidate有什么区别。我认为一般情况下,只有在UI被锁定的情况下才能明显区分它们的区别,而那样的情况本来就很糟糕。 - MusiGenesis

1

Invalidate() 只是“使无效”控件或窗体(标记为重新绘制),但不强制重绘。只有当应用程序在消息队列中没有更多消息要处理时,它才会被重新绘制。如果您想强制重绘,则可以使用 Refresh()


正确,但它永远不会被重新绘制。 - Nick Heiner

0

无效化 (Invalidate) 或者 刷新 (Refresh) 在这种情况下具有相同的作用,并且强制进行重绘(最终结果)。如果您从未看到任何重新绘制的内容,则表示在 DrawWorldBox 中没有任何东西被绘制,或者已经绘制的内容已经超出了 PictureBox 显示图像的范围。

请确保(使用断点、日志或按照您的喜好逐步执行代码),某些事物正在被添加到 selectionRectangles 中,并且其中至少一个矩形覆盖了 PictureBox 的可见部分。还要确保您正在使用的笔的颜色不恰巧与背景颜色相同。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接