Search code examples
c#.netwinformsinvalidationonpaint

invalidate multiple rectangles or regions


I'm having problems with Invalidate() because it calls OnPaint before I'm ready... Invalidate(new Rectangle(x, y, width, height)) works fine when I need to repaint one and only one area, but what I need to do is create a collection of rectangles to invalidate and then use Update() method to repaint all invalidated zones. I'm a bit lost between Invalidate() and Update(): how to use them and what's the clean way to do so.

I need to achieve this because I'm working on cellular automatons such as "Langton's ant" or "Conway's Game of life". Small grids are not a problem but with big sized grids (700x500) the painting is a very important issue.

So my question is how do I invalidate X rectangles without calling OnPaint at each invalidation and then call OnPaint to refresh only the specified areas (X rectangles would = at least hundreds, thousands for sure)?


Solution

  • Calling Invalidate method does not immediately raise the Paint event. It only sets the specified region as invalid and queues a paint event. Next call to Invalidate only adds up the region to previously invalidated region. It puts a new Paint event only if there is no Paint event in the queue.

    From the Remarks section of the Invalidate method:

    Calling the Invalidate method does not force a synchronous paint; to force a synchronous paint, call the Update method after calling the Invalidate method. When this method is called with no parameters, the entire client area is added to the update region.


    More explanation:

    Windows Forms technology is a wrapper on Win32's user32 library. To understand under the hoods of the paint event, you need to know how user32 works.

    Message Queue:

    Every process in windows has a message queue. When anything happens to a window that belongs to the process, Windows pushes an event into the message queue of the process. There is a message loop in every application, that extracts the messages from the queue (by calling GetMessage()) and dispatches the message (calls appropriate function, called a Window Procedure, by calling DispatchMessage()). So the messages are being processed one after another. It means that when a message is being processed, no other message can be processed.
    That's why when you do a time-consuming operation in a form (withoud starting a new thread), the application stops respongind: You are stuck in processing one message (for example Click event of a button), so the application cannot process other messages (mouse event, painting events, etc.).

    In Windows Forms, Application.Run method runs the message loop of the application. Messages are passed to Control.WndProc method and this method determines the appropriate OnXxxx method to call (OnKeyPress, OnMouseMove, OnResize, etc.), and that method raises the respective event (KeyPress, MouseMove, Rezie, etc).

    WM_PAINT:

    When a program's window needs to be painted (for example when it is first shown or it is restored from minimized state), Windows queues a WM_PAINT messages into the message queue, only if there is no unprocessed WM_PAINT for the window in the message queue. Also the message loop only extract the WM_PAINT message from the queue only when there is no other message in the queue. Qoute from the WM_PAINT page in MSDN:

    GetMessage returns the WM_PAINT message when there are no other messages in the application's message queue, and DispatchMessage sends the message to the appropriate window procedure.

    In Windows Forms, WM_PAINT translates to OnPaint method which raises the Paint event.

    When you call Invalidate (which calls the Win32 InvalidateRect function) several times in a single method, the Paint event still didn't get the chance to be raised. The current event that is being processed must be finished, also other messages that are sent in the mean time should be processed, then the Paint event is raised.

    Please follow the links in the answer and read them thoroughly.