Mike Driscoll's generic wizard from "wxPython Recipes" not refreshing on 4.1.1

I have been having problems with wxPython ‘4.1.1 gtk3 (phoenix) wxWidgets 3.1.5’ on Linux. Specifically with a custom wizard (i.e. no wx.adv.Wizard) I created for my software (relax). To double check this, I pulled out the ‘Generic Wizard’ code from Mike Driscoll’s book “wxPython Recipes”:

import wx

"""From wxPython Recipes by Mike Driscoll, page 66-68."""

class WizardPage(wx.Panel):
    """A Simple wizard page"""

    def __init__(self, parent, title=None):
        """Constructor"""
        wx.Panel.__init__(self, parent)

        sizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(sizer)

        if title:
            title = wx.StaticText(self, -1, title)
            title.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD))
            sizer.Add(title, 0, wx.ALIGN_CENTER|wx.ALL, 5)
            sizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.ALL, 5)


class WizardPanel(wx.Panel):
    """"""

    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent=parent)
        self.pages = []
        self.page_num = 0

        self.mainSizer = wx.BoxSizer(wx.VERTICAL)
        self.panelSizer = wx.BoxSizer(wx.VERTICAL)
        btnSizer = wx.BoxSizer(wx.HORIZONTAL)

        # add prev/next buttons
        self.prevBtn = wx.Button(self, label="Previous")
        self.prevBtn.Bind(wx.EVT_BUTTON, self.onPrev)
        #btnSizer.Add(self.prevBtn, 0, wx.ALL|wx.ALIGN_RIGHT, 5)
        btnSizer.Add(self.prevBtn, 0, wx.ALL, 5)

        self.nextBtn = wx.Button(self, label="Next")
        self.nextBtn.Bind(wx.EVT_BUTTON, self.onNext)
        #btnSizer.Add(self.nextBtn, 0, wx.ALL|wx.ALIGN_RIGHT, 5)
        btnSizer.Add(self.nextBtn, 0, wx.ALL, 5)

        # finish layout
        self.mainSizer.Add(self.panelSizer, 1, wx.EXPAND)
        self.mainSizer.Add(btnSizer, 0, wx.ALIGN_RIGHT)
        self.SetSizer(self.mainSizer)

    def addPage(self, title=None):
        """"""
        panel = WizardPage(self, title)
        self.panelSizer.Add(panel, 2, wx.EXPAND)
        self.pages.append(panel)
        if len(self.pages) > 1:
            # hide all panels after the first one
            panel.Hide()
            self.Layout()
            self.Update()

    def onNext(self, event):
        """"""
        pageCount = len(self.pages)
        if pageCount-1 != self.page_num:
            self.pages[self.page_num].Hide()
            self.page_num += 1
            self.pages[self.page_num].Show()
            self.panelSizer.Layout()
        else:
            print("End of pages!")

        if self.nextBtn.GetLabel() == "Finish":
            # close the app
            self.GetParent().Close()

        if pageCount == self.page_num+1:
            # change label
            self.nextBtn.SetLabel("Finish")

    def onPrev(self, event):
        """"""
        pageCount = len(self.pages)
        if self.page_num-1 != -1:
            self.pages[self.page_num].Hide()
            self.page_num -= 1
            self.pages[self.page_num].Show()
            self.panelSizer.Layout()
        else:
            print("You're already on the first page!")

class MainFrame(wx.Frame):
    """"""

    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, None, title="Generic Wizard", size=(800,600))

        self.panel = WizardPanel(self)
        self.panel.addPage("Page 1")
        self.panel.addPage("Page 2")
        self.panel.addPage("Page 3")

        self.Show()

if __name__ == "__main__":
    app = wx.App(False)
    frame = MainFrame()
    app.MainLoop()

This clearly shows the problem. The first page of the wizard shows “Page 1”, the second only shows part of the text of “Page 2” so that it looks more like “Par”. Is this a known issue with 4.1.1? Or is there a new way to force a refresh/update/layout of the panels?

Cheers,
Edward

I am seeing the same problem with the Wizard examples in the wxPython demo application.

(Tested on Python 3.8.10 + wxPython 4.1.1 gtk3 (phoenix) wxWidgets 3.1.5 + Linux Mint 20.3)

It looks like this issue was raised back in 2019.

That’s exactly the same issue! I’ve refactored a ~10k SLOC set of files now about 3 times to try to fix this issue that I thought was my own fault (over-complexity with shared wx elements stored in a singlton and hyper-nested wx.BoxSizers), but now I can stop that torture :grinning: Thanks!

I guess the next step will now be more complicated than fixing my own broken code :thinking:

