wxAUI breaks wxOwnerDrawnComboBox/wxComboCtrl (Issue #23399)

40 views
Skip to first unread message

Andrea Gavana

unread,
Mar 29, 2023, 2:59:16 PM3/29/23
to wx-...@googlegroups.com, Subscribed

Describe the bug:

This comes from wxPython, but since it uses pure C++ classes I thought I would report it here to see if anyone has any idea.

I have noticed a very peculiar - and fantastically annoying - behavior of wx.adv.OwnerDrawnComboBox (and wx.ComboCtrl as well) when embedded in a AUI (wx.lib.agw.aui or wx.aui) floating pane that can be closed and re-opened (hidden via the “x” close button in the floating pane and shown again by some other action).

Please consider the sample app attached to this message. After you start it up:

  1. Click on the “Launch” button: this will show a AUI floating pane containing a simple OwnerDrawnComboBox. Notice that you can interact with the combobox and the dropdown works all right

  2. Close the floating pane via the “x” button. This will not destroy the floating pane as it doesn’t have the DestroyOnClose() flag set

  3. Click again on the “Launch” button: The AUI floating pane will be re-shown again. Notice that the OwnerDrawnComboBox does not react anymore - i.e., no dropdown is shown, it’s unusable.

Using the wxPython Widget Inspection Tool, it appears that the second time the AUI pane is opened the wxTransientPopupWindow (child of OwnerDrawnComboBox) position has ridiculous values - see below:

This is on Windows 10 64 bit, Python 3.9.10 wxPython 4.2.1a1.dev5553+986d79b9 msw (phoenix) wxWidgets 3.2.2 - but it happens also with the regular wxPython 4.2.0.

The issue appears only on Windows, GTK seems to work ok.

It happens with wx.lib.agw.aui and wx.aui as well.

`
#!/usr/bin/env python

import wx
import wx.adv
import wx.lib.agw.aui as aui

==> NOTE: <==

It happens with wx.aui as well

class PenStyleComboBox(wx.adv.OwnerDrawnComboBox):

def __init__(self, parent, style):

    wx.adv.OwnerDrawnComboBox.__init__(self, pos=(30, 30), parent=parent, style=style)
    penStyles = ["Solid", "Transparent", "Dot"]
    self.SetItems(penStyles)

# Overridden from OwnerDrawnComboBox, called to draw each
# item in the list
def OnDrawItem(self, dc, rect, item, flags):
    if item == wx.NOT_FOUND:
        # painting the control, but there is no valid item selected yet
        return

    r = wx.Rect(*rect)  # make a copy
    r.Deflate(3, 5)

    penStyle = wx.PENSTYLE_SOLID
    if item == 1:
        penStyle = wx.PENSTYLE_TRANSPARENT
    elif item == 2:
        penStyle = wx.PENSTYLE_DOT

    pen = wx.Pen(dc.GetTextForeground(), 3, penStyle)
    dc.SetPen(pen)

    if flags & wx.adv.ODCB_PAINTING_CONTROL:
        # for painting the control itself
        dc.DrawLine(int(r.x + 5), int(r.y + r.height / 2), int(r.x + r.width) - 5, int(r.y + r.height / 2))

    else:
        # for painting the items in the popup
        dc.DrawText(self.GetString(item),
                    int(r.x + 3),
                    int((r.y + 0) + ((r.height / 2) - dc.GetCharHeight()) / 2)
                    )
        dc.DrawLine(int(r.x + 5), int(r.y + ((r.height / 4) * 3) + 1), int(r.x + r.width - 5),
                    int(r.y + ((r.height / 4) * 3) + 1))

# Overridden from OwnerDrawnComboBox, called for drawing the
# background area of each item.
def OnDrawBackground(self, dc, rect, item, flags):
    # If the item is selected, or its item # iseven, or we are painting the
    # combo control itself, then use the default rendering.
    if (item & 1 == 0 or flags & (wx.adv.ODCB_PAINTING_CONTROL |
                                  wx.adv.ODCB_PAINTING_SELECTED)):
        wx.adv.OwnerDrawnComboBox.OnDrawBackground(self, dc, rect, item, flags)
        return

    # Otherwise, draw every other background with different colour.
    bgCol = wx.Colour(240, 240, 250)
    dc.SetBrush(wx.Brush(bgCol))
    dc.SetPen(wx.Pen(bgCol))
    dc.DrawRectangle(rect)

# Overridden from OwnerDrawnComboBox, should return the height
# needed to display an item in the popup, or -1 for default
def OnMeasureItem(self, item):
    return 24

# Overridden from OwnerDrawnComboBox.  Callback for item width, or
# -1 for default/undetermined
def OnMeasureItemWidth(self, item):
    return -1  # default - will be measured from text width

----------------------------------------------------------------------

class TestFrame(wx.Frame):
def init(self, parent=None):
wx.Frame.init(self, parent, -1, 'Test OwnerDrawnComboBox & AUI', size=(800, 600))

    self._mgr = aui.AuiManager()
    self._mgr.SetManagedWindow(self)

    self.center_panel = wx.Panel(self)
    button = wx.Button(self.center_panel, -1, 'Launch', pos=(20, 20))
    button.Bind(wx.EVT_BUTTON, self.OnLaunch)

    self.owcb_panel = wx.Panel(self)
    pscb = PenStyleComboBox(self.owcb_panel, style=wx.CB_READONLY)

    self._mgr.AddPane(self.center_panel, aui.AuiPaneInfo().CenterPane().Name('Center'))
    self._mgr.AddPane(self.owcb_panel, aui.AuiPaneInfo().Name('OWCB').Float().Hide().Dockable(False).
                      FloatingSize(wx.Size(400, 400)).FloatingPosition(wx.Point(200, 200)))
    self._mgr.Update()
    self.CenterOnScreen()
    self.Show()


def OnLaunch(self, event):

    pane = self._mgr.GetPane(self.owcb_panel)

    if not pane.IsShown():
        pane.Float()
        pane.Show()
        self._mgr.Update()

if name == 'main':
app = wx.App(0)
TestFrame()
app.MainLoop()
`

Platform and version information

  • wxWidgets version you use: 3.2.2
  • wxWidgets port you use: wxMSW
  • OS and its version: Windows 10 64 bit


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issues/23399@github.com>

VZ

unread,
Mar 29, 2023, 6:25:54 PM3/29/23
to wx-...@googlegroups.com, Subscribed

Thanks for reporting this, but I don't really know what's going on here and would need to debug it. So it would be nice if this could be reproduced in the aui sample, as this would make debugging it easier.


Reply to this email directly, view it on GitHub, or unsubscribe.

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issues/23399/1489414422@github.com>

Andrea Gavana

unread,
Mar 30, 2023, 2:51:43 AM3/30/23
to wx-...@googlegroups.com, Subscribed

Thank you @vadz for considering the issue.

The problem could of course be due to an obscure wxPython/sip behavior, although I find it unlikely given how thin the wxPython wrapper is on top of wxWidgets.

I have attempted to create a patch to the auidemo.cpp module. But please bear in mind that I have zero knowledge of C++, I don't have a way to build wxWidgets and no compiler to test my code.

I basically coded it blind: it shouldn't be too bad, but I can't guarantee that it will compile as it is.

Happy to assist in any way I can. Thank you.

auidemo.cpp.patch


Reply to this email directly, view it on GitHub, or unsubscribe.

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issues/23399/1489786101@github.com>

VZ

unread,
Mar 30, 2023, 9:09:30 PM3/30/23
to wx-...@googlegroups.com, Subscribed

Thanks, I've fixed the attached patch (BTW, please always make unified diffs as explained in https://www.wxwidgets.org/develop/how-to-submit-patches/) and can see the problem now. It happens due to the fact that the popup window is destroyed when its parent TLW is destroyed, but the combobox isn't aware of it and continues to use the invalid window.

Unfortunately fixing it is not pretty, but I think I've finally done it in #23406. Please test it if you can and let me know if you see any problems. TIA!

Patch allowing to reproduce the problem
diff --git a/samples/aui/auidemo.cpp b/samples/aui/auidemo.cpp
index 024fb1eb4f..56931ff660 100644
--- a/samples/aui/auidemo.cpp
+++ b/samples/aui/auidemo.cpp
@@ -32,6 +32,8 @@
 #include "wx/textdlg.h"
 
 #include "wx/aui/aui.h"
+#include "wx/combo.h"
+#include "wx/odcombo.h"
 #include "../sample.xpm"
 
 // -- application --
@@ -55,6 +57,7 @@ class MyFrame : public wxFrame
     enum
     {
         ID_CreateTree = wxID_HIGHEST,
+        ID_CreateOwnerDrawnComboBox,
         ID_CreateGrid,
         ID_CreateText,
         ID_CreateHTML,
@@ -132,6 +135,8 @@ class MyFrame : public wxFrame
     void OnEraseBackground(wxEraseEvent& evt);
     void OnSize(wxSizeEvent& evt);
 
+    wxOwnerDrawnComboBox* CreateOwnerDrawnComboBox();
+    void OnCreateOwnerDrawnComboBox(wxCommandEvent& evt);
     void OnCreateTree(wxCommandEvent& evt);
     void OnCreateGrid(wxCommandEvent& evt);
     void OnCreateHTML(wxCommandEvent& evt);
@@ -574,6 +579,7 @@ bool MyApp::OnInit()
 wxBEGIN_EVENT_TABLE(MyFrame, wxFrame)
     EVT_ERASE_BACKGROUND(MyFrame::OnEraseBackground)
     EVT_SIZE(MyFrame::OnSize)
+    EVT_MENU(MyFrame::ID_CreateOwnerDrawnComboBox, MyFrame::OnCreateOwnerDrawnComboBox)
     EVT_MENU(MyFrame::ID_CreateTree, MyFrame::OnCreateTree)
     EVT_MENU(MyFrame::ID_CreateGrid, MyFrame::OnCreateGrid)
     EVT_MENU(MyFrame::ID_CreateText, MyFrame::OnCreateText)
@@ -679,6 +685,7 @@ bool MyApp::OnInit()
     file_menu->Append(wxID_EXIT);
 
     wxMenu* view_menu = new wxMenu;
+    view_menu->Append(ID_CreateOwnerDrawnComboBox, _("Create OwnerDrawnComboBox"));
     view_menu->Append(ID_CreateText, _("Create Text Control"));
     view_menu->Append(ID_CreateHTML, _("Create HTML Control"));
     view_menu->Append(ID_CreateTree, _("Create Tree"));
@@ -1768,3 +1775,148 @@ wxString MyFrame::GetIntroText()
 
     return wxString::FromAscii(text);
 }
+
+void MyFrame::OnCreateOwnerDrawnComboBox(wxCommandEvent& WXUNUSED(event))
+{
+
+    wxAuiPaneInfo& pane2 = m_mgr.GetPane("OwnerDrawnComboBox");
+    if (!pane2.IsOk())
+    {
+        m_mgr.AddPane(CreateOwnerDrawnComboBox(), wxAuiPaneInfo().
+                      Caption("OwnerDrawnComboBox").Name("OwnerDrawnComboBox").
+                      Float().FloatingPosition(GetStartPosition()).Hide().
+                      FloatingSize(FromDIP(wxSize(200,300))));
+    }
+
+    wxAuiPaneInfo& pane = m_mgr.GetPane("OwnerDrawnComboBox");
+    if (!pane.IsShown())
+    {
+        pane.Float();
+        pane.Show();
+        m_mgr.Update();
+    }
+}
+
+// ----------------------------------------------------------------------------
+// wxOwnerDrawnComboBox with custom paint list items
+// ----------------------------------------------------------------------------
+
+class wxPenStyleComboBox : public wxOwnerDrawnComboBox
+{
+public:
+    virtual void OnDrawItem( wxDC& dc,
+                             const wxRect& rect,
+                             int item,
+                             int flags ) const override
+    {
+        if ( item == wxNOT_FOUND )
+            return;
+
+        wxRect r(rect);
+        r.Deflate(3);
+        r.height -= 2;
+
+        wxPenStyle penStyle = wxPENSTYLE_SOLID;
+        if ( item == 1 )
+            penStyle = wxPENSTYLE_TRANSPARENT;
+        else if ( item == 2 )
+            penStyle = wxPENSTYLE_DOT;
+        else if ( item == 3 )
+            penStyle = wxPENSTYLE_LONG_DASH;
+        else if ( item == 4 )
+            penStyle = wxPENSTYLE_SHORT_DASH;
+        else if ( item == 5 )
+            penStyle = wxPENSTYLE_DOT_DASH;
+        else if ( item == 6 )
+            penStyle = wxPENSTYLE_BDIAGONAL_HATCH;
+        else if ( item == 7 )
+            penStyle = wxPENSTYLE_CROSSDIAG_HATCH;
+        else if ( item == 8 )
+            penStyle = wxPENSTYLE_FDIAGONAL_HATCH;
+        else if ( item == 9 )
+            penStyle = wxPENSTYLE_CROSS_HATCH;
+        else if ( item == 10 )
+            penStyle = wxPENSTYLE_HORIZONTAL_HATCH;
+        else if ( item == 11 )
+            penStyle = wxPENSTYLE_VERTICAL_HATCH;
+
+        wxPen pen( dc.GetTextForeground(), 3, penStyle );
+
+        // Get text colour as pen colour
+        dc.SetPen( pen );
+
+        if ( !(flags & wxODCB_PAINTING_CONTROL) )
+        {
+            dc.DrawText(GetString( item ),
+                        r.x + 3,
+                        (r.y + 0) + ( (r.height/2) - dc.GetCharHeight() )/2
+                       );
+
+            dc.DrawLine( r.x+5, r.y+((r.height/4)*3), r.x+r.width - 5, r.y+((r.height/4)*3) );
+        }
+        else
+        {
+            dc.DrawLine( r.x+5, r.y+r.height/2, r.x+r.width - 5, r.y+r.height/2 );
+        }
+    }
+
+    virtual void OnDrawBackground( wxDC& dc, const wxRect& rect,
+                                   int item, int flags ) const override
+    {
+
+        // If item is selected or even, or we are painting the
+        // combo control itself, use the default rendering.
+        if ( (flags & (wxODCB_PAINTING_CONTROL|wxODCB_PAINTING_SELECTED)) ||
+             (item & 1) == 0 )
+        {
+            wxOwnerDrawnComboBox::OnDrawBackground(dc,rect,item,flags);
+            return;
+        }
+
+        // Otherwise, draw every other background with different colour.
+        wxColour bgCol(240,240,250);
+        dc.SetBrush(wxBrush(bgCol));
+        dc.SetPen(wxPen(bgCol));
+        dc.DrawRectangle(rect);
+    }
+
+    virtual wxCoord OnMeasureItem( size_t item ) const override
+    {
+        // Simply demonstrate the ability to have variable-height items
+        return FromDIP( item & 1 ? 36 : 24 );
+    }
+
+    virtual wxCoord OnMeasureItemWidth( size_t WXUNUSED(item) ) const override
+    {
+        return -1; // default - will be measured from text width
+    }
+
+};
+
+wxOwnerDrawnComboBox* MyFrame::CreateOwnerDrawnComboBox()
+{
+    wxArrayString arrItems;
+    // Create common strings array
+    arrItems.Add( "Solid" );
+    arrItems.Add( "Transparent" );
+    arrItems.Add( "Dot" );
+    arrItems.Add( "Long Dash" );
+    arrItems.Add( "Short Dash" );
+    arrItems.Add( "Dot Dash" );
+    arrItems.Add( "Backward Diagonal Hatch" );
+    arrItems.Add( "Cross-diagonal Hatch" );
+    arrItems.Add( "Forward Diagonal Hatch" );
+    arrItems.Add( "Cross Hatch" );
+    arrItems.Add( "Horizontal Hatch" );
+    arrItems.Add( "Vertical Hatch" );
+    wxOwnerDrawnComboBox* odc = new wxPenStyleComboBox();
+    odc->Create(this,wxID_ANY,wxEmptyString,
+                wxPoint(30, 30), wxDefaultSize,
+                arrItems,
+                wxCB_READONLY
+               );
+
+
+    odc->SetSelection(0);
+    return odc;
+}


Reply to this email directly, view it on GitHub, or unsubscribe.

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issues/23399/1491153535@github.com>

Andrea Gavana

unread,
Mar 31, 2023, 5:10:10 AM3/31/23
to wx-...@googlegroups.com, Subscribed

Thank you @vadz for the super-fast reply and fix. I believe I will have to wait for someone to trigger a wxPython build in order to test this, I have no way to build wxWidgets and wxPython on my work PC. And I guess even triggering a wxPython build won't change anything as the PR has not been applied in wxWidgets...


Reply to this email directly, view it on GitHub, or unsubscribe.

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issues/23399/1491596611@github.com>

VZ

unread,
Apr 1, 2023, 2:16:52 PM4/1/23
to wx-...@googlegroups.com, Subscribed

Closed #23399 as completed via ac21107.


Reply to this email directly, view it on GitHub, or unsubscribe.

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issue/23399/issue_event/8904623321@github.com>

VZ

unread,
Apr 1, 2023, 2:20:10 PM4/1/23
to wx-...@googlegroups.com, Subscribed

I've merged the PR so hopefully this should be fixed in the next wxPython build. If not, please comment here.


Reply to this email directly, view it on GitHub, or unsubscribe.

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issues/23399/1493066564@github.com>

Andrea Gavana

unread,
Apr 11, 2023, 1:20:47 AM4/11/23
to wx-...@googlegroups.com, Subscribed

Thank you @vadz for addressing this issue. I have managed to cherry-pick only the changes you have made to the few .cpp/.h files, as wxPython does not compile with the current wxWidgets master.

After recompiling wxPython with your changes, the C++ sample I created (and you adjusted) works as intended.

However, if I do the following:

  1. In the initialization of the wxOwnerDrawnComboBox call odc.Set(array_of_strings)
  2. Do the procedure of opening the floating pane and closing it again (as described in the original issue)
  3. In the OnDrawItem method, calling odc.GetStrings() now returns an empty list

This means that I can't rely on GetStrings() to return what I need, as apparently wxOwnerDrawnComboBox "forgets" whatever was set initially using odc.Set(). I believe the only way out is to store the array_of_strings array in another attribute myself, and then access its value using the integer item parameter in the OnDrawItem method.


Reply to this email directly, view it on GitHub, or unsubscribe.

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issues/23399/1502703573@github.com>

VZ

unread,
Apr 11, 2023, 7:50:45 AM4/11/23
to wx-...@googlegroups.com, Subscribed

This might be something wxPython-specific because it works correctly for me in the C++ sample: if I add

        if ( item == 0 )
            wxLogDebug("Number of items: %zu", GetStrings().size());

to the beginning of OnDrawItem() in the patch above, it outputs 12 for me both before and after recreating the combobox.


Reply to this email directly, view it on GitHub, or unsubscribe.

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issues/23399/1503182853@github.com>

Reply all
Reply to author
Forward
0 new messages