I'm building a UI in wxPython,[note 1] and I have three main body panels, two toolbars, and a status bar, with two BoxSizer elements (a vertical one containing all of the above, and a horizontal one containing the body panels). I cannot get the layout to work quite right, and I'm getting some behavior that I can't find in the documentation. I'm leaving out some details, but here is the relevant (working) part of the app:
import wx
import wx.adv
class MyApp(wx.App):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def OnInit(self):
self.frame = AppFrame(parent=None, title="My Sweet App")
self.SetTopWindow(self.frame)
self.frame.Show()
return True
class AppFrame(wx.Frame):
def __init__(self, size=(1024, 768), *args, **kwargs):
super().__init__(size=size, *args, **kwargs)
# Menu (quit only)
menubar = wx.MenuBar()
file_menu = wx.Menu()
quit_item = wx.MenuItem(
file_menu, wx.ID_EXIT, '&Exit', 'Close the application')
file_menu.Append(quit_item)
self.Bind(wx.EVT_MENU, OnQuit, id=wx.ID_EXIT)
menubar.Append(file_menu, '&File')
self.SetMenuBar(menubar)
# Outer box wrapper
main_box = wx.BoxSizer(orient=wx.VERTICAL)
self.SetSizer(main_box)
main_box.SetMinSize(size)
# Inner box with the three main view panels in it
wrap_panels_box = wx.BoxSizer(orient=wx.HORIZONTAL)
wrap_panels_box.SetMinSize(200, 200)
panel1 = wx.Panel(self, -1)
panel2 = wx.Panel(self, -1)
panel3 = wx.Panel(self, -1)
common_flags = wx.SizerFlags().Expand().Border(wx.ALL, 5)
wrap_panels_box.AddMany([(panel1, common_flags),
(panel2, common_flags),
(panel3, common_flags)])
# Two toolbars for different sets of commands
toolbar1 = wx.ToolBar(parent=self)
tool1 = toolbar1.CreateTool(
toolId=wx.ID_NEW, label='',
bmpNormal=wx.ArtProvider.GetBitmap(wx.ART_NEW, wx.ART_TOOLBAR))
toolbar1.AddTool(tool1)
toolbar1.Realize()
toolbar2 = wx.ToolBar(parent=self)
tool2 = toolbar2.CreateTool(
toolId=wx.ID_SAVE, label='',
bmpNormal=wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE, wx.ART_TOOLBAR))
toolbar2.AddTool(tool2)
toolbar2.Realize()
statusbar = wx.StatusBar(parent=self)
# Add all layout elements
bar_flags = common_flags.Proportion(0)
main_box.Add(toolbar1, bar_flags)
main_box.Add(wrap_panels_box, common_flags.Proportion(1))
main_box.Add(toolbar2, bar_flags)
main_box.Add(statusbar, bar_flags)
self.Layout()
def OnQuit(event):
exit(0)
if __name__ == '__main__':
app = MyApp()
app.MainLoop()
All the sub-component generation methods (_GenerateMenu(), _GenerateToolStatusBars(), and _GenerateViewPanels()) work as expected and basically as desired, so I'm leaving them aside.
The various pieces are largely in the right places, but I have a couple quirks here.
1. Status bar expansion
The status bar returned by the _GenerateToolStatusBars() method acts like it has Proportion(1) set on it: it expands or contracts vertically as the main window is expanded vertically. It also has additional space above it. I can make this stop, however, by setting the panel proportions as follows:
bar_flags = common_flags.Proportion(-1)
main_box.Add(toolbar1, bar_flags)
main_box.Add(wrap_panels_box, common_flags.Proportion(0))
main_box.Add(toolbar2, bar_flags)
main_box.Add(statusbar, bar_flags)
A -1 value isn't even documented for the Proportion()[note 2] setting on a Sizer, and the behavior basically matches what I would expect from the original code sample. What's going on here?
2. Later elements in BoxSizer sliding over earlier elements
Regardless of how I have the proportions set (at least between the two options above), the latter items behave as expected with relation to each other. They slide over the first element if the box becomes small, however. So, if I have _GenerateViewPanels() return the panels (as usual), they slide up and cover the top toolbar. If I make that do nothing (none of the normal panels are generated), the next toolbar slides up and covers the top toolbar. To reiterate: none of the bottom toolbars or panels interact with each other that way; they only do it with the first toolbar. As before, I'm confused: what's going on here?
[Edit: Made the above code a fully working sample application.]
Notes:
I'm using a wxPython snapshot build of Phoenix, specifically 2.9.5.81-r73784, running against Python 3.3 on both Windows 7 and OS X. It is possible this is a problem from the snapshot build, but I'm doubtful.
Proportion() is a fancy wrapper for the basic proportion=<value> argument for adding an element to a Sizer. As for why I tried -1, I just mis-remembered the default/base values for the proportion argument to BoxSizer.Add().
The problem is apparently with the distinct Proportion() calls. A little testing and use of the wxPython Widget Inspection Tool makes clear that common_flags is being modified by the calls on it.
All methods of SizerFlags objects return the same object (not a copy of the object), so calling a method updates the object and all references to it – it does not return a copy, but the same object. So, the original code with comments added explaining what went wrong:
common_flags = wx.SizerFlags().Expand().Border(wx.ALL, 1) # creates the object
bar_flags = common_flags.Proportion(0) # bar_flags points to the common_flags object
# Referencing common_flags with Proportion set to 0
main_box.Add(toolbar1, bar_flags)
# Changes the value of common_flags.
main_box.Add(wrap_panels_box, common_flags.Proportion(1))
# Since bar_flags points to common_flags, it also has Proportion set to 1
main_box.Add(toolbar2, bar_flags)
main_box.Add(statusbar, bar_flags)
The solution is simple: declare bar_flags and box_flags as separate objects. This involves some small repetition of code, but it's worth note that you are not repeating the action on the same object; you are performing the same actions on multiple objects. Supplying the following code instead solves the issue:
bar_flags = wx.SizerFlags().Expand().Border(wx.ALL, 1).Proportion(0)
box_flags = wx.SizerFlags().Expand().Border(wx.ALL, 1).Proportion(1)
main_box.Add(tool_status_bars.main, bar_flags)
main_box.Add(wrap_panels_box, box_flags)
main_box.Add(tool_status_bars.panel_view, bar_flags)
main_box.Add(tool_status_bars.status, bar_flags)
As expected, the boxes now relate to each other as they should: the wrap_panels_box expands, while the tool and status bars do not.
Related
I have a bunch of QTextEdits in a QVBoxLayout in a QScrollArea.
The texts can often get very long and the horizontal space is limited by design, and QTextEdit automatically wraps text in multiple lines which is good.
I want to automatically resize the QTextEdit to fit to the wrapped text, the text itself will always be in one line, and the wrapped text can have multiple lines, I want the QTextEdits to fit to the height of wrapped lines.
By hours upon hours of Google searching, I have found a solution, but it doesn't work as expected, there can sometimes be one extra line at the bottom, I will post example code below:
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
font = QFont('Noto Serif', 9)
class Editor(QTextEdit):
doubleClicked = pyqtSignal(QTextEdit)
def __init__(self):
super().__init__()
self.setReadOnly(True)
self.setFont(font)
self.textChanged.connect(self.autoResize)
self.margins = self.contentsMargins()
self.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen)
#pyqtSlot(QMouseEvent)
def mouseDoubleClickEvent(self, e: QMouseEvent) -> None:
self.doubleClicked.emit(self)
def autoResize(self):
self.show()
height = int(self.document().size().height() + self.margins.top() + self.margins.bottom())
self.setFixedHeight(height)
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.resize(405, 720)
frame = self.frameGeometry()
center = self.screen().availableGeometry().center()
frame.moveCenter(center)
self.move(frame.topLeft())
self.centralwidget = QWidget(self)
self.vbox = QVBoxLayout(self.centralwidget)
self.scrollArea = QScrollArea(self.centralwidget)
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.verticalLayout = QVBoxLayout(self.scrollAreaWidgetContents)
self.verticalLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.scrollArea.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
self.vbox.addWidget(self.scrollArea)
self.setCentralWidget(self.centralwidget)
def addItems():
items = [
"L'Estro Armonico No. 9 in D Major\u2014III. Allegro",
"L'Estro Armonico No. 6 in A Minor\u2014III. Presto",
"L'Estro Armonico No. 6 in A Minor\u2014I. Allegro",
"L'Estro Armonico No. 1 in D major\u2014I. Allegro",
"12 Concertos Op.3 \u2014 L'estro Armonico \u2014 Concerto No. 6 In A Minor For Solo Violin RV 356\u2014Presto",
"Ultimate Mozart\u2014 The Essential Masterpieces",
"Serenade in G K.525 Eine kleine Nachtmusik\u20141. Allegro",
"Vivaldi\u2014 L'estro Armonico",
"Academy of St. Martin in the Fields",
"Are You With Me \u2014 Reality"
]
for i in items:
textbox = Editor()
textbox.setText(i)
window.verticalLayout.addWidget(textbox)
app = QApplication([])
window = Window()
window.show()
addItems()
app.exec()
Note you will need Noto Serif for it to run correctly (and of course you can just replace it), and of course you need PyQt6.
In the example, the first seven textboxs all have one extra empty line at the bottom, and the last three don't have it.
What caused the extra line and how to remove it?
Update:
I have to set Qt.WidgetAttribute.WA_DontShowOnScreen because if I don't set it, calling .show() of QTextEdit will cause the QTextEdit show up in the middle of the screen and quickly disappear, and it's annoying.
I have to call .show() because calling document().size() without show(), the values will all be 0 and I don't know why it is like this.
The problem comes from the fact that you're trying to set the height too early. In fact, if you add a print(self.show()) just after show(), you'll see that all will show a default size (probably, 256x192).
This depends on two aspects:
when a widget is shown the first time, it's not yet completely "mapped" in the OS window management, so it will use default sizes depending on many aspects;
you're setting the text before adding it to the layout, so the QTextEdit will know nothing about the required size of the parent;
Then another problem arises: if the window is resized, the contents will not adapt until the text is changed.
In order to properly set a vertical height based on the contents, you should set the document's textWidth, and also call autoResize everytime the widget is resized.
class Editor(QTextEdit):
def __init__(self):
super().__init__()
self.setReadOnly(True)
self.setFont(font)
self.textChanged.connect(self.autoResize)
def autoResize(self):
self.document().setTextWidth(self.viewport().width())
margins = self.contentsMargins()
height = int(self.document().size().height() + margins.top() + margins.bottom())
self.setFixedHeight(height)
def resizeEvent(self, event):
self.autoResize()
Note that:
the margins should be dynamically accessed, not stored in the __init__;
mouseDoubleClickEvent is a function that is called on a mouse event, it's not (nor it should) be a slot, so using the pyqtSlot decorator is pointless;
while conceptually fine for a "main" layout like in the case of a layout for the scroll area contents, setting the alignment of a layout doesn't set the alignment of its items, but only that of the layout; while the result is often the same, in practice it's very different (so the result is not always the same, especially if more layouts are added to the same parent layout);
double click in text fields is very commonly used for advanced selection (usually, select the word under the cursor), and choosing to prevent such action (thus changing a known UI convention) should be taken into careful consideration;
I'm training on wxpython by making a window with a menu bar and a status bar.
I'm on mac os so maybe that it works differently because I don't know what is the problem with my code, but I didn't find anything on the Internet.
Here is my code :
#!/usr/bin/python
# coding: utf-8
import wx
class Menus(wx.Frame):
def __init__(self, ptitle):
wx.Frame.__init__(self, None, 1, title = ptitle, size = (500, 300))
menuFile = wx.Menu()
menuFile.Append(wx.ID_OPEN, "&Open\tCTRL+o")
menuFile.Append(wx.ID_CLOSE, "&Close\tCTRL+c")
menuFile.AppendSeparator()
menuFile.Append(wx.ID_EXIT, "&Quit\tCTRL+q")
menuBar = wx.MenuBar()
menuBar.Append(menuFile, "&File")
self.SetMenuBar(menuBar)
self.bar = wx.StatusBar(self, 1)
self.bar.SetFieldsCount(2)
self.bar.SetStatusWidths([1,1])
self.SetStatusBar(self.bar)
self.Bind(wx.EVT_MENU, self.OnExit, id=wx.ID_EXIT)
self.Bind(wx.EVT_MENU, self.OnOpen, id=wx.ID_OPEN)
self.Bind(wx.EVT_MENU, self.OnClose, id=wx.ID_CLOSE)
def OnOpen(self, evt):
self.bar.SetStatusText("Choice -> open", 1)
def OnClose(self, evt):
self.bar.SetStatusText("Choice -> close", 1)
def OnExit(self, evt):
self.Destroy()
class App(wx.App):
def OnInit(self):
window = Menus("Window with menu")
window.Show(True)
self.SetTopWindow(window)
return True
app = App()
app.MainLoop()
When I click on Open or Close, there is no text on the status bar, but there is the status bar. If I choose instead of setting the status text to print something in the terminal, it works fine. I also tried tu write self.bar.SetStatusText("Text") and it doesn't work neither.
It would be great if someone knows where is the problem with this status bar.
Thank you
With self.bar.SetStatusWidths([1,1]) you are setting the widths to 1 pixel and 1 pixel wide respectively.
You should use [-1,-1] (equal), [-1,-2] (part 2 twice as large as part 1) etc
or use fixed widths [150,200] for example.
There are two types of fields: fixed widths and variable width fields. For the fixed width fields you should specify their (constant) width in pixels. For the variable width fields, specify a negative number which indicates how the field should expand: the space left for all variable width fields is divided between them according to the absolute value of this number. A variable width field with width of -2 gets twice as much of it as a field with width -1 and so on.
For example, to create one fixed width field of width 100 in the right part of the status bar and two more fields which get 66% and 33% of the remaining space correspondingly, you should use an array containing -2, -1 and 100.
The "Close current document" message, I suspect, is internal, very much like the automatic icon added to the menu.
Finally, self.bar.SetStatusText("Text") should read self.bar.SetStatusText("Text",1) where the 1 is the index of the status bar field that you want the text to be displayed in.
e.g.
self.bar = wx.StatusBar(self, 1)
self.bar.SetFieldsCount(3)
self.bar.SetStatusWidths([200,-1,-2])
self.SetStatusBar(self.bar)
self.bar.SetStatusText("Second position",1)
self.bar.SetStatusText("Third position",2)
I am writing a small app that works very well on linux, but I have some trouble on windows. Here is the code sample:
import wx
#####################################################################
class Main(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="StackOverflow", pos=wx.DefaultPosition, size=(800,600))
self.SetMinSize( self.GetSize() )
p = wx.Panel(self)
nb = wx.Notebook(p)
page1 = AddToCollection(nb)
page2 = CollectionStatistics(nb)
nb.AddPage(page1, "Page 1")
nb.AddPage(page2, "Page 2")
# finally, put the notebook in a sizer for the panel to manage
# the layout
sizer = wx.BoxSizer()
sizer.Add(nb, 1, wx.EXPAND)
p.SetSizer(sizer)
#########################################################################
class CollectionStatistics(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
#########################################################################
class AddToCollection(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.v1_qty_list = [str(x) for x in range(9)]
self.v2_qty_list = [str(x) for x in range(9)]
self.sizername = wx.GridBagSizer(5, 5)
self.sizername.AddGrowableCol(0,0)
self.name_txt = wx.StaticText(self, label="Enter Name :")
self.sizername.Add(self.name_txt,(2,0),(1,1),wx.EXPAND)
self.name = wx.TextCtrl(self,style=wx.TE_PROCESS_ENTER,value=u"")
self.sizername.Add(self.name,(3,0),(1,1),wx.EXPAND)
self.Bind(wx.EVT_TEXT_ENTER, self.OnPressEnter, self.name)
self.SetSizerAndFit(self.sizername)
self.SetSizeHints(-1,self.GetSize().y,-1,self.GetSize().y )
##########################################################################
def OnPressEnter(self,event):
self.selected_name = self.name.GetValue()
self.AddToCol()
##########################################################################
def AddToCol(self):
self.sizerAdd = wx.GridBagSizer(5, 5)
self.sizerAdd.AddGrowableCol(0, 0)
self.name.Enable(False)
### Expansion
self.expansion = wx.Choice(self, -1, choices=['test 1', 'test 2'])
self.expansion.SetSelection(0)
self.sizerAdd.Add(self.expansion,(5,0),(1,6),wx.EXPAND)
### Quantities txt
self.v1_txt = wx.StaticText(self, label="V1 Quantity :")
self.sizerAdd.Add(self.v1_txt,(7,0),(1,1),wx.EXPAND)
self.v2_txt = wx.StaticText(self, label="V2 Quantity :")
self.sizerAdd.Add(self.v2_txt,(8,0),(1,1),wx.EXPAND)
### Quantities choices
self.v1_qty = wx.Choice(self, -1, choices=self.v1_qty_list)
self.v1_qty.SetSelection(0)
self.sizerAdd.Add(self.v1_qty,(7,5),(1,1),wx.EXPAND)
self.v2_qty = wx.Choice(self, -1, choices=self.v1_qty_list)
self.v2_qty.SetSelection(0)
self.sizerAdd.Add(self.v2_qty,(8,5),(1,1),wx.EXPAND)
### Ok Button
self.Add_btn = wx.Button(self, -1, "Add")
self.Add_btn.Bind(wx.EVT_BUTTON, self.OnAdd)
self.sizerAdd.Add(self.Add_btn,(9,5),(1,1),wx.EXPAND)
### Reset Button
self.Reset_btn = wx.Button(self, -1, "Reset")
self.Reset_btn.Bind(wx.EVT_BUTTON, self.OnResetPanel)
self.sizerAdd.Add(self.Reset_btn,(9,4),(1,1),wx.EXPAND)
self.SetSizerAndFit(self.sizerAdd)
self.SetSizeHints(-1,self.GetSize().y,-1,self.GetSize().y )
######################################################################
def OnResetPanel(self,event):
### Kill all children
self.expansion.Destroy()
self.v1_txt.Destroy()
self.v1_qty.Destroy()
self.v2_txt.Destroy()
self.v2_qty.Destroy()
self.Add_btn.Destroy()
self.Reset_btn.Destroy()
### Reinitialise sizer
self.name.Enable(True)
self.name.SetValue("")
######################################################################
def OnAdd(self,event):
print 'Add'
self.OnResetPanel(self)
######################################################################
######################################################################
if __name__ == "__main__":
app = wx.App()
Main().Show()
app.MainLoop()
Basically, I have a TextCtrl in a first sizer which is waiting for an entry. Once the user hits enter, several objects appear in a second sizer.
The issue on windows seems to come from the use of the two gridbagsizers (sizername and sizerAdd). After pressing enter (waited event in the __init__), the objects defined within the sizerAdd do not appear. When I extend the window where the script is running, these objects appear magically !
Any idea ?
EDIT : The code is now runnable
I think the problem in your code is these two lines at the end of your AddToCol method:
self.SetSizerAndFit(self.sizerAdd)
self.SetSizeHints(-1,self.GetSize().y,-1,self.GetSize().y )
At this point, you're changing the sizer of the AddToCollection panel from self.sizername to self.sizerAdd. The Enter Name: label and the textbox however are still within the self.sizername sizer. However, this sizer isn't the sizer for any window, nor has it been added to any other sizer.
Generally, in wxPython, every sizer should be set as the sizer for a window, or be added to another sizer. This other sizer would then be the sizer for a window, or be contained within another sizer, and so on. In your case, your self.sizername sizer ends up being neither, and in this situation I would expect unpredictable behaviour. If your code works on Linux then I would say that it happens to work by accident.
I can think of a few things you could do here:
Add self.sizerAdd as a child of self.sizername. This can be done by replacing the two lines above with
self.sizername.Add(self.sizerAdd,(4,0),(1,1),wx.EXPAND)
self.sizername.Layout()
In AddToCol, add the widgets directly to the self.sizername sizer instead of adding them to self.sizerAdd.
Create a wx.BoxSizer() with vertical orientation, set that to be the sizer for the AddToCollection panel, and add the self.sizername and self.sizerAdd sizers to your BoxSizer.
In all three cases, after creating the new widgets you will need to call the Layout() method on the top-level sizer, be it either self.sizername or the top-level BoxSizer. The code snippet under option 1 includes this line already.
Additionally, you may need to modify your OnResetPanel() method. If you chose options 1 or 3, you will need to remove the self.sizerAdd sizer from whichever sizer you added it to. For example, in option 1, you would add the line
self.sizername.Remove(self.sizerAdd)
Another approach would be for your AddToCol method to create all the widgets within a Panel and add that to the main panel at the end. Your AddToCol method would then need to create a child panel, add the extra controls as children of this panel instead of the main panel (self), set the sizer of the child panel to self.sizerAdd and finally add this panel to the self.sizername sizer.
def AddToCol(self):
self.sizerAdd = wx.GridBagSizer(5, 5)
self.sizerAdd.AddGrowableCol(0, 0)
self.name.Enable(False)
self.child_panel = wx.Panel(self)
### Expansion
self.expansion = wx.Choice(self.child_panel, -1, choices=['test 1', 'test 2'])
self.expansion.SetSelection(0)
self.sizerAdd.Add(self.expansion,(5,0),(1,6),wx.EXPAND)
# Create other widgets as before but with the parent set to self.child_panel
# instead of self.
self.child_panel.SetSizer(self.sizerAdd)
self.sizername.Add(self.child_panel,(4,0),(1,1),wx.EXPAND)
self.sizername.Layout()
You would then also need to replace the line
self.sizername.Remove(self.sizerAdd)
in OnResetPanel() with the two lines:
self.sizername.Remove(self.child_panel)
self.child_panel.Destroy()
One thing which bugged me about my approach 1 above was that I saw the widgets briefly appear in the top-left corner before appearing in the correct place. This adaptation fixes this problem and so makes the GUI behave itself a bit better. I couldn't reproduce your black area issue you mention in your comment, but hopefully this approach fixes your problem as well.
I am trying to create a project tool with a wx.Notebook with tabs at the top and a general panel at the bottom. The bottom panel should be independent of the notebook and not change when the tabs change. When I only add the notebook the notebook itself works fine, but when I also add my bottom panel (an extended wx.Panel) the notebook gets squeezed like shown in the image here.
I have one panel for the window and add the notebook and bottom panel (called BottomGroup, extending wx.Panel) to separate panels. Can anyone spot the trouble? Maybe something with the sizers? My Window is like this (nevermind that the tabbing is wrong):
class Window(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, None, wx.ID_ANY, title)
self.InitUI()
def InitUI(self):
menuBar = wx.MenuBar()
menu = wx.Menu()
menu_load = menu.Append(wx.ID_OPEN, 'Open', 'Open project')
menu_save = menu.Append(wx.ID_SAVE, 'Save', 'Save project')
menu_save_as = menu.Append(wx.ID_SAVEAS, 'Save as', 'Save project as')
menu_exit = menu.Append(wx.ID_EXIT, 'Quit', 'Quit application')
menuBar.Append(menu, '&File')
self.SetMenuBar(menuBar)
mainPanel = wx.Panel(self)
self.noteBookPanel = wx.Panel(mainPanel)
self.notebook = wx.Notebook(self.noteBookPanel)
self.bottomPanel = wx.Panel(mainPanel)
self.bottomGroup = BottomGroup(self.bottomPanel)
mainSizer = wx.BoxSizer(wx.VERTICAL)
self.pageNodePathsTables = PageNodesPathsTables(self.notebook)
self.pageEscapeCriteria = PageEscapeCriteria(self.notebook)
self.pageFileHandling = PageFileHandling(self.notebook)
self.notebook.AddPage(self.pageNodePathsTables, "Define Paths and Nodes")
self.notebook.AddPage(self.pageEscapeCriteria, "Define Escape Criteria")
self.notebook.AddPage(self.pageFileHandling, "File Handling")
mainSizer.Add(self.noteBookPanel,1,wx.TOP)
mainSizer.Add(self.bottomGroup,1,wx.BOTTOM)
self.Bind(wx.EVT_MENU, self.onSave, menu_save)
self.Bind(wx.EVT_MENU, self.onLoad, menu_load)
self.Bind(wx.EVT_MENU, self.OnQuit, menu_exit)
self.SetDimensions(WindowOpenX,WindowOpenY,WindowWidth,WindowHeight)
self.Show(True)
Update:
I have refactored my code to this (only the refactored part shown):
self.notebook = wx.Notebook(self)
self.bottomGroup = BottomGroup(self)
self.setupMenu()
#frameSizer = wx.GridBagSizer(rowGap,columnGap)
#frameSizer.Add(self.notebook,pos=(0,0), span=(1,1),
# flag=wx.LEFT|wx.TOP|wx.EXPAND, border = 5)
#frameSizer.Add(self.bottomGroup,pos=(1,0), span=(1,1),
# flag=wx.LEFT|wx.BOTTOM|wx.EXPAND, border = 5)
frameSizer = wx.BoxSizer(wx.VERTICAL)
frameSizer.Add(self.notebook, 2, wx.EXPAND)
frameSizer.Add(self.bottomGroup,0)
self.SetSizer(frameSizer)
where self.setupMenu() is defined as:
def setupMenu(self):
self.pageNodePathsTables = PageNodesPathsTables(self.notebook)
self.pageEscapeCriteria = PageEscapeCriteria(self.notebook)
self.pageFileHandling = PageFileHandling(self.notebook)
self.pagePlotHistoryData = PagePlotHistoryData(self.notebook)
self.pageCalculateEscape = PageCalculateEscape(self.notebook)
self.notebook.AddPage(self.pageNodePathsTables, "Define Paths and Nodes")
self.notebook.AddPage(self.pageEscapeCriteria, "Define Escape Criteria")
self.notebook.AddPage(self.pageFileHandling, "File Handling")
self.notebook.AddPage(self.pagePlotHistoryData, "Plot History Data")
self.notebook.AddPage(self.pageCalculateEscape, "Calculate Escape")
This is a lot clearer and easier than the above code. It works fine, except that the bottomGroup is now stacked upon the notebook (i.e. both elements start at the upper left corner of the wx.Frame). I have tried both the wx.BoxSizer and the wx.GridBagLayout (as commented out above). Do you have any suggestions to this problem?
My BottomGroup is defined like this:
class BottomGroup(wx.Panel):
def __init__(self,parent):
wx.Panel.__init__(self,parent)
panelSizer = wx.GridBagSizer(rowGap,columnGap)
btnSaveProject = wx.Button(parent, label="Save project", size=(100,50))
btnLoadProject = wx.Button(parent, label="Open project", size=(100,50))
panelSizer.Add(btnSaveProject, pos=(0,0), span=(1,1),
flag=wx.EXPAND|wx.LEFT, border = borderWidth)
panelSizer.Add(btnLoadProject, pos=(0,1), span=(1,1),
flag=wx.EXPAND|wx.LEFT, border = borderWidth)
self.SetSizer(panelSizer)
My main method is like this:
if __name__ == '__main__':
app = wx.App()
Window(None, WindowHeader)
app.MainLoop()
I don't want to provide you with a full solution, but rather a little advice on UI programming.
You screwed up by creating to many panels and never setting a sizer. Before writing UI code always think of the structure (hierarchy) of widgets you want to create. Think of their layout. Then write down the code, grouped by principle of locality: approximately one level of the hierarchy should be handled in a few lines.
Good, let's apply the principle. Notebook and bottom panel at top level:
wx.Frame
+- wx.NoteBook
+- PageNodesPathsTables
+- ...
+- BottomGroup
At this level, everything comes down to these four, simple lines:
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.notebook, 2, wx.EXPAND)
sizer.Add(self.bottomPanel, 0)
self.SetSizer(sizer)
Obviously you know how to handle a wx.NoteBook properly. Very important is the call to self.SetSizer (non-existent in your code), because otherwise the frame has no idea which sizer it should use for layouting its children.
You demands about the partitioning in two halves where pretty clear. Words of wisdom: Don't do more nesting than needed. This might be more extensible (and can easily be postponed), but at the moment it's not needed. If you do something like this:
self.bottomPanel = wx.Panel(mainPanel)
self.bottomGroup = BottomGroup(self.bottomPanel)
You have introduced at least one level (self.bottomPanel) with a single child (self.bottomGroup) that has no clue how to layout its child components. Erase the unnecessary nesting, and you will get rid of panel creation, sizer creation, sizer assignment, and so on. Collapsing it will get you a quicker overview. Todo:
Rewrite this section of you program. Get rid of superfluous instructions.
Make sure every panel has a sizer.
Refactor menu creation in a separate method.
Parameter parent in the Window constructor is unused. Fix.
Your example was missing a main method, imports, etc. Next time, supply an SSCCE.
I'm attempting to set up something like this:
However, I'm having a bunch of trouble in figuring it out. I made this general purpose function to wrap a list of objects in a wx.gridsizer, and then add that to a wx.StaticBoxSizer to get the border around everything. Then it return the staticBox sizer to main to be added to the main vertical boxsizer.
def buildHorizontalSizer(self, objects, label=None):
if label:
box = wx.StaticBox(self.panel, -1, label)
# for i in dir(box):
# print i
sizer = wx.StaticBoxSizer(box, wx.HORIZONTAL)
else:
sizer = wx.BoxSizer(wx.HORIZONTAL)
grid = wx.GridBagSizer(hgap=3, vgap=0)
for i in range(len(objects)):
if i==0:
grid.Add(objects[i], flag=wx.ALIGN_RIGHT)
else:
grid.Add(objects[i], flag=wx.ALIGN_LEFT)
sizer.Add(grid)
return sizer
In each field, from left to right, there is a StaticText, TextCtrl, and then a Button.
How do I configure the cells so that they have different widths?
use
objects[i].SetMinSize((width,height))
Should do what you want ... you may have to also call
objects[i].SetSize((width,height))
before adding it to the sizer