Problem updating widget immediately with Layout and Update

I have a problem updating a widget immediately with Layout and Update. I present a test program that demonstrates the problem.

Overview

The test program lays out a few widgets vertically. First a button that triggers a change in layout, then a text widget surrounded by two panels to more clearly indicate the size of the text widget. It looks like this:

Tests program.

When the button is clicked, the size of the text widget is increased followed by a call to Layout and Update on the frame. I would expect the frame to redraw immediately at this point, but it doesn't. At least not correctly.

Test program

The structure of the test program looks like this:

  1. testlayout.py
import wx
import time

class TestFrame(wx.Frame):

    <<TestFrame>>

class Text(wx.Panel):

    <<Text>>

if __name__ == "__main__":
    app = wx.App()
    frame = TestFrame()
    frame.Show()
    app.MainLoop()

If you prefer the read the complete test program, go to Complete test program.

The __init__ method of the test frame lays out the widgets using a vertical sizer and hooks up the click event.

  1. testlayout.py
  2. TestFrame
def __init__(self):
    wx.Frame.__init__(self, None)
    self.button = wx.Button(self, label="increase text size")
    self.button.Bind(wx.EVT_BUTTON, self._on_increase_size_click)
    self.top = wx.Panel(self, size=(-1, 20))
    self.top.SetBackgroundColour("yellow")
    self.text = Text(self)
    self.bottom = wx.Panel(self, size=(-1, 20))
    self.bottom.SetBackgroundColour("pink")
    self.Sizer = wx.BoxSizer(wx.VERTICAL)
    self.Sizer.Add(
        self.button,
        border=5, proportion=0, flag=wx.EXPAND|wx.ALL
    )
    self.Sizer.Add(
        self.top,
        border=5, proportion=0, flag=wx.EXPAND|wx.ALL
    )
    self.Sizer.Add(
        self.text,
        border=5, proportion=0, flag=wx.EXPAND|wx.ALL
    )
    self.Sizer.Add(
        self.bottom,
        border=5, proportion=0, flag=wx.EXPAND|wx.ALL
    )

The click event handler increases the size of the text widget and tries to do an immediate update. The sleep is there to see if the update is delayed or not.

  1. testlayout.py
  2. TestFrame
def _on_increase_size_click(self, event):
    print("")
    print("Click")
    self.text.increase_size()
    self.Layout()
    print("Update")
    self.Update()
    time.sleep(2)
    print("Update done")

The __init__ method of the text widget sets an initial size and hooks up paint and size events.

  1. testlayout.py
  2. Text
def __init__(self, parent):
    wx.Panel.__init__(self, parent)
    self.min_h = 0
    self.increase_size()
    self.Bind(wx.EVT_PAINT, self._on_paint)
    self.Bind(wx.EVT_SIZE, self._on_size)

The increase_size method increases the height and sets a new min size.

  1. testlayout.py
  2. Text
def increase_size(self):
    self.min_h += 10
    self.SetMinSize((-1, self.min_h))

The paint event handler draws a bunch of lines of text and also prints the rectangles that were invalidated.

  1. testlayout.py
  2. Text
def _on_paint(self, event):
    print("repaint text")
    upd = wx.RegionIterator(self.GetUpdateRegion())
    while upd.HaveRects():
        print("  {}".format(upd.GetRect()))
        upd.Next()
    dc = wx.PaintDC(self)
    y = 0
    for x in range(20):
        line = "line {}".format(x)
        dc.DrawText(line, 5, y)
        y += dc.GetTextExtent(line)[1]
    event.Skip()

The size event just prints the new size.

  1. testlayout.py
  2. Text
def _on_size(self, event):
    print("resize text {}".format(event.GetSize()))
    event.Skip()

Results

Here is the output of a button click:

Click
resize text (396, 20)
Update
repaint text
  (0, 0, 396, 10)
Update done
repaint text
  (0, 0, 396, 20)
  • First, the click event hander is entered.
  • The text widget properly gets a resize event. (My guess is as a result of the Layout call.)
  • Then Update is called.
  • A repaint of the text widget is then done, but the invalidated rectangle is only 10px high. The new height is 20.
  • After the sleep call, a repaint of the text widget is done again, now with the correct rectangle.

So in practice, nothing is shown on the screen until after the sleep call.

It seems to me that the Layout call depends on something happening by following events. Adding a Refresh call immediately after Layout does not seem to have any effect.

Why does the immediate repaint don't get the correct size?

Complete test program

import wx
import time

class TestFrame(wx.Frame):

    def __init__(self):
        wx.Frame.__init__(self, None)
        self.button = wx.Button(self, label="increase text size")
        self.button.Bind(wx.EVT_BUTTON, self._on_increase_size_click)
        self.top = wx.Panel(self, size=(-1, 20))
        self.top.SetBackgroundColour("yellow")
        self.text = Text(self)
        self.bottom = wx.Panel(self, size=(-1, 20))
        self.bottom.SetBackgroundColour("pink")
        self.Sizer = wx.BoxSizer(wx.VERTICAL)
        self.Sizer.Add(
            self.button,
            border=5, proportion=0, flag=wx.EXPAND|wx.ALL
        )
        self.Sizer.Add(
            self.top,
            border=5, proportion=0, flag=wx.EXPAND|wx.ALL
        )
        self.Sizer.Add(
            self.text,
            border=5, proportion=0, flag=wx.EXPAND|wx.ALL
        )
        self.Sizer.Add(
            self.bottom,
            border=5, proportion=0, flag=wx.EXPAND|wx.ALL
        )

    def _on_increase_size_click(self, event):
        print("")
        print("Click")
        self.text.increase_size()
        self.Layout()
        print("Update")
        self.Update()
        time.sleep(2)
        print("Update done")

