UltimateListCtrl in a dialog inhibits display of custom control in another window

I have an application that uses a number of non-modal dialogs, one of which contains a custom control derived from wx.Control. The custom control displays several pieces of text, plus small bitmaps and some lines and had been working fine until I recently made a change to two other dialogs in the application.

Previously those two other dialogs had been using a wx.ListCtrl to display some data. I wanted to be able to highlight some of the data in different colours, so I changed the dialogs to use an UltimateListCtrl instead. That conversion worked OK and the dialogs now display the values in the required colours.

However, I have noticed that once I have displayed one of the dialogs containing an UltimateListCtrl, the custom control remains blank when it is updated. This effect persists even after the dialog containing the UltimateListCtrl has been destroyed! The only way to get the custom control working again is to restart the application. Running the application in debug shows that its Draw() method is being called, but the commands it executes appear to have no visible effects.

Below is a much simplified application that demonstrates the problem. If you click on the “Inc Number” button, the number shown in the CustomCtrl will increment.

If you click on the “ListCtrl Dialog” button a dialog containing a wx.ListCtrl will appear. This will not prevent the CustomCtrl from updating when you click the “Inc Number” button.

If you click on the “ULC Dialog” button a dialog containing an UltimateListCtrl will appear. If you then click on the “Inc Number” button, the CustomCtrl will go blank and not display the number value.

I am only able to test this on Python 3.10.6 + wxPython 4.2.0 gtk3 (phoenix) wxWidgets 3.2.0 + Linux Mint 21.1.

Does anyone have any ideas why this is happening? I wondered if the UltimateListCtrl is changing some global property that is affecting the behaviour of the custom control.

import sys
import wx
from wx.lib.agw import ultimatelistctrl as ULC

DATA = (
    ("One",   "Alpha"),
    ("Two",   "Bravo"),
    ("Three", "Charlie"),
    ("Four",  "Delta"),
    ("Five",  "Echo"),
)

class CustomCtrl(wx.Control):
    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
                 size=wx.DefaultSize, style=wx.BORDER_NONE,
                 validator=wx.DefaultValidator, name="CustomCtrl"):
        wx.Control.__init__(self, parent, id, pos, size, style, validator, name)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
        self.Bind(wx.EVT_PAINT,            self.OnPaint)
        self.number = 0

    def Draw(self, dc):
        print("In Draw() number = %d" % self.number)
        bg_brush = wx.Brush(wx.WHITE, wx.BRUSHSTYLE_SOLID)
        dc.SetBackground(bg_brush)
        dc.Clear()
        width, height = dc.GetSize()
        dc.SetTextForeground(wx.BLACK)
        xc = width // 2
        yc = height // 2
        text = str(self.number)
        tw, th = dc.GetTextExtent(text)
        x = xc - tw // 2
        y = yc - th // 2
        dc.DrawText(text, x, y)

    def incNumber(self):
        self.number += 1
        self.Refresh()
        self.Update()

    def OnEraseBackground(self, event):
        pass

    def OnPaint(self, _event):
        dc = wx.BufferedPaintDC(self)
        self.Draw(dc)


class ListCtrlDialog(wx.Dialog):
    def __init__(self, parent):
        wx.Dialog.__init__(self, parent, title="LC Dialog")
        self.SetSize((400, 200))
        main_sizer = wx.BoxSizer(wx.VERTICAL)
        style = wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES
        self.list_ctrl = wx.ListCtrl(self, style=style)
        main_sizer.Add(self.list_ctrl, 1, wx.EXPAND, 0)
        self.SetSizer(main_sizer)
        self.Layout()

        self.list_ctrl.InsertColumn(0, "Number")
        self.list_ctrl.InsertColumn(1, "Code Word")
        for n, w in DATA:
            idx = self.list_ctrl.InsertItem(sys.maxsize, n)
            self.list_ctrl.SetItem(idx, 1, w)

        self.Bind(wx.EVT_CLOSE, self.OnClose)

    def OnClose(self, _evt):
        self.Destroy()


