Desktop GUI development: wxPython¶
Introduction¶
Desktop GUIs:¶
Traditional Graphical User Interface Applications
Run entirely on local machine – interactive, interface and logic code in one process.
Advantages:
- Easier to write – all in one program
- Faster – data/interface direct communication
- Faster display: direct to screen (or even OpenGL, etc.)
- Runs without network
- Save/Manipulate local files
- Familiar install/start/stop/run, etc.
Python Options¶
Multiple GUI frameworks available:
- PyGTK
- PyQT / PySide
- TkInter
- wxPython
- PyGame
- Native GUIs: Cocoa (PyObjC), PythonWin
- Kivy for touchscreen (mobile) platforms
- Panda3d for 3D or 2D games
- Some more minor ones...
wxPython¶
Why wxPython?
- Python wrapper around C++ toolkit (wxWidget)
- wxWidgets is a wrapper around native toolkit:
- Windows: Win32 (64)
- OS-X: Cocoa
- Linux: GTK
- Native look and feel
- License: (modified) LGPL
Legacy: it was the best option for me when I first needed something...
See http://www.wxpython.org for more information
Installing¶
wxPython is a big complicated build:
- can’t just
pip
orsetup.py install
from source.
This is complicated for py3 – only available in the pre-release “Phoenix” version.
see: Installing wxPython for details.
wxPython on py2:¶
Windows and OS-X:
- use the binaries on http://wxpython.org/download.php
Linux: use your system’s package manager
NOTE: there are some issues with some packages:
- May be old version
- May use standard wx build – more crash prone!
(some run-time checking turned off)
Documentation¶
Docs and Demos: download these!
wxPython Demo: run this!
(Examples of every Widget available)
Primary wx docs:
- Written for C++, with Python notes: http://wxpython.org/onlinedocs.php
- This may help: http://wiki.wxpython.org/C%2B%2BGuideForwxPythoneers
Phoenix docs:
http://wxpython.org/Phoenix/docs/html/main.html
The wxPython wiki: lots of good stuff here:
Some starting points¶
How to learn wxPython
wxPython Style Guide
Notes on the Demo:
wxpython-users: mailing list – great resource (and great community):
My own repository of samples:
Pythonic code:¶
Over the years, wxPython has grown a number of things to make it more “pythonic” – hide some of that C++ legacy
Properties:
The C++ classes are full of getters and setters:
wxTextCtrl::SetValue
wxTextCtrl::GetValue
These methods have been translated into properties for Python
MyTextCtrl.Value = some_string
another_string = wxTextCtrl.Value
(The “Get/Set” versions are still there, but it’s klunkier code)
Other Python options: some specific wx types can be accessed with standard python types:
wxPoint
— (x,y)
( tuple )
wxList
— [1,2,3]
(python list)
wxSize
— (w,h)
(tuple)
...
Using these makes your code cleaner and more pythonic
Basic Structure¶
How is a wxPython app structured?
wx.Window¶
Pretty much everything you see on the screen is a wx.Window
It is the superclass for all the “widgets”, “controls”, or whatever you want to call them
It is essentially a rectangle on the screen that catches events
You generally don’t use it by itself, though you may derive from it to make a new widget
Historical Note: “wxWidgets” was called “wxWindows” – until Microsoft threatened to sue them.
Since everything is a wx.Window
, it’s good to know its methods and signature:
def __init__(parent,
id=wx.ID_ANY,
pos=wx.DefaultPosition,
size=wx.DefaultSize,
style=0,
name=wx.PanelNameStr)
parent (wx.Window)
id (int)
pos (wx.Point)
size (wx.Size)
style (long)
name (string)
Method types:
- Appearance: Colors, Fonts, Labels, Styles
- Geometry: Size, Position, IsShown, Move, etc.
- Layout: Sizers, etc.
- Many others!
Event-Driven programming¶
On app startup, the .MainLoop()
method is called.
The mainloop takes control – monitoring for events, then dispatching them.
Events can come from the system, or user interaction: keyboard, mouse, etc.
All the work of your app is done in response to events
You only need to response to (Bind) the events you care about
Not so different than a web app, except events are finer-grained
(every mouse move, etc.)
wx.App¶
Every wx app has a single wx.App
instance:
app = wx.App(False)
frame = DemoFrame(None, title="Micro App")
frame.Show()
app.MainLoop()
(the False
means: “don’t re-direct stdout to a Window”)
And you almost always start the MainLoop
wx.Frame¶
wx.Frame
is a “top level” Window: One with a title bar, min-max buttons,etc.
Most apps have a single wx.Frame
– central interaction with the app.
This is where menu bars, etc. are placed, and often the core GUI logic of the app.
class TestFrame(wx.Frame):
def __init__(self, *args, **kwargs):
kwargs.setdefault('title', "Simple test App")
wx.Frame.__init__(self, *args, **kwargs)
demo: Examples/wxpython/basic_app_1.py
Controls¶
Or Widgets or .....
Event Handlers¶
Event handlers have a common signature:
def onOpen(self, evt=None):
print "open menu selected"
self.app_logic.file_open()
The second parameter is the wx.Event
object that initiated the call – it holds information about the event that can be useful.
I like to give the event parameter a default None, so the handler can be called from other parts of the code as well.
demo: Examples/wxpython/basic_app_2.py
Common Dialogs¶
wxPython provides a number of common Dialogs. These wrap the native ones where possible for a native look and feel.
wx.MessageDialog
wx.ColourDialog
wx.FileDialog
wx.PageSetupDialog
wx.FontDialog
wx.DirDialog
wx.SingleChoiceDialog
wx.TextEntryDialog
- ...
These do pretty much what you’d expect...
wx.FileDialog¶
Example use of a common dialog: wx.FileDialog
dlg = wx.FileDialog(self,
message="Save file as ...",
defaultDir=os.getcwd(),
defaultFile="",
wildcard=wildcard,
style=wx.SAVE )
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
else:
print "The file dialog was canceled before anything was selected"
dlg.Destroy()
example: Examples\wxpython/basic_app_3.py
Basic Widgets¶
All the basic widgets (controls) you’d expect are there:
- Buttons
- TextCtrl (Text Control)
- Check Boxes
- List Box
- Combo Box
- Slider
- Spin Control
- . . . . .
Way too many to list here!
See the docs and the Demo to find the one you need
Using a Control¶
A Button is about as simple as it gets
__init__(parent, id, label="", pos=wx.DefaultPosition, ...)
Mostly the same as wx.Window, and other controls....
## add just a single button:
self.theButton = wx.Button(self, label="Push Me")
self.theButton.Bind(wx.EVT_BUTTON, self.onButton)
## and give it an event handler
def onButton(self, evt=None):
print "You pushed the button!"
code: Examples/wxpython/basic_app_4.py
wx.Panel¶
A wx.Panel
is a wx.Window
that you can put other controls on
It supplies nifty things like tab traversal, etc.
You can put controls right on a wx.Frame
(we just did it), but a wx.Panel
provided extra features, the “normal” look, and helps you organize and re-use your code
Mostly the same as wx.Window
, and other controls...
class ButtonPanel(wx.Panel):
def __init__(self, *args, **kwargs):
wx.Panel.__init__(self, *args, **kwargs)
self.theButton = wx.Button(self, label="Push Me")
self.theButton.Bind(wx.EVT_BUTTON, self.onButton)
def onButton(self, evt=None):
print("You pushed the button!")
And use it in the Frame:
self.buttonPanel = ButtonPanel(self)
code: Examples/wxpython/basic_app_5.py
Control Layout¶
With more than one control, you need to figure out how to place them and how big to make them.
You may have noticed that wx.Window
takes pos
and size
parameters.
You may have also noticed that I didn’t use them.
Why not?
Absolute Positioning¶
Specifying the size and location of controls with pixel coordinates.
This is a serious pain to do!
Though it can be made a lot easier with GUI-building tools...
So why not?
When you add or remove a control, the layout changes:
- recalculate all positions and sizes
When you change the text on a control the layout changes:
- recalculate all positions and sizes
When you try it on another platform the layout changes:
- recalculate all positions and sizes
When the user changes default font size, the layout changes:
- recalculate all positions and sizes
Sizers:¶
The alternative is “Sizers”
wx.Sizer
is wx’s system for automatically determining the size and location of controls
Instead of thinking in terms of what size and position a given control should be, you think in terms of how they relate to each other:
“I want a column of buttons all the same size along the left edge of the Panel”
Sizers capture that logic and compute the sizes and locations for you.
They will re-size things for you when anything changes: - adding, removing, changing labels - re-sizing the Window, etc...
Sizers take a while to wrap your brain around...
But it’s worth the learning curve.
Nice discussion here:
http://wiki.wxpython.org/UsingSizers
I have the graphic posted on the wall by my desk...
Sizer Example¶
The Basic BoxSizer
:
- Lays out a row or column of controls...
Sizer.Add(window, proportion, flag, border)
## do the layout
S = wx.BoxSizer(wx.VERTICAL)
S.Add(theButton1, 0, wx.GROW | wx.ALL, 4)
S.Add(theButton2, 0, wx.GROW | wx.ALL, 4)
self.SetSizerAndFit(S)
code: Examples/wxpython/basic_app_6.py
Nested Sizers¶
How do I get them centered both ways?
- Nest a vertical sizer inside a horizonal one
- And add stretchable spacers...
buttonSizer = wx.BoxSizer(wx.VERTICAL)
buttonSizer.Add(theButton1, 0, wx.GROW | wx.ALL, 4)
buttonSizer.Add(theButton2, 0, wx.GROW | wx.ALL, 4)
mainSizer = wx.BoxSizer(wx.HORIZONTAL)
mainSizer.Add((1,1), 1) # stretchable space
mainSizer.Add(buttonSizer, 0, wx.ALIGN_CENTER) # the sizer with the buttons in it
mainSizer.Add((1,1), 1) # stretchable space
Widget Inspection Tool¶
How do I keep all this straight?
The Widget Inspection Tool (WIT) is very handy:
app = TestApp(False)
## set up the WIT -- to help debug sizers
import wx.lib.inspection
wx.lib.inspection.InspectionTool().Show()
app.MainLoop()
(you can also bring it up from a menu event, or...)
code: Examples/wxpython/basic_app_7.py
Other Sizers¶
Sizers for laying out stuff in grids...
wx.GridSizer
wx.FlexGridSizer
wx.GridBagSizer
(you can do it all with a GridBagSizer
)
See the docs for info.
Hierarchies...¶
wxPython has multiple independent hierarchies ...
The nested parent-child relationship:
- every
wx.Window
has a parent - every
wx.Window
has zero or more children
The class Hierarchy
- sub classes of
wx.Window
- classes with instances as attributes
The Layout Hierarchy
- Sizers within Sizers...
- Arbitrarily deep.
Each of these takes care of different concerns: confusing but powerful.
Accessing inputs¶
Much of the point of a GUI is to collect data from the user.
So you need to be able to access what s/he has input.
## add a text control:
self.textControl = wx.TextCtrl(self)
def onGetData(self, evt=None):
print("get data button pressed")
contents = self.textControl.Value
print("the contents are:", contents)
Most controls have a .Value
property
Setting Values¶
You also want to display data...
So you need to be able to set the values, too:
# and another text control:
self.outTextControl = wx.TextCtrl(self,
style=wx.TE_READONLY)
def onGetData(self, evt=None):
self.outTextControl.Value = self.inTextControl.Value
You can set the .Value
property too...
example: Examples/wxpython/basic_app8.py
Code-generated GUIs...¶
You shouldn’t write the same repetitive code for a GUI...
You may need to build a GUI to match data at run time.
Lots of ways to do that with wxPython – Sizers help a lot.
Try to do it whenever you find yourself writing repetitive code...
The key is how to do the event Binding:
def OnButton(self, evt):
label = evt.GetEventObject().GetLabel()
do_somethign_with_label(label)
example: Examples/wxpython/CalculatorDemo.py
The “lambda trick”
– a way to pass custom data to an event handler:
for name in ["first", "second", "third"]:
btn = wx.Button(self, label=name)
btn.Bind(wx.EVT_BUTTON,
lambda evt, n=name: self.OnButton(evt, n) )
....
def OnButton(self, Event, name):
print("In OnButton:", name)
http://wiki.wxpython.org/Passing%20Arguments%20to%20Callbacks
Miscellaneous¶
Assorted handy features....
Long Running Tasks¶
The UI is locked up while an event is being handled.
So you want all event handlers to run fast.
But what if there is significant work to do?
Enter: threading and multi-processing
But: wxPython is not thread-safe: almost all wx methods must be called from within the same thread.
Thread-safe operations: Creating and Posting Events
wx.CallAfter
¶
Easiest way to communicate with threads:
wx.CallAfter
Puts an event on the event stack, calls the designated function or method when the stack is cleared:
wx.CallAfter(function_to_call, *args, **kwargs)
# *args, **kwargs are passed on to function_to_call
(see also: wx.CallLater()
)
BILS¶
Browser Interface, Local Server
Web app: Server runs on local machine
Browser is the interface – but all running local
Can wrap the Browser window in a desktop app: Chrome Embedded Framework, wxWebkit, etc.
Good way to get both a web app and desktop app with one codebase
Example: Cameo Chemicals
(PyCon 2009: Browser Interface, Local Server Application)
LAB¶
Make a very simple address book app:
- Really basic data model is in
address_book_data.py
- Finish the form to edit an entry – subclass of a
wx.Panel
(entry_form.py
) - The form goes on a
wx.Frame
(address_book_app.py
) - Add a way to switch between entries (
switcher.py
) - Add a “new record” button
- Add file-save and file-open menus to the frame
- Add some validation, better layout, etc....
Examples/wxpython/address_book/