Can a GridBagSizer row be highlighted and have an event attached?

another one is no highlighting (get it in only in debugger), running Windows 10 / 11 Home, Python 3.11.4, wxPython 4.2.1 :roll_eyes:

There is an upload button in the editor which allows you to upload .py .zip and various image types. A download link will appear in the post for .py and .zip files. Image files will be displayed in the post. However, the button may not appear for new users initially (I don’t remember the criteria).

Windows (and Pylint) likes

            for w in widgets:
                w.SetBackgroundColour(color)
                w.Refresh()
            # [w.SetBackgroundColour(color) for w in widgets]

Thanks @da-dada, I’ll change my code so that windows will work.

swap_rows.py (8.7 KB)

This is the latest version of the GBS row-swapping code. It incorporates the fix provided by @da-dada.
This version deals correctly with unused columns on some rows and widgets that take up more than one column.

The biggest issue I ran into was how the GBS stored widgets that span more than a single column. It creates a GBSizerItem object with the same widgets in it for each column that the widget spans into. I had to remove all but one of them before processing the swapping of the rows.

kiss :face_with_hand_over_mouth:

        for idx, row in enumerate(positions):
            sizer_items.append({})

            for rc in row:
                sizer_items[idx][gbs.FindItemAtPosition(rc)] = None

        sizer_items = list(sizer_items)

That won’t work for a couple of reasons.

One, you’re using a normal dict and I’m using an OrderedDict. I’m using an OrderedDict to keep widgets (windows) in the order they were in in the row. The reason for using an OrderedDict and using the GBSizerItem as the key is to remove any that are dups because any type of dict will do that automatically, adding the same key more than once the last one wins, so to speak. I’d prefer to use a set, but sets don’t work with unhashable objects.

The second reason your code won’t work is that sizer_items = list(sizer_items) will not convert the keys in the OrderedDict to a list. If you notice the value of all keys is None because they’re getting thrown away.

I’ll go even a bit further :thinking:

        sizer_items = []

        for row in positions:
            pos = {}

            for rc in row:
                pos[gbs.FindItemAtPosition(rc)] = None
            sizer_items.append(pos)

        # sizer_items = list(sizer_items)

That has all the same issues as I described above.
How are you converting the keys to lists and a normal dict is unordered so if there is more than one widget in a row they will probably be out of order.

these Python people don’t only up the speed, they also get more feature-complete :rofl:

try this simple wx approach (no Python magic) :cowboy_hat_face:

    def gbs_swap_rows(self, gbs, row0, row1, flag=0, border=0):
        """
        Swap any two rows in a GridBagSizer keeping most parameters.
        """
        rows = gbs.GetRows()
        cols = gbs.GetCols()
        assert row0 != row1, (f"row0 ({row0}) and row1 ({row1}) cannot "
                              "be the same.")
        assert -1 < row0 < rows, ("The row0 value is invalid can only be "
                                  f"between 0 and {rows-1}, found {row0}")
        assert -1 < row1 < rows, ("The row1 value is invalid can only be "
                                  f"between 0 and {rows-1}, found {row1}")
        assert -1 < cols, f"The number of columns must be >= 0, found {cols}."

        w0 = []
        w1 = []
        for entry in gbs.GetChildren():
            if entry.GetPos()[0] == row0:
                w = entry.GetWindow()
                w0.append((w, gbs.GetItemPosition(w), gbs.GetItemSpan(w), entry))
            elif entry.GetPos()[0] == row1:
                w1.append(entry.GetWindow())
        for entry in w0:
            gbs.Remove(self.get_sizer_item_index(gbs, entry[3]))
        for entry in w1:
            row = gbs.GetItemPosition(entry)
            row.SetRow(row0)
            gbs.SetItemPosition(entry, row)
        for entry in w0:
            entry[1].SetRow(row1)
            gbs.Add(entry[0], entry[1], entry[2], flag=flag, border=border)

@da-dada, Your code seems to work.
I’ll have to test it a bit more than I have so far.

or, more simple (one method axed) :joy:

        w0 = []
        w1 = []
        for idx, entry in enumerate(gbs.GetChildren()):
            if (x := entry.GetPos()[0]) == row0:
                w = entry.GetWindow()
                w0.append((w, gbs.GetItemPosition(w), gbs.GetItemSpan(w), idx))
            elif x == row1:
                w1.append(entry.GetWindow())
        for entry in reversed(w0):
            gbs.Remove(entry[3])
        for entry in w1:
            row = gbs.GetItemPosition(entry)
            row.SetRow(row0)
            gbs.SetItemPosition(entry, row)
        for entry in w0:
            entry[1].SetRow(row1)
            gbs.Add(entry[0], entry[1], entry[2], flag=flag, border=border)

@da-dada, you’re spending way too much time on someone else’s problem, but thanks for all your suggestions and code.

well, I think we only touched upon problems in this coding (sub classing, class variables, comprehensions, etc) and I tried to comprehend the purpose (someone else might have jumped in :sweat_smile:), but no avail
this little example shows that wxPython is definitely not the cause :sneezing_face:

how would I subclass a GBS :rofl:
(and use grid_bag_sizer = GridBagSizer())

class GridBagSizer(wx.GridBagSizer):

    def swap_rows(self, row0, row1, flag=0, border=0):
        """
        Swap any two rows in a GridBagSizer keeping most parameters.
        """
        w0 = []
        w1 = []
        for idx, entry in enumerate(self.GetChildren()):
            if (x := entry.GetPos()[0]) == row0:
                w = entry.GetWindow()
                w0.append((w, self.GetItemPosition(w), self.GetItemSpan(w), idx))
            elif x == row1:
                w1.append(entry.GetWindow())
        for entry in reversed(w0):
            self.Remove(entry[3])
        for entry in w1:
            row = self.GetItemPosition(entry)
            row.SetRow(row0)
            self.SetItemPosition(entry, row)
        for entry in w0:
            entry[1].SetRow(row1)
            self.Add(entry[0], entry[1], entry[2], flag=flag, border=border)

    def highlight_row(self, row, color=None):
        for entry in self.GetChildren():
            if entry.GetPos()[0] == row:
                w = entry.GetWindow()
                w.SetBackgroundColour(color)
                w.Refresh(

swap_rows.py (7.6 KB)

@da-dada I’ve modified your code slightly. I renamed all your variable entry to names that more closely reflect what’s in them. I’ve also removed the flag and border args since I found that I was able to grab them from the GBSizerItem directly. Plus I still love list comprehensions. LOL

So here is something to think about. What will happen if there is another sizer in the GBS such as a BoxSizer set to HORIZONTAL. I haven’t tested this yet.

in the derived GBS one could veto the Add of non window controls (just to keep it simple) :innocent:

I think this is structurally what we are talking about, can easily be expanded (the events object is the grid bag sizer) :ghost:

import wx

class GBSEvent(wx.PyEvent):

    EventType = wx.NewEventType()
    EVT_GBS_ROW_SELECTED = wx.PyEventBinder(EventType)

    def __init__(self):
        super().__init__(eventType=self.EventType)
        # self.EventObject = self
        self._getAttrDict().update(
                    {'EventObject': self,
                    'EventCategory': wx.EVT_CATEGORY_UI})

class GridBagSizer(wx.GridBagSizer):

    def __init__(self, evh, *args, **kargs):
        super().__init__(*args, **kargs)
        self.evh = evh
        self.evt = GBSEvent()
        self.evt.SetEventObject(self)
        self.row_selected = None

    def Add(self, *args, **kargs):
        super().Add(*args, **kargs)
        if isinstance(args[0], wx.Window):
            args[0].Bind(wx.EVT_LEFT_DOWN, self.select)

    def select(self, evt):
        rs = self.row_selected
        self.deselect()
        row = self.FindItem(evt.GetEventObject()).GetPos()[0]
        if row != rs:
            for entry in self.GetChildren():
                if entry.GetPos()[0] == row:
                    w = entry.GetWindow()
                    w.SetBackgroundColour('light blue')
                    w.Refresh()
            self.row_selected = row
            self.evt.row = row
            self.evh.AddPendingEvent(self.evt)

    def deselect(self):
        if self.row_selected is not None:
            for entry in self.GetChildren():
                if entry.GetPos()[0] == self.row_selected:
                    w = entry.GetWindow()
                    w.SetBackgroundColour(None)
                    w.Refresh()
            self.row_selected = None

    def swap_rows(self, row1, row0=None):
        """
        Swap any two rows in a GridBagSizer.
        """
        if row0 is None:
            if self.row_selected is None:
                row0 = -1
            row0 = self.row_selected

        def str_int(row):
            if not isinstance(row, str|int):
                row = -1
            elif isinstance(row, str):
                if row.isnumeric():
                    row = int(row)
                else:
                    row = -1
            if row < 0 or row > self.GetRows() - 1:
                row = -1
            return row

        if (row0 := str_int(row0)) < 0 or (row1 := str_int(row1)) < 0:
            return
        # swap
        self.deselect()
        w0 = []
        w1 = []
        for idx, entry in enumerate(self.GetChildren()):
            if (x := entry.GetPos()[0]) == row0:
                w = entry.GetWindow()
                w0.append((w, self.GetItemPosition(w), self.GetItemSpan(w),
                                    entry.GetFlag(), entry.GetBorder(), idx))
            elif x == row1:
                w1.append(entry.GetWindow())
        for entry in reversed(w0):
            self.Remove(entry[5])
        for entry in w1:
            row = self.GetItemPosition(entry)
            row.SetRow(row0)
            self.SetItemPosition(entry, row)
        for entry in w0:
            entry[1].SetRow(row1)
            self.Add(entry[0], entry[1], entry[2],
                                             flag=entry[3], border=entry[4])
        self.Layout()

class MyFrame(wx.Frame):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Create the sizer objects.
        vbox = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(vbox)

        spin = wx.SpinCtrl(self)
        vbox.Add(spin, 0, wx.CENTER | wx.ALL, 0)

        gbs = GridBagSizer(self)
        vbox.Add(gbs, 0, wx.CENTER | wx.ALL, 6)
        self.Bind(gbs.evt.EVT_GBS_ROW_SELECTED,
                        lambda evt: spin.SetValue(evt.row))
        spin.Bind(wx.EVT_TEXT,
                        lambda evt: gbs.swap_rows(evt.GetString()))

        # Add the widgets to the GridBagSizer.
        self.create_widgets(gbs)
        spin.SetRange(0, gbs.GetRows()-1)

        self.Show()

    def create_widgets(self, gbs):
        widget = wx.StaticText(self, -1, "Two column wide text (move me).",
                                 style=0)
        gbs.Add(widget, (0, 0), (1, 2), wx.ALL, 6)
        num_widgets = 13
        num = 0

        for idx in range(num_widgets):
            dec = idx % 3
            if not dec:
                num += 1
            label = f"Widget {num}.{dec}"
            widget = wx.StaticText(self, -1, label, style=0)
            pos, span = (num, dec), (1, 1)
            gbs.Add(widget, pos, span, wx.ALIGN_CENTER | wx.ALL, 6)

if __name__ == "__main__":
    app = wx.App()
    MyFrame(None, title="Swap Widgets in a GridBagSizer")
    app.MainLoop()

This last version you posted does not permit the swapping of rows after the first one. You need to re-click the row to swap it again. I consider this a bad user experience.
However, I like the idea of not overriding the widget to get it to respond to mouse clicks.