class ULCDialog(wx.Dialog):
    def __init__(self, parent):
        wx.Dialog.__init__(self, parent, title="ULC Dialog")
        self.SetSize((400, 200))
        main_sizer = wx.BoxSizer(wx.VERTICAL)
        agw_style = wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES
        self.ulc = ULC.UltimateListCtrl(self, agwStyle=agw_style)
        main_sizer.Add(self.ulc, 1, wx.EXPAND, 0)
        self.SetSizer(main_sizer)
        self.Layout()

        self.ulc.InsertColumn(0, "Number")
        self.ulc.InsertColumn(1, "Code Word")
        for n, w in DATA:
            idx = self.ulc.InsertStringItem(sys.maxsize, n)
            self.ulc.SetStringItem(idx, 1, w)

        self.Bind(wx.EVT_CLOSE, self.OnClose)

    def OnClose(self, _evt):
        self.Destroy()


class TestFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, title="Test Frame")
        self.SetSize((250, 200))
        main_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.custom_ctrl = CustomCtrl(self, size=(50, 50))
        main_sizer.Add(self.custom_ctrl, 0, wx.ALL, 16)
        button_sizer = wx.BoxSizer(wx.VERTICAL)
        self.inc_num_button = wx.Button(self, label="Inc Number")
        button_sizer.Add(self.inc_num_button, 0, 0, 0)
        self.lc_dialog_button = wx.Button(self, label="ListCtrl Dialog")
        button_sizer.Add(self.lc_dialog_button, 0, wx.TOP, 8)
        self.ulc_dialog_button = wx.Button(self, label="ULC Dialog")
        button_sizer.Add(self.ulc_dialog_button, 0, wx.TOP, 8)
        main_sizer.Add(button_sizer, 0, wx.TOP, 8)
        self.SetSizer(main_sizer)
        self.Layout()
        self.Bind(wx.EVT_BUTTON, self.OnIncNumber, self.inc_num_button)
        self.Bind(wx.EVT_BUTTON, self.OnULCDialog, self.ulc_dialog_button)
        self.Bind(wx.EVT_BUTTON, self.OnLCDialog,  self.lc_dialog_button)
        self.ulc_dialog = None
        self.lc_dialog = None

    def OnIncNumber(self, _evt):
        self.custom_ctrl.incNumber()

    def OnLCDialog(self, _evt):
        if not self.lc_dialog:
            self.lc_dialog = ListCtrlDialog(self)
            self.lc_dialog.SetPosition((300, 0))
            self.lc_dialog.Show()
        else:
            self.lc_dialog.Destroy()

    def OnULCDialog(self, _evt):
        if not self.ulc_dialog:
            self.ulc_dialog = ULCDialog(self)
            self.ulc_dialog.SetPosition((300, 220))
            self.ulc_dialog.Show()
        else:
            self.ulc_dialog.Destroy()


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

I can reproduce this on Windows, with both wxPython 4.2.0 and 4.1.1.

It looks like setting a pen works around it. I added

        dc.SetPen(wx.Pen('CYAN', 1, wx.PENSTYLE_SOLID))

to your program, right after SetTextForeground, and the problem went away.

I can’t explain why. It’s not like anything is actually painted in the cyan pen. But ULC does use wx.PENSTYLE_TRANSPARENT at one point, and perhaps that somehow sticks?

Thanks for your suggestion, Anders. Unfortunately it didn’t make any difference when run on linux.

Another possible workaround is to change wx.BufferedPaintDC to a plain wx.PaintDC.

1 Like

Thanks Anders, that does work on linux.

I also noticed that several examples in the wxPython demo use a wx.AutoBufferedPaintDC. You just need to call self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) in the custom control’s __init__ method. It also runs OK when a dialog containing a ULC has been displayed.

I have now tried both of these types of paint DC in my real application and they both fix the conflict with the ULC.

One of the dialogs in my real application uses several of the custom controls which can be animated using a timer to show a sequence of plotted weather reports at a number of sites. Running the timer at its fastest speed I can’t see any signs of flickering with either the wx.PaintDC or wx.AutoBufferedPaintDC, so it doesn’t seem to matter which I choose.