wxpython: wxWidgets with Python
wxWidgets is a way to write Graphical User Interfaces (GUIs, with buttons, menus, etc) for Win32, Mac OS X, etc from C++, Python, Perl, and C#/.NET. It’s used in our IIA Software Engineering Project. Though it’s cross-platform, the resulting programs don’t look the same on all machines – they follow the look-and-feel of the machine. Whereas other graphics libraries we use at CUED (OpenGL and GLUT) aren’t Object-Oriented, wxWidgets is, so you need some awareness of using classes.
Versions
This document briefly introduces wxWidgets 3.0
when used with Python 3, highlighting a few issues that sometimes puzzle newcomers. Knowing which version you’re using matters, especially when you reports bugs.
- On Linux, type “
uname -a
” to find out the operating system version, or search in Settings. - Type
python
to find the Python version, then typeimport wx wx.version()
to find the wxWidgets version.
These examples were developed on “18.04.1-Ubuntu” using “Python 3.7.6” and “4.0.4 gtk2 (phoenix) wxWidgets 3.0.5”. Run on different systems these programs will have a different appearance to the screendumps on this page.
Concepts
The basic concepts are much the same as in most graphics systems.
- Widgets (things – windows, buttons, etc). These have properties like color, size, etc. Some widgets (e.g. frames) can have other widgets inside them.
wxGLCanvas
is a useful widget that allows you to use OpenGL commands in a window. This lets you use advanced graphics. For simpler drawing you can use the wxDC class. - Events (triggering actions – keypresses, window resizing, mouse clicks, etc). When an event happens, your program might want to react to it. You can arrange for particular events in certain widgets to cause a routine to run. These routines are referred to as “callbacks” or “handlers”.
- Sizers – these are layout managers: they organise widgets when they’re within other widgets. If for example you want a row of buttons in a panel, you don’t have to calculate sizes and coordinates when the main window is resized. If you specify for each button a minimal size, a stretch factor, a border, and what’s to the left of it, the sizer routine will manage the re-drawing.
Programs with GUIs often have quite a different organisation to non-GUI programs. What you do is create the widgets and set things up so that the appropriate callbacks are called. Then you call the main event-loop, giving control over to the application. From then on, the program waits for events. When they happen, the program calls the appropriate callback then waits for the next event. Exiting the loop exits from the program.
You’ll need to understand about classes and inheritance in order to make the most of the documentation. For example the page about GLCanvas
tells you about the extra facilities that GLCanvas
offers and shows you what classes it inherits from. What it doesn’t say explicitly is that GLCanvas
may also have access to the functions offered by the inherited classes which are documented elsewhere.
Identifiers
Each component needs to be given an integer identifier. You can provide explicit values. There are some Standard Event Identifiers – wx.ID_EXIT
, etc. Using wx.ID_ANY
lets wxWidgets assign an unused identifier to the component automatically.
Writing a program
Here are the main steps. Full code is in the next section
- Create a
wx.App
object. This is the top level of your program. - Create an object derived from the frame class – this will be the parent of widgets
- Create widgets, and put them in sizers. Put the sizers in frames or sizers
- Write the handlers to deal with events
- Launch the app
Demo 1 – The basics
This program creates a window with a button, some text, and a menu bar. The side_sizer
manages the text and the button. The main_sizer
manages the side_sizer
. Note that you can resize the window, but there’s a minimum size.
# Adapted from an example by Dr Gee (CUED) import wx class Gui(wx.Frame): def __init__(self, title): """Initialise widgets and layout.""" super().__init__(parent=None, title=title, size=(400, 400)) # Configure the file menu fileMenu = wx.Menu() menuBar = wx.MenuBar() fileMenu.Append(wx.ID_EXIT, "&Exit") menuBar.Append(fileMenu, "&File") self.SetMenuBar(menuBar) # Configure the widgets self.text = wx.StaticText(self, wx.ID_ANY, "Some text") self.button1 = wx.Button(self, wx.ID_ANY, "Button1") # Bind events to widgets self.Bind(wx.EVT_MENU, self.OnMenu) self.button1.Bind(wx.EVT_BUTTON, self.OnButton1) # Configure sizers for layout main_sizer = wx.BoxSizer(wx.HORIZONTAL) side_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.Add(side_sizer, 1, wx.ALL, 5) side_sizer.Add(self.text, 1, wx.TOP, 10) side_sizer.Add(self.button1, 1, wx.ALL, 5) self.SetSizeHints(300, 300) self.SetSizer(main_sizer) def OnMenu(self, event): """Handle the event when the user selects a menu item.""" Id = event.GetId() if Id == wx.ID_EXIT: self.Close(True) def OnButton1(self, event): """Handle the event when the user clicks button1.""" print ("Button 1 pressed") app = wx.App() gui = Gui("Demo") gui.Show(True) app.MainLoop()
The following part of the constructor for Gui
may need an explanation.
super().__init__(parent=None, title=title, size=(400, 400))
When a derived object is created, it will by default first call the default constructor of the class it’s derived from (the superclass). In this case however, we don’t want to call wx.Frame
‘s default constructor, we want to pass some arguments. It’s a constructor “with a member initialiser list”
Troubleshooting
In general, if your program’s Python is legal, but doesn’t work properly
- Start from an earlier version that works and add a line or two at a time.
- Check to see if you’re using a faultly implementation – the code’s supposed to work the same on all platforms, but that’s not always so in practise. Commands that are optional on some systems are required on others.
- If widgets don’t appear, or are in the wrong place when you resize the window, check that the widgets are being managed by the appropriate sizers, and the sizers are in turn associated with a parent sizer or frame. Nothing should be unmanaged. Drawing a diagram may help. In the example above
side_sizer
stacks its contents (text and a button) vertically.main_sizer
arranges its contents (side_sizer
) horizontally, and is associated withGui
(demo1.py:2299): Gtk-WARNING **: 07:47:43.883: Unable to locate theme engine in module_path: "adwaita"
when I start a wxWidgets program. They’re harmless, and eventually removable.Segmentation fault (core dumped)
message the system may not give you much of a clue about your mistake. The message means that you’ve managed to make some C++ code crash (which isn’t hard to do when using wxWidgets). Check carefully the arguments used in the call that seems to be the cause.Demo 2 – Toolbars
This extension of the first program adds a toolbar whose final “Exit” icon has a callback, and whose second “open file” icon creates a wxFileDialog
to get a “*txt” filename from the user. The icons for the toolbox are supplied with wxWidgets
(or, optionally, the operating system)
# Adapted from an example by Dr Gee (CUED) import wx from wx import ArtProvider class Gui(wx.Frame): QuitID=999 OpenID=998 def __init__(self, title): """Initialise widgets and layout.""" super().__init__(parent=None, title=title, size=(400, 400)) QuitID=999 OpenID=998 locale = wx.Locale(wx.LANGUAGE_ENGLISH) # Configure the file menu fileMenu = wx.Menu() menuBar = wx.MenuBar() fileMenu.Append(wx.ID_EXIT, "&Exit") menuBar.Append(fileMenu, "&File") self.SetMenuBar(menuBar) toolbar=self.CreateToolBar() myimage=wx.ArtProvider.GetBitmap(wx.ART_NEW, wx.ART_TOOLBAR) toolbar.AddTool(wx.ID_ANY,"New file", myimage) myimage=wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_TOOLBAR) toolbar.AddTool(OpenID,"Open file", myimage) myimage=wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE, wx.ART_TOOLBAR) toolbar.AddTool(wx.ID_ANY,"Save file", myimage) myimage=wx.ArtProvider.GetBitmap(wx.ART_QUIT, wx.ART_TOOLBAR) toolbar.AddTool(QuitID,"Quit", myimage) toolbar.Bind(wx.EVT_TOOL, self.Toolbarhandler) toolbar.Realize() self.ToolBar = toolbar # Configure the widgets self.text = wx.StaticText(self, wx.ID_ANY, "Some text") self.button1 = wx.Button(self, wx.ID_ANY, "Button1") # Bind events to widgets self.Bind(wx.EVT_MENU, self.OnMenu) self.button1.Bind(wx.EVT_BUTTON, self.OnButton1) # Configure sizers for layout main_sizer = wx.BoxSizer(wx.HORIZONTAL) side_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.Add(side_sizer, 1, wx.ALL, 5) side_sizer.Add(self.text, 1, wx.TOP, 10) side_sizer.Add(self.button1, 1, wx.ALL, 5) self.SetSizeHints(300, 300) self.SetSizer(main_sizer) def OnMenu(self, event): """Handle the event when the user selects a menu item.""" Id = event.GetId() if Id == wx.ID_EXIT: print("Quitting") self.Close(True) def OnButton1(self, event): """Handle the event when the user clicks button1.""" print ("Button 1 pressed") def Toolbarhandler(self, event): if event.GetId()==self.QuitID: print("Quitting") self.Close(True) if event.GetId()==self.OpenID: openFileDialog= wx.FileDialog(self, "Open txt file", "", "", wildcard="TXT files (*.txt)|*.txt", style=wx.FD_OPEN+wx.FD_FILE_MUST_EXIST) if openFileDialog.ShowModal() == wx.ID_CANCEL: print("The user cancelled") return # the user changed idea... print("File chosen=",openFileDialog.GetPath()) app = wx.App() gui = Gui("Demo") gui.Show(True) app.MainLoop()
Finding windows
After creating a window or a button, you may need to access it again from a different function from where it was created. To do this you’ll need to store the value returned when the window was created and make that value visible from elsewhere. Alternatively you can use wXwidget’s FindWindowById
, FindWindowByLabel
, or FindWindowByName
function to find the window when you need it. The next program shows how to do this.
Demo 3 – Scrollbars
A varient of the wxWindow
called wx.ScrolledWindow
can be used if you want scrollbars with automatic callbacks. Compared with the previous program the program below has an extra window with a Run button. This extra window has scrollbars that will only appear when the displayed window dimensions are less than the actual window size (in the illustrated situation the displayed width but not the height is too small). This code also shows FindWindowByName
in use – if you click on Button 2, the text on Button 1 will change.
# Adapted from an example by Dr Gee (CUED) import wx class Gui(wx.Frame): def __init__(self, title): """Initialise widgets and layout.""" super().__init__(parent=None, title=title, size=(400, 100)) # Configure the file menu fileMenu = wx.Menu() menuBar = wx.MenuBar() fileMenu.Append(wx.ID_EXIT, "&Exit") menuBar.Append(fileMenu, "&File") self.SetMenuBar(menuBar) button_sizer = wx.BoxSizer(wx.HORIZONTAL) # Configure the widgets self.text = wx.StaticText(self, wx.ID_ANY, "Some text") button_sizer.Add(self.text, 1, wx.TOP+wx.LEFT+wx.RIGHT, 5) self.button1 = wx.Button(self, wx.ID_ANY, label="Button 1", name="SomeNameOrOther") button_sizer.Add(self.button1, 1, wx.RIGHT, 5) self.button2 = wx.Button(self, wx.ID_ANY, "Button2") button_sizer.Add(self.button2 , 1, wx.RIGHT, 5) # Bind events to widgets self.Bind(wx.EVT_MENU, self.OnMenu) self.button1.Bind(wx.EVT_BUTTON, self.OnButton1) self.button2.Bind(wx.EVT_BUTTON, self.OnButton2) # Configure sizers for layout main_sizer = wx.BoxSizer(wx.HORIZONTAL) side_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.Add(side_sizer, 1, wx.ALL, 5) side_sizer.Add(self.text, 1, wx.TOP, 10) side_sizer.Add(self.button1, 1, wx.ALL, 5) side_sizer.Add(self.button2, 1, wx.ALL, 5) self.SetSizeHints(300, 100) self.SetSizer(button_sizer) controlwin = wx.ScrolledWindow(self, -1, wx.DefaultPosition, wx.DefaultSize, wx.SUNKEN_BORDER|wx.HSCROLL|wx.VSCROLL) button_sizer.Add(controlwin,1, wx.EXPAND | wx.ALL, 10) button_sizer2 = wx.BoxSizer(wx.VERTICAL) controlwin.SetSizer(button_sizer2) controlwin.SetScrollRate(10, 10) controlwin.SetAutoLayout(True) button_sizer2.Add(wx.Button(controlwin, wx.ID_ANY, "Run"), 0, wx.ALL, 10) def OnMenu(self, event): """Handle the event when the user selects a menu item.""" Id = event.GetId() if Id == wx.ID_EXIT: print("Quitting") self.Close(True) def OnButton1(self, event): """Handle the event when the user clicks button1.""" print ("Button 1 pressed") def OnButton2(self, event): """Now find button 1 and change its label.""" print ("Button 2 pressed") tmp=wx.FindWindowByName("SomeNameOrOther") if tmp!=None: tmp.SetLabel("Button 1 updated") app = wx.App() gui = Gui("Demo") gui.Show(True) app.MainLoop()
Demo 4 – Canvas with scrollbars
There are various ways of doing this. I don’t know if the method used here is best. Because of the way ShowScrollbars
is used, the horizontal scrollbar will always be visible. The vertical scrollbar will disappear if the window’s big enough.
import wx import wx.glcanvas as wxcanvas from OpenGL import GL, GLUT class MyGLCanvas(wxcanvas.GLCanvas): def __init__(self, parent,id,pos,size): """Initialise canvas properties and useful variables.""" super().__init__(parent, -1,pos=pos,size=size, attribList=[wxcanvas.WX_GL_RGBA, wxcanvas.WX_GL_DOUBLEBUFFER, wxcanvas.WX_GL_DEPTH_SIZE, 16, 0]) GLUT.glutInit() self.init = False self.context = wxcanvas.GLContext(self) # Initialise variables for panning self.pan_x = 0 self.pan_y = 0 # Initialise variables for zooming self.zoom = 1 # Bind events to the canvas self.Bind(wx.EVT_PAINT, self.on_paint) self.Bind(wx.EVT_SIZE, self.on_size) def init_gl(self): """Configure and initialise the OpenGL context.""" size = self.GetClientSize() self.SetCurrent(self.context) GL.glDrawBuffer(GL.GL_BACK) GL.glClearColor(1.0, 1.0, 1.0, 0.0) GL.glViewport(0, 0, size.width, size.height) GL.glMatrixMode(GL.GL_PROJECTION) GL.glLoadIdentity() GL.glOrtho(0, size.width, 0, size.height, -1, 1) GL.glMatrixMode(GL.GL_MODELVIEW) GL.glLoadIdentity() GL.glTranslated(self.pan_x, self.pan_y, 0.0) GL.glScaled(self.zoom, self.zoom, self.zoom) def render(self, text): """Handle all drawing operations.""" self.SetCurrent(self.context) if not self.init: # Configure the viewport, modelview and projection matrices self.init_gl() self.init = True # Clear everything GL.glClear(GL.GL_COLOR_BUFFER_BIT) # Draw specified text at position (10, 10) self.render_text(text, 10, 10) # Draw a sample signal trace GL.glColor3f(0.0, 0.0, 1.0) # signal trace is blue GL.glBegin(GL.GL_LINE_STRIP) for i in range(10): x = (i * 20) + 10 x_next = (i * 20) + 30 if i % 2 == 0: y = 75 else: y = 100 GL.glVertex2f(x, y) GL.glVertex2f(x_next, y) GL.glEnd() # We have been drawing to the back buffer, flush the graphics pipeline # and swap the back buffer to the front GL.glFlush() self.SwapBuffers() def on_paint(self, event): """Handle the paint event.""" self.SetCurrent(self.context) if not self.init: # Configure the viewport, modelview and projection matrices self.init_gl() self.init = True size = self.GetClientSize() text = "".join(["Canvas redrawn on paint event, size is ", str(size.width), ", ", str(size.height)]) self.render(text) def on_size(self, event): """Handle the canvas resize event.""" # Forces reconfiguration of the viewport, modelview and projection # matrices on the next paint event self.init = False def render_text(self, text, x_pos, y_pos): """Handle text drawing operations.""" GL.glColor3f(0.0, 0.0, 0.0) # text is black GL.glRasterPos2f(x_pos, y_pos) font = GLUT.GLUT_BITMAP_HELVETICA_12 for character in text: if character == '\n': y_pos = y_pos - 20 GL.glRasterPos2f(x_pos, y_pos) else: GLUT.glutBitmapCharacter(font, ord(character)) class Gui(wx.Frame): def __init__(self, title): """Initialise widgets and layout.""" super().__init__(parent=None, title=title, size=(300, 200)) # Configure the file menu fileMenu = wx.Menu() menuBar = wx.MenuBar() fileMenu.Append(wx.ID_EXIT, "&Exit") menuBar.Append(fileMenu, "&File") self.SetMenuBar(menuBar) self.scrollable = wx.ScrolledCanvas(self, wx.ID_ANY ) self.scrollable.SetSizeHints(200, 200) self.scrollable.ShowScrollbars(wx.SHOW_SB_ALWAYS,wx.SHOW_SB_DEFAULT) self.scrollable.SetScrollbars(20, 20, 15, 10) # Configure the widgets self.text = wx.StaticText(self, wx.ID_ANY, "Some text") self.run_button = wx.Button(self, wx.ID_ANY, "Run") # Bind events to widgets self.Bind(wx.EVT_MENU, self.on_menu) self.run_button.Bind(wx.EVT_BUTTON, self.on_run_button) # Configure sizers for layout main_sizer = wx.BoxSizer(wx.HORIZONTAL) side_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.Add(side_sizer, 1, wx.ALL, 5) self.canvas = MyGLCanvas(self.scrollable, wx.ID_ANY, wx.DefaultPosition, wx.Size(300,200)) self.canvas.SetSizeHints(500, 500) side_sizer.Add(self.text, 1, wx.TOP, 10) side_sizer.Add(self.run_button, 1, wx.ALL, 5) main_sizer.Add(self.scrollable, 1, wx.EXPAND+wx.TOP, 5) self.SetSizeHints(200, 200) self.SetSizer(main_sizer) def on_menu(self, event): """Handle the event when the user selects a menu item.""" Id = event.GetId() if Id == wx.ID_EXIT: self.Close(True) def on_run_button(self, event): """Handle the event when the user clicks the run button.""" text = "Run button pressed." self.canvas.render(text) app = wx.App() gui = Gui("Demo") gui.Show(True) app.MainLoop()
There are various other ways to have scrollbars, and different versions of wxpython offer different options. Consequently the documentation can be confusing – e.g. I’m still unsure whether wx.SHOW_SB_ALWAYS and wx.ALWAYS_SHOW_SB are the same thing. Some windows will automatically deal with scroll-related events. In such situations think SetScrollbar() (which explicitly sets scrollbar features) might struggle unless the default behaviour is turned off.
Sources of wxWidgets information
You’ll need to find some on-line information. Make sure that the wxWidgets release you’re using matches what the documentation describes (on Linux type “wx-config –release” to see what’s installed). Here are some suggestions