GridSizer plus Panel Subclass

Hi,

I have this weird issue with an application for Windows.

Setup:
ScrolledPanel - where the items are dynamic, based on data from a database. Each item is an instance of a subclass of wx.Panel, UTListPanel, with sub items placed at static positions within the panel.

The UTListPanels are created, setup and then added to an array (also tried to add one by one to a wx.GridSizer.

The GridSizer is setup with 3 items per row, zero padding.

I added some extra paint code to make this more visible, but as you see, the items inside the UTListPanel are getting offset (wrongly) on the second line.

screen

Anyone has an idea why this happens, and mostly, how to avoid it?

Difficult to say without seeing some code…

Are you calling SetupScrolling() on the ScrolledPanel after you add stuff? Did you try to call Layout() - possibly inside a wx.CallAfter - for the sizer?

There’s a ton of things you can try but honestly I have no crystal ball to look into your code.

Hi,

The method that creates the Panel subclasses:

Some before constructions:

self.wListPanel = wx.lib.scrolledpanel.ScrolledPanel(self.wPanel, pos=(400,60), size=(770, 646))
self.wScrollSizer = wx.GridSizer( 3, 0, 0 )

# ----------------------------
# createRoleControllers
# ----------------------------
def createRoleControllers(self):
	
	# Check if we already have controllers
	if len(self.aListControllers)>0:
		return
		
	for oDept in self.aDeptsList:
		print(f"Adding: {oDept['deptname']}")
		oCtrl = UTListPanel(self.wListPanel, pos=(0, 0), size=(232,320))
		oCtrl.setup(oDept['deptname'], oDept['deptid'], self, self.aShiftsList)
		self.aListControllers.append(oCtrl)
	
	self.wScrollSizer.AddMany(self.aListControllers)		
	self.wListPanel.SetSizer(self.wScrollSizer)
	self.wListPanel.SetupScrolling()
	self.wListPanel.Update()

============================================

UTShiftList Class

============================================

class UTListPanel(wx.Panel):

# ----------------------------
# Constructor
# ----------------------------
def __init__(self, *arg, **kw):
	wx.Panel.__init__(self, *arg, **kw)
	self.SetDropTarget(MyDropTarget(self))

# ----------------------------
# setup
# ----------------------------
def setup(self, sInName, nInDeptId, oInParent, aInShiftsList):
	
	# Snag
	self.oParent = oInParent
	self.sName = sInName
	self.nDeptId = nInDeptId

	# The List
	self.wList = wx.ListCtrl(self,  pos=(10, 10), size=(210, 200), style=wx.LC_REPORT | wx.LC_SINGLE_SEL)
	self.wList.InsertColumn(0, self.sName)		
	self.wList.SetColumnWidth(0, 190)		
	
	# The Delete button
	self.wDeleteButton = wx.Button(self, label="-", pos=(5, 218), size=(22, 22))
	self.wDeleteButton.Disable()

	# The Service Popup
	self.wServiceLabel = wx.StaticText(self, label="Service:", pos=(40,222), size=(50, 20), style=wx.ALIGN_LEFT)
	self.wServicePopup = wx.Choice(self, pos=(90, 218), size=(130, 24))
	self.wDeleteButton.Disable()
	
	# Service notes
	self.wServiceNote = wx.TextCtrl(self, pos=(10,244), size=(210, 60), style=wx.TE_MULTILINE )
	self.wServiceNote.Disable()

removed a bunch code here that is not of interest

I’m not sure why you need to give specific position and size for the ScrolledPanel - that’s generally not a good idea, as it’s going to break on machines with different resolutions/sizes.

That said, your code as it stands is not runnable. This timeless Wiki entry from Robin sums it up:

https://wiki.wxpython.org/MakingSampleApps

Stripped down to a bare minimum to show the issues

Main.py

#!/usr/bin/python
import os, sys
import wx
import wx.adv
import wx.lib.scrolledpanel
import json
import pprint
import pickle

from UTListPanel import UTListPanel

============================================================

Main Window Class - PersPlanGUI

============================================================

class PersPlanGUI(wx.Frame):

# ============================================================
# Constructor
# ============================================================
def __init__(self, *args, **kw):
	
	
	# call super init
	super(PersPlanGUI, self).__init__(*args, **kw)

	self.sProgramName = "Sample"
	self.oWindowSize=wx.Size(1200, 800)

	self.aListControllers = []
				
	# create a panel
	self.wPanel = wx.Panel(self, style =  wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX), pos=wx.DefaultPosition)
	self.wPanel.SetBackgroundColour( wx.Colour( 245, 245, 245 ) )
	self.SetMinSize(self.oWindowSize)
	self.SetMaxSize(self.oWindowSize)
	self.SetSize(self.oWindowSize)
	self.SetTitle(f"Sample Window Title")

	# create GUI
	self.makeGUI()

	# Create menu bar
	self.makeMenuBar()
	self.createRoleControllers()