class Text(wx.Panel):

    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        self.min_h = 0
        self.increase_size()
        self.Bind(wx.EVT_PAINT, self._on_paint)
        self.Bind(wx.EVT_SIZE, self._on_size)

    def increase_size(self):
        self.min_h += 10
        self.SetMinSize((-1, self.min_h))

    def _on_paint(self, event):
        print("repaint text")
        upd = wx.RegionIterator(self.GetUpdateRegion())
        while upd.HaveRects():
            print("  {}".format(upd.GetRect()))
            upd.Next()
        dc = wx.PaintDC(self)
        y = 0
        for x in range(20):
            line = "line {}".format(x)
            dc.DrawText(line, 5, y)
            y += dc.GetTextExtent(line)[1]
        event.Skip()

    def _on_size(self, event):
        print("resize text {}".format(event.GetSize()))
        event.Skip()

if __name__ == "__main__":
    app = wx.App()
    frame = TestFrame()
    frame.Show()
    app.MainLoop()

I should add the version I’ve tested with:

$ python
Python 2.7.15 (default, May  9 2018, 11:32:33) 
[GCC 7.3.1 20180130 (Red Hat 7.3.1-2)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import wx
>>> wx.version()
'3.0.2.0 gtk3 (classic)'

$ python3
Python 3.6.5 (default, Apr  4 2018, 15:09:05) 
[GCC 7.3.1 20180130 (Red Hat 7.3.1-2)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import wx
>>> wx.version()
'4.0.1 gtk3 (phoenix)'

Why do you think that the repaint should get the full size? By default it will only repaint the part that was previously covered or unavailable.
If you need a full refresh, add self.text.Refresh().

Regards,
Dietmar

P.S.: That’s also stated in the documentation for Update:
https://wxpython.org/Phoenix/docs/html/wx.Window.html#wx.Window.Update

I would suggest not to call Update at all. Just call Refresh and wait for the Paint event.

As I stated, doing a Refresh before did not seem to have any effect.

My thinking was that Layout would implicitly invalidate the changed region.

I know I can wait for the paint to happen after Layout. But in this particular case I was interested in doing it immediately.

OK, I see. What you see is platform dependent.

On Windows the Refresh call fixed things from partial paint to full paint.

My experience with Update is not the best, as Update+ClientDC do not work well with Wayland. Using Refresh and handling the Paint event is more reliable. So even if you find a workaround now for using Update, it may fail on other platforms.

Yes. Probably the Update is a bit platform dependent.

But it works in my environment if I don’t resize anything. The area is repainted. But I can not get it to work when sizes changes.

I’ve used Typometer to measure delay of repaints. If I did Update the repaint happened much faster. I don’t remember exactly. But it was something like 15ms faster. Which might be noticeable. That’s why I’m interested in doing immediate repaints.

  • Refresh simply tells the system what part of the widget you want to have repainted. The given rectangle (or whole widget) is added to the current invalidated area, if any, and then the Refresh call returns. When the next EVT_PAINT event happens for whatever reason then that invalidated area is painted.

  • Update simply causes an EVT_PAINT event to be sent immediately, and processed before the Update call returns, instead of waiting for the next naturally occurring paint event to happen.

Almost all of time using Update is not needed, and sometimes it can be a little detrimental. For example, on OSX there is a pipeline of operations to move display details from all of the running applications to the screen. This is highly optimized and works to keep the screen updated and appearing very smooth and crisp and keeping the applications efficient. When an application does something like drawing to a DC without a paint event, or forcing a paint event at the wrong moment, then that pipeline is interrupted to do that drawing and then has to be restarted again to redo processing what was interrupted before.

So, in other words, in almost all cases using Refresh or RefreshRect when needed, and letting the drawing happen in naturally occurring paint events is usually the best choice. The platforms are already highly optimized for this and have been fine tuned for decades to make it the best it can be.

Now for some comments about the example code above:

  1. You should never Skip a paint event once a wxPaintDC has been created. On Windows this tells the system that the window painting has not been processed, but since there are still invalidated areas on the window then it sends the paint event again immediately, and again, and again…

  2. Calling self.Update() in the frame is not needed, because none of it needs to be repainted. Only its child widgets have changed. If portions of the frame become visible or are damaged as part of moving the children around in the Layout then there will be a paint event to take care of that.

  3. The invalidated rectangle after the resize in the Text widget is only 10px because that is the only “newly exposed” portion of the Text widget. If you want the whole widget to be redrawn after a resize you can either call the Text widget’s Refresh or create it with the wx.FULL_REPAINT_ON_RESIZE style flag.

2 Likes

You need to update what the widgets are on, not the widgets themselves.
Use the Layout() or Update() functions
Example: aui.Update() #Update all widgets in AUI Manager

It worked for me.