Regards,
Edward

There is also an issue raised on GitHub for wxWidgets which describes a similar problem (although they refer to it being caused by bold font, whereas in the wxPython cases it seems to be caused by changing the font size).

https://github.com/wxWidgets/wxWidgets/issues/19053

In Mike’s example, if I change the WizardPage class as shown below, then the title doesn’t get truncated on pages 2 and 3. However, the small size doesn’t look very impressive!

class WizardPage(wx.Panel):
    """A Simple wizard page"""

    def __init__(self, parent, title=None):
        """Constructor"""
        wx.Panel.__init__(self, parent)

        sizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(sizer)

        if title:
            title = wx.StaticText(self, -1, title)
            font = title.GetFont()
            font.SetWeight(wx.FONTWEIGHT_BOLD)
            title.SetFont(font)
            sizer.Add(title, 0, wx.ALIGN_CENTER|wx.ALL, 5)
            sizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.ALL, 5)

Interesting! Maybe there is a ugly way to work around this bug by taking a longer walk around wx. Through experimentation, I found the following avoids the layout and refresh issue:

$ diff -u  gui/generic_wizard_orig.py gui/generic_wizard.py
--- gui/generic_wizard_orig.py  2022-07-07 18:51:24.737529020 +0200
+++ gui/generic_wizard.py       2022-07-07 18:55:54.185893252 +0200
@@ -16,8 +16,9 @@
         if title:
             title = wx.StaticText(self, -1, title)
             title.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD))
-            sizer.Add(title, 0, wx.ALIGN_CENTER|wx.ALL, 5)
+            sizer.Add(title, 1, wx.EXPAND|wx.ALL, 5)
             sizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.ALL, 5)
+            sizer.AddStretchSpacer()
 
 
 class WizardPanel(wx.Panel):

The title text is however not centered and the vertical layout is not ideal. But it might show that the issue is something to do with the wx.BoxSizer not being refreshed when in hidden panel that has Show() called?

Regards,
Edward

Below is an alternative workaround that writes text on a bitmap, which is then used to create a wx.StaticBitmap. The title then appears correctly on all the pages.

def getTextBitmap(width, height, background, text, font_size=12, text_colour=wx.BLACK):
    """"""
    bitmap = wx.Bitmap(width, height)
    dc = wx.MemoryDC(bitmap)
    dc.SetBackground(wx.Brush(background))
    dc.Clear()
    font = dc.GetFont()
    font.SetPointSize(font_size)
    font.SetWeight(wx.FONTWEIGHT_BOLD)
    dc.SetFont(font)
    dc.SetTextForeground(text_colour)
    tw, th = dc.GetTextExtent(text)
    dc.DrawText(text, (width - tw) // 2, (height - th) // 2)
    del dc
    return bitmap


class WizardPage(wx.Panel):
    """A Simple wizard page"""

    def __init__(self, parent, title=None):
        """Constructor"""
        wx.Panel.__init__(self, parent)

        sizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(sizer)

        if title:
            bg = self.GetBackgroundColour()
            bitmap = getTextBitmap(100, 32, bg, title, font_size=18, text_colour=wx.BLACK)
            title_bmp = wx.StaticBitmap(self, -1, bitmap)
            sizer.Add(title_bmp, 0, wx.ALIGN_CENTER|wx.ALL, 5)
            sizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.ALL, 5)

Regards,
Richard

This is a great idea. I might use that for my own project. I would use dc.GetTextExtent(title) after calling dc.SetFont() to automatically determine the size to make the wx.Bitmap.

However the layout problem is still an issue for me as I have complicated dialogs/wizards. Here is a window that I use as both a simple dialog and as part of complex wizards:

This looks good as it is not part of a wizard. But when used as part of a wizard and not the first page, it ends up as:

I can fix the text at the top with a bitmap, and the text for the options, but the alignment of the elements fails.

Regards,
Edward

I might try to translate the generic wizard code into C++ and see if the issue is in wx itself.

This issue is clearly in wxWidgets. Here is my quick translation of Mike Driscoll’s generic wizard from Python to C++:

#include <iostream>
#include <vector>
#include <wx/statline.h>
#include <wx/wxprec.h>
 
#ifndef WX_PRECOMP
    #include <wx/wx.h>
#endif
 
/*
 * Python to C++ translation of the generic wizard from "wxPython Recipes" by Mike Driscoll, page 66-68.
 */

// A Simple wizard page.
class WizardPage : public wxPanel
{
public:
    WizardPage(wxPanel *parent, const wxString &titleText);

private:
    wxBoxSizer *sizer;
    wxStaticText *title;
    wxStaticLine *line;
};


class WizardPanel : public wxPanel
{
public:
    WizardPanel(wxFrame *parent);
    void addPage(const wxString &title);

private:
    int page_num = 0;
    int pageCount;
    std::vector<WizardPage*> pages;
    wxBoxSizer *btnSizer, *mainSizer, *panelSizer;
    wxButton *prevBtn, *nextBtn;
    WizardPage *panel;

    void onNext(wxEvent &event);
    void onPrev(wxEvent &event);
};


class MyApp : public wxApp
{
public:
    virtual bool OnInit();
};


class MainFrame : public wxFrame
{
public:
    MainFrame(const wxString &title, const wxSize &size);
};
 


WizardPage::WizardPage(wxPanel *parent, const wxString &titleText)
    : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
{
    sizer = new wxBoxSizer(wxVERTICAL);
    this->SetSizer(sizer);

    if (title != NULL) {
        title = new wxStaticText(this, -1, titleText);
        title->SetFont(wxFont(18, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD));
        sizer->Add(title, 0, wxALIGN_CENTER|wxALL, 5);
        line = new wxStaticLine(this, -1);
        sizer->Add(line, 0, wxEXPAND|wxALL, 5);
    }
}


WizardPanel::WizardPanel(wxFrame *parent)
    : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
{
    mainSizer = new wxBoxSizer(wxVERTICAL);
    panelSizer = new wxBoxSizer(wxVERTICAL);
    btnSizer = new wxBoxSizer(wxHORIZONTAL);

    // add prev/next buttons.
    prevBtn = new wxButton(this, wxID_ANY, wxT("Previous"));
    prevBtn->Bind(wxEVT_BUTTON, &WizardPanel::onPrev, this);
    btnSizer->Add(prevBtn, 0, wxALL, 5);

    nextBtn = new wxButton(this, wxID_ANY, wxT("Next"));
    nextBtn->Bind(wxEVT_BUTTON, &WizardPanel::onNext, this);
    btnSizer->Add(nextBtn, 0, wxALL, 5);

    // finish layout
    mainSizer->Add(panelSizer, 1, wxEXPAND);
    mainSizer->Add(btnSizer, 0, wxALIGN_RIGHT);
    this->SetSizer(mainSizer);
}


void WizardPanel::addPage(const wxString &title)
{
    panel = new WizardPage(this, title);
    panelSizer->Add(panel, 2, wxEXPAND);
    pages.push_back(panel);
    if (pages.size() > 1) {
        std::cout << "page >1" << std::endl;
        panel->Hide();
        this->Layout();
        this->Update();
    }
}


void WizardPanel::onNext(wxEvent &event)
{
    pageCount = pages.size();
    if (pageCount-1 != page_num) {
        pages[page_num]->Hide();
        page_num += 1;
        pages[page_num]->Show();
        panelSizer->Layout();
    } else {
        std::cout << "End of pages!" << std::endl;
    }

    if (nextBtn->GetLabel() == wxT("Finish")) {
        this->GetParent()->Close();
    }

    if (pageCount == page_num+1) {
        nextBtn->SetLabel(wxT("Finish"));
    }
}


void WizardPanel::onPrev(wxEvent &event)
{
    pageCount = pages.size();
    if (page_num-1 != -1) {
        pages[page_num]->Hide();
        page_num -= 1;
        pages[page_num]->Show();
        panelSizer->Layout();
    } else {
        std::cout << "You're already on the first page!" << std::endl;
    }
}


MainFrame::MainFrame(const wxString& title, const wxSize& size)
    : wxFrame(NULL, wxID_ANY, title, wxDefaultPosition, size)
{
    WizardPanel *panel = new WizardPanel(this);
    panel->addPage(wxT("Page 1"));
    panel->addPage(wxT("Page 2"));
    panel->addPage(wxT("Page 3"));

    this->Show();
}


bool MyApp::OnInit()
{
    MainFrame *frame = new MainFrame(wxT("Generic Wizard"), wxSize(800, 600));
    frame->Show(true);
    return true;
}
 
wxIMPLEMENT_APP(MyApp);

I compile this (in a file called generic_wizard.cpp) with:

g++ -v -c `wx-config --cxxflags` generic_wizard.cpp
g++ -v -o generic_wizard generic_wizard.o `wx-config --libs`

The first page of the wizard looks like this:

And the second page is:

1 Like

Any advice on where upstream I should report this?

Cheers,
Edward

Cheers! I’ve finally reported this GTK3 wizard bug upstream on the wxWidgets GitHub bug tracker.

It appears that this bug has been fixed with the wxWidgets 3.2.0 release!