Question

I have a custom progress bar–like view that basically consists of two sizers side by side: the one on the left has a red background and the one on the right has a light gray background. By adjusting the relative sizes of the sizers I can indicate different percentages. For whatever reason—maybe because this Window consists only of sizers, with no “substantial” elements—the progress bar is not visible when its containing Frame first appears. When the Frame is resized by any amount in any direction, the progress bar suddenly pops into view, with the correct size and position and everything.

Below is a minimal code sample. It doesn’t quite demonstrate the problem I’m describing: when run as-is, the progress bar will appear correctly.

If I remove the wx.EXPAND flag from either Add() call in TestWindow.init, however, then when the window first appears the progress bar will either be the wrong size or completely invisible. (Resizing the window causes the bar to become visible and/or take the correct size.) When the run_on_main_thread decorator is not applied to update_sizes, though, the bar is always displayed correctly right off the bat, regardless of whether the wx.EXPAND flag was included!

I’m not sure why my efforts toward thread safety have caused this drawing issue. Can anyone suggest a way to make the progress bar display correctly from the beginning, while still ensuring that update_sizes will only run on the main thread? For example, can I trigger an EVT_SIZE on the window so that it will refresh itself without any user interaction?

I’m using Python 2.7.5 and wxPython 2.9.5.0 on OS X 10.9.2.

Code

from functools import wraps
from math import floor
import wx

def run_on_main_thread(fn):
    """Decorator. Forces the function to run on the main thread.

    Any calls to a function that is wrapped in this decorator will return
    immediately; the return value of such a function is not available.

    """
    @wraps(fn)
    def deferred_caller(*args, **kwargs):
        wx.CallAfter(fn, *args, **kwargs)

    return deferred_caller


class PercentageBar(wx.Window):
    def __init__(self, parent, percentage=0.0):
        wx.Window.__init__(self, parent)

        border_color = '#000000'
        active_color = '#cc0000'
        inactive_color = '#d3d7cf'
        height = 24

        self.percentage = percentage

        self.SetBackgroundColour(border_color)

        self.active_rectangle = wx.Panel(self, size=(-1, height))
        self.active_rectangle.SetBackgroundColour(active_color)

        self.inactive_rectangle = wx.Panel(self, size=(-1, height))
        self.inactive_rectangle.SetBackgroundColour(inactive_color)

        self.sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.SetSizer(self.sizer)
        self.update_sizes()

    @run_on_main_thread
    def update_sizes(self):
        self.sizer.Clear(False)

        if self.percentage != 0.0 and self.percentage != 1.0:
            active_flags = wx.EXPAND | (wx.ALL & ~wx.RIGHT)
        else:
            active_flags = wx.EXPAND | wx.ALL

        self.sizer.Add(self.active_rectangle, floor(1000 * self.percentage),
                active_flags, 1)

        self.sizer.Add(self.inactive_rectangle, floor(1000 * (1.0 - self.percentage)),
                wx.EXPAND | wx.ALL, 1)

        if self.percentage == 0.0:
            self.active_rectangle.Hide()
            self.inactive_rectangle.Show()
        elif self.percentage == 1.0:
            self.active_rectangle.Show()
            self.inactive_rectangle.Hide()
        else:
            self.active_rectangle.Show()
            self.inactive_rectangle.Show()

        self.sizer.Layout()
        self.Refresh()


class TestWindow(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, title='Test')

        vertical_sizer = wx.BoxSizer(wx.VERTICAL)
        vertical_sizer.Add((1, 1), 1)
        vertical_sizer.Add(PercentageBar(self, percentage=0.33), 2, wx.EXPAND)
        vertical_sizer.Add((1, 1), 1)

        horizontal_sizer = wx.BoxSizer(wx.HORIZONTAL)
        horizontal_sizer.Add((1, 1), 1)
        horizontal_sizer.Add(vertical_sizer, 10, wx.EXPAND)
        horizontal_sizer.Add((1, 1), 1)

        self.SetSizer(horizontal_sizer)


if __name__ == '__main__':
    app = wx.App()
    window = TestWindow(None)
    window.Show()
    app.MainLoop()
Was it helpful?

Solution

Your example is working as expected for me with wxPython 3.0, but your symptom is a common one. Usually it means that the initial size event is happening before the sizer has been set, so the default EVT_SIZE handler doesn't yet have a sizer to use for the layout. As soon as the window is resized then the sizer is used to do the layout and all is as you expected it to be before.

To deal with this problem you just need to trigger the built-in layout features after the frame and its content have been created. (Usually at the end of __init__) Since the auto-layout is tied to the size events, then you can use the frame's SendSizeEvent method to let it have one. Or you can just explicily call the Layout method yourself.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top