# ============================================================
#
#					GUI Setup
#
# ============================================================
	
# ----------------------------------------------------
# Build main window GUI
# ---------------------------------------------------
def makeGUI(self):		
	#
	# The People box
	#
	self.PeopleBox = wx.StaticBox(self.wPanel, label="xxxxxxxxx:", pos=(10,13), size=(380, 693) )
		
	self.wListPanel = wx.lib.scrolledpanel.ScrolledPanel(self.wPanel, pos=(400,60), size=(770, 646)) 
	self.wScrollSizer = wx.GridSizer( 3, 0, 0 )
	
# ---------------------------------------------------
# Make Menu Bar
# ---------------------------------------------------
def makeMenuBar(self):
		
	# --- File Menu
	self.fileMenu = wx.Menu()
	self.quitItem = self.fileMenu.Append(102, "&Quit\tCTRL-Q", f"Quit {self.sProgramName}")

	self.menuBar = wx.MenuBar()
	self.menuBar.Append(self.fileMenu, "&File")

	self.SetMenuBar(self.menuBar)
	

# ============================================================
#
#					Event Handlers
#
# ============================================================


# ----------------------------
# onQuit
# ----------------------------
def onQuit(self, event):
	self.Close(True)

	
# ============================================================
#
#						Handlers
#
# ============================================================

# ----------------------------
# createRoleControllers
# ----------------------------
def createRoleControllers(self):
	
	# Check if we already have controllers
	if len(self.aListControllers)>0:
		return
		
	for index in  range(0,9):
		oCtrl = UTListPanel(self.wListPanel, pos=(0, 0), size=(232,320))
		oCtrl.setup(f"Panel #{index}", index, self, [])
		self.aListControllers.append(oCtrl)
	
	self.wScrollSizer.AddMany(self.aListControllers)	
	self.wListPanel.SetSizer(self.wScrollSizer)
	self.wListPanel.SetupScrolling()
	self.wListPanel.Update()

==========================================================================================================

### ENTRY POINT

==========================================================================================================

if name == ‘main’:
oApp = wx.App()
oMainWindow = PersPlanGUI(None, title="")

# Check for debug option
oApp.SetTopWindow(oMainWindow);
oMainWindow.Show()
oApp.MainLoop()

And UTListPanel.py

#!/usr/bin/python
import os, sys
import wx

#!/usr/bin/python
import os, sys
import wx
import wx.adv

============================================

UTShiftList Class

============================================

class UTListPanel(wx.Panel):

# ----------------------------
# Constructor
# ----------------------------
def __init__(self, *arg, **kw):
	wx.Panel.__init__(self, *arg, **kw)

	self.oBackgroundColor = wx.Colour( red=235, green=235, blue=235 ) 
	self.SetBackgroundColour(self.oBackgroundColor)
	self.bDragging=False
	
# ----------------------------
# setup
# ----------------------------
def setup(self, sInName, nInDeptId, oInParent, aInShiftsList):
	
	# Snag
	self.oParent = oInParent
	self.sName = sInName
	self.nDeptId = nInDeptId
	
	# The List
	self.wList = wx.ListCtrl(self,  pos=(10, 10), size=(210, 200), style=wx.LC_REPORT | wx.LC_SINGLE_SEL)
	self.wList.InsertColumn(0, self.sName)		
	self.wList.SetColumnWidth(0, 190)		
	
	# The Delete button
	self.wDeleteButton = wx.Button(self, label="-", pos=(5, 218), size=(22, 22))

	# The Service Popup
	self.wServiceLabel = wx.StaticText(self, label="Service:", pos=(40,222), size=(50, 20), style=wx.ALIGN_LEFT)
	self.wServicePopup = wx.Choice(self, pos=(90, 218), size=(130, 24))
	
	# Service notes
	self.wServiceNote = wx.TextCtrl(self, pos=(10,244), size=(210, 60), style=wx.TE_MULTILINE )

I really like your ambitions :rofl:

I’ve written about half a dozen in-house applications over the part 18 months using wxpython, and this is the first time I’ve been stumped by an issue I can neither explain, nor find a way around, so that’s why I decided to see if someone with more experience about the ins and outs of it could give me a ptr to what the heck is happening.

that’s fair enough but when I run Pylint over your ‘minimum’ there is the first error on line 122; so I don’t know how that piece of art could run at all :roll_eyes:

Which of the files? I ran then but I saw you cannot upload a file so I pasted in the code, I guess it broke on the way.

I wrapped all into one file, and btw, it looks correct on macOS (where I am now)

Skärmavbild 2024-10-14 kl. 23.32.24

#!/usr/bin/python
import os, sys
import wx
import wx.adv
import wx.lib.scrolledpanel
import json

============================================

UTShiftList Class

============================================

class UTListPanel(wx.Panel):

# ----------------------------
# Constructor
# ----------------------------
def __init__(self, *arg, **kw):
	wx.Panel.__init__(self, *arg, **kw)

	self.oBackgroundColor = wx.Colour( red=235, green=235, blue=235 ) 
	self.SetBackgroundColour(self.oBackgroundColor)
	self.bDragging=False
	
# ----------------------------
# setup
# ----------------------------
def setup(self, sInName, nInDeptId, oInParent, aInShiftsList):
	
	# Snag
	self.oParent = oInParent
	self.sName = sInName
	self.nDeptId = nInDeptId
	
	# The List
	self.wList = wx.ListCtrl(self,  pos=(10, 10), size=(210, 200), style=wx.LC_REPORT | wx.LC_SINGLE_SEL)
	self.wList.InsertColumn(0, self.sName)		
	self.wList.SetColumnWidth(0, 190)		
	
	# The Delete button
	self.wDeleteButton = wx.Button(self, label="-", pos=(5, 218), size=(22, 22))

	# The Service Popup
	self.wServiceLabel = wx.StaticText(self, label="Service:", pos=(40,222), size=(50, 20), style=wx.ALIGN_LEFT)
	self.wServicePopup = wx.Choice(self, pos=(90, 218), size=(130, 24))
	
	# Service notes
	self.wServiceNote = wx.TextCtrl(self, pos=(10,244), size=(210, 60), style=wx.TE_MULTILINE )

============================================================

Main Window Class - PersPlanGUI

============================================================

class PersPlanGUI(wx.Frame):

# ============================================================
# Constructor
# ============================================================
def __init__(self, *args, **kw):
	
	
	# call super init
	super(PersPlanGUI, self).__init__(*args, **kw)

	self.sProgramName = "Sample"
	self.oWindowSize=wx.Size(1200, 800)

	self.aListControllers = []
				
	# create a panel
	self.wPanel = wx.Panel(self, style =  wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX), pos=wx.DefaultPosition)
	self.wPanel.SetBackgroundColour( wx.Colour( 245, 245, 245 ) )
	self.SetMinSize(self.oWindowSize)
	self.SetMaxSize(self.oWindowSize)
	self.SetSize(self.oWindowSize)
	self.SetTitle(f"Sample Window Title")

	# create GUI
	self.makeGUI()

	# Create menu bar
	self.makeMenuBar()
	self.createRoleControllers()

# ============================================================
#
#					GUI Setup
#
# ============================================================
	
# ----------------------------------------------------
# Build main window GUI
# ---------------------------------------------------
def makeGUI(self):		
	#
	# The People box
	#
	self.PeopleBox = wx.StaticBox(self.wPanel, label="xxxxxxxxx:", pos=(10,13), size=(380, 693) )
		
	self.wListPanel = wx.lib.scrolledpanel.ScrolledPanel(self.wPanel, pos=(400,60), size=(770, 646)) 
	self.wScrollSizer = wx.GridSizer( 3, 0, 0 )
	
# ---------------------------------------------------
# Make Menu Bar
# ---------------------------------------------------
def makeMenuBar(self):
		
	# --- File Menu
	self.fileMenu = wx.Menu()
	self.quitItem = self.fileMenu.Append(102, "&Quit\tCTRL-Q", f"Quit {self.sProgramName}")

	self.menuBar = wx.MenuBar()
	self.menuBar.Append(self.fileMenu, "&File")

	self.SetMenuBar(self.menuBar)
	

# ============================================================
#
#					Event Handlers
#
# ============================================================


# ----------------------------
# onQuit
# ----------------------------
def onQuit(self, event):
	self.Close(True)

	
# ============================================================
#
#						Handlers
#
# ============================================================

# ----------------------------
# createRoleControllers
# ----------------------------
def createRoleControllers(self):
	
	# Check if we already have controllers
	if len(self.aListControllers)>0:
		return
		
	for index in  range(0,9):
		oCtrl = UTListPanel(self.wListPanel, pos=(0, 0), size=(232,320))
		oCtrl.setup(f"Panel #{index}", index, self, [])
		self.aListControllers.append(oCtrl)
	
	self.wScrollSizer.AddMany(self.aListControllers)	
	self.wListPanel.SetSizer(self.wScrollSizer)
	self.wListPanel.SetupScrolling()
	self.wListPanel.Update()

==========================================================================================================

### ENTRY POINT

==========================================================================================================

if name == ‘main’:
oApp = wx.App()
oMainWindow = PersPlanGUI(None, title="")

# Check for debug option
oApp.SetTopWindow(oMainWindow);
oMainWindow.Show()
oApp.MainLoop()

there is an icon Preformated text (Ctrl + Shift + C) and that will format your pasted text, at least on Windows (btw the error has now moved to 154) :crazy_face:

#!/usr/bin/python
import os, sys
import wx
import wx.adv
import wx.lib.scrolledpanel
import json

# ============================================
#
# UTShiftList Class
#
# ============================================
class UTListPanel(wx.Panel):

	# ----------------------------
	# Constructor
	# ----------------------------
	def __init__(self, *arg, **kw):
		wx.Panel.__init__(self, *arg, **kw)

		self.oBackgroundColor = wx.Colour( red=235, green=235, blue=235 ) 
		self.SetBackgroundColour(self.oBackgroundColor)
		self.bDragging=False
		
	# ----------------------------
	# setup
	# ----------------------------
	def setup(self, sInName, nInDeptId, oInParent, aInShiftsList):
		
		# Snag
		self.oParent = oInParent
		self.sName = sInName
		self.nDeptId = nInDeptId
		
		# The List
		self.wList = wx.ListCtrl(self,  pos=(10, 10), size=(210, 200), style=wx.LC_REPORT | wx.LC_SINGLE_SEL)
		self.wList.InsertColumn(0, self.sName)		
		self.wList.SetColumnWidth(0, 190)		
		
		# The Delete button
		self.wDeleteButton = wx.Button(self, label="-", pos=(5, 218), size=(22, 22))

		# The Service Popup
		self.wServiceLabel = wx.StaticText(self, label="Service:", pos=(40,222), size=(50, 20), style=wx.ALIGN_LEFT)
		self.wServicePopup = wx.Choice(self, pos=(90, 218), size=(130, 24))
		
		# Service notes
		self.wServiceNote = wx.TextCtrl(self, pos=(10,244), size=(210, 60), style=wx.TE_MULTILINE )
				
# ============================================================
#
#
#				Main Window Class - PersPlanGUI
#
#
# ============================================================
class PersPlanGUI(wx.Frame):

	# ============================================================
	# Constructor
	# ============================================================
	def __init__(self, *args, **kw):
		
		
		# call super init
		super(PersPlanGUI, self).__init__(*args, **kw)

		self.sProgramName = "Sample"
		self.oWindowSize=wx.Size(1200, 800)
	
		self.aListControllers = []
					
		# create a panel
		self.wPanel = wx.Panel(self, style =  wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX), pos=wx.DefaultPosition)
		self.wPanel.SetBackgroundColour( wx.Colour( 245, 245, 245 ) )
		self.SetMinSize(self.oWindowSize)
		self.SetMaxSize(self.oWindowSize)
		self.SetSize(self.oWindowSize)
		self.SetTitle(f"Sample Window Title")

		# create GUI
		self.makeGUI()

		# Create menu bar
		self.makeMenuBar()
		self.createRoleControllers()
	
	# ============================================================
	#
	#					GUI Setup
	#
	# ============================================================
		
	# ----------------------------------------------------
	# Build main window GUI
	# ---------------------------------------------------
	def makeGUI(self):		
		#
		# The People box
		#
		self.PeopleBox = wx.StaticBox(self.wPanel, label="xxxxxxxxx:", pos=(10,13), size=(380, 693) )
			
		self.wListPanel = wx.lib.scrolledpanel.ScrolledPanel(self.wPanel, pos=(400,60), size=(770, 646)) 
		self.wScrollSizer = wx.GridSizer( 3, 0, 0 )
		
	# ---------------------------------------------------
	# Make Menu Bar
	# ---------------------------------------------------
	def makeMenuBar(self):
			
		# --- File Menu
		self.fileMenu = wx.Menu()
		self.quitItem = self.fileMenu.Append(102, "&Quit\tCTRL-Q", f"Quit {self.sProgramName}")

		self.menuBar = wx.MenuBar()
		self.menuBar.Append(self.fileMenu, "&File")

		self.SetMenuBar(self.menuBar)
		

	# ============================================================
	#
	#					Event Handlers
	#
	# ============================================================

	
	# ----------------------------
	# onQuit
	# ----------------------------
	def onQuit(self, event):
		self.Close(True)

		
	# ============================================================
	#
	#						Handlers
	#
	# ============================================================
	
	# ----------------------------
	# createRoleControllers
	# ----------------------------
	def createRoleControllers(self):
		
		# Check if we already have controllers
		if len(self.aListControllers)>0:
			return
			
		for index in  range(0,9):
			oCtrl = UTListPanel(self.wListPanel, pos=(0, 0), size=(232,320))
			oCtrl.setup(f"Panel #{index}", index, self, [])
			self.aListControllers.append(oCtrl)
		
		self.wScrollSizer.AddMany(self.aListControllers)	
		self.wListPanel.SetSizer(self.wScrollSizer)
		self.wListPanel.SetupScrolling()
		self.wListPanel.Update()

	
# ==========================================================================================================
#
#
#
#												### ENTRY POINT ###
#
#
#
# ==========================================================================================================
if __name__ == '__main__':
	oApp = wx.App()
	oMainWindow = PersPlanGUI(None, title="")
	
	# Check for debug option
	oApp.SetTopWindow(oMainWindow);
	oMainWindow.Show()
	oApp.MainLoop()

Thanks!
Now the code doesnt look mangled

ok, the code is executable, but, as @Andrea_Gavana mentioned, sizers & absolute positioning / sizes don’t go together (see sizer) and that fact is completely mangled
so I would also suggest sizers, sizers, sizers :wink:

The problem seems to only happen on Windows, not macOS (I haven’t tested on linux yet, but will when I have the opportunity), which says it has not to do with sizers really, but I will give sizers a go even for the Panels to see if that helps.

If you look, the sizer is used to place the Panels inside the scroller.
The absolute positioning is used to place items inside the Panels.

I haven’t tried your code - on holiday at the moment - but as a first guess I’d say you should remove absolute positioning for stuff that goes into the GridSizer to start with. Second, I would try and add something like:

wx.CallAfter(sizer.Layout)

After you add your items to the GridSizer. I would also attempt to use SendSizeEvent() on the ScrolledPanel or, at worst, to brute force it like this:

sz = scrolled_panel.GetSize()
scrolled_panel.SetSize((sz[0], sz[1]+1))
scrolled_panel.SetSize(sz)

Or something like that.

Thanks @Andrea_Gavana and @da-dada
It looks like Windows is much more sensitive to Sizers than macOS or Linux.
Works after I added sizers in the Panel to adjust the items.