С#: Windows Forms: что может заставить Invalidate() не перерисовывать?

Я использую 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();
}

Что я могу делать неправильно здесь?

Ответ 1

Чтобы понять это, вам нужно понять, как это работает на уровне ОС.

Элементы управления Windows отображаются в ответ на сообщение WM_PAINT. Когда они получают это сообщение, они рисуют ту часть, которая была признана недействительной. Конкретные элементы управления могут быть признаны недействительными, а определенные области элементов управления могут быть аннулированы, все это делается для сведения к минимуму количества перерисовок, выполненных.

В конце концов, Windows увидит, что некоторые элементы управления требуют перекраски и выдают им сообщения WM_PAINT. Но это происходит только после того, как все другие сообщения обработаны, а это означает, что Invalidate не вызывает немедленного перерисовки. Refresh технически необходимо, но не всегда надежно. ( ОБНОВЛЕНИЕ: Это связано с тем, что Refresh - virtual, и в дикой природе есть определенные элементы управления, которые переопределяют этот метод с неправильной реализацией.)

Существует один метод, который заставляет немедленную краску выдавать сообщение WM_PAINT, и это Control.Update. Поэтому, если вы хотите принудительно выполнить перерисовку, вы используете:

control.Invalidate();
control.Update();

Это всегда будет перерисовывать элемент управления, независимо от того, что еще происходит, даже если пользовательский интерфейс все еще обрабатывает сообщения. Буквально, я полагаю, что вместо PostMessage используется API SendMessage, который заставляет графику делать синхронно, а не бросать ее в конце длинной очереди сообщений.

Ответ 2

Invalidate() только "аннулирует" элемент управления или форму (маркирует его для перерисовки), но не вызывает перерисовки. Он будет перерисован, как только приложение обернется, чтобы повторно перерисовать, когда больше сообщений не обрабатывается в очереди сообщений. Если вы хотите принудительно перерисовать, вы можете использовать Refresh().

Ответ 3

Invalidate или Refresh будет делать то же самое в этом случае и принудительно выполнить перерисовку (в конечном итоге). Если вы не видите ничего перерисовывающегося (когда-либо), то это означает, что ничто не было нарисовано вообще в DrawWorldBox, или все, что было нарисовано, было снято с видимой части изображения PictureBox.

Убедитесь, что (используя точки останова или протоколирование или перешагивая код, как вы предпочитаете), что что-то существует, добавляется к selectionRectangles и что по крайней мере один из этих прямоугольников охватывает видимую часть PictureBox. Также убедитесь, что ручка, которую вы рисуете, не совпадает с цветом фона.