another one is no highlighting (get it in only in debugger), running Windows 10 / 11 Home, Python 3.11.4, wxPython 4.2.1
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
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
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
try this simple wx approach (no Python magic)
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)
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 ), but no avail
this little example shows that wxPython is definitely not the cause
how would I subclass a GBS
(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)
I think this is structurally what we are talking about, can easily be expanded (the events object is the grid bag sizer)
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.