do not be surprised , if the below python code does not work properly on your machine these days!
You must know some stuff before you start with this crash course:
- Python programming language. Basic concepts, modules, OOP programming.
- Event driven programming.
If 1 is not met you should try the following:
- if you have no programming knowledge:
Instant Hacking http://hetland.org/python/instant-hacking
- if you have some programming knowledge:
If 2 is not met try:
Introduction to Event-Driven Programming http://www.ferg.org/projects/ferg-event_driven_programming.html
Altho not prerequisites there are some things that could help you a lot in your GUI adventures. More at BestPractices. It is probably best that you read that after you finish the course.
- wxpython is a GUI creation library.
- Most GUIs are about presenting the user with some information and then reacting to user input.
- the information can be either static or dynamic
- Most of the user input is represented by keyboard strokes and/or by mouse movement/clicks.
The simplest Application
Hello World App:
1 import wx #import of wx namespace 2 app = wx.App(0) # creation of the wx.App object (initialisation stuff) 3 frame = wx.Frame(None, title="Hello World") # creation of a Frame with "Hello World" as title 4 frame.Show() # frames are invisible by default so we use Show() to make them visible 5 app.MainLoop() # here the app enters a loop waiting for user input
Although there are only 5 lines this app does everything a GUI is supposed to to: presents information (the "Hello World" title) and reacts to user input (you can drag the frame around the screen, you can resize it and you can minimize, maximize or close it).
This is possible because a lot of stuff is taken care off automatically by wxpython. You don't have to worry about drawing the frame or reacting to mouse input on the minimize/maximize/close buttons, wxpython takes care of that.
Custom Widgets, Custom Event Handling
Hello My World App:
1 import wx 2 3 class MyFrame(wx.Frame): 4 def __init__(self): 5 wx.Frame.__init__(self, None, title="Hello My World") 6 self.Bind(wx.EVT_CLOSE, self.OnClose) 7 self.Show() 8 9 def OnClose(self, evt): 10 dlg = wx.MessageDialog(self, 'Are you sure you want to close My World?', 11 'Closing...', wx.YES_NO | wx.ICON_QUESTION) 12 ret = dlg.ShowModal() 13 dlg.Destroy() 14 15 if ret == wx.ID_YES: 16 evt.Skip() 17 18 app = wx.App(0) 19 MyFrame() 20 app.MainLoop()
Here we see a custom made Frame. MyFrame does not take any parameters and it knows that it has no parent (None), it knows that its title is "Hello My World" and it also knows that after creation it should show itself.
The line with self.Bind(...) informs wxpython that we want something special to happen when the user tries to close the frame by binding the EVT_CLOSE event to our custom handler. So... when the user clicks on the close button wxpython will first call our custom handler. In this handler we create a dialog asking the user for confirmation and if the user presses the Yes button we pass the event to the next handler by skipping it (evt.Skip()). The next handler is the default handler, the one installed automatically by the wxpython toolkit and this handler destroys the Frame and exists the Main Loop effectively ending the Application.
If we don't skip the event the default handler will not be called so the frame will not close.
In the event handler we used a wx.MessageDialog... the best way to learn more about it is to open the wx documentation provided by wxpython and look for it, there you will find how to customize it. Maybe you want an Ok/Cancel combo or maybe you want an exclamation mark instead of the question mark... you'll find all about this on the wxMessageDialog page in the wx docs. This documentation is for C++ so a beginner pythoneer might have some problems. WxDocsForPythonProgrammers might provide a couple of clues.
Anatomy of a wxpython application
Most of wxpython apps will go through the following scenario:
- an app object gets created, its creation will take care of the initialization of the toolkit.
- a custom frame/dialog/window gets created and shown.
the app's MainLoop() gets called and this will put the app in a loop where events generated by widgets will be dispatched to their handlers.
In most of the apps, steps 1 and 3 will be just 2 lines: step 1 will be something like app = wx.App(0) and step 3 will be something like app.MainLoop().
Step 2 will involve:
- 2.1 creating some widgets and placing them in the frame 2.2 binding events generated by these widgets to custom handlers.
Step 2.2 is rather straightforward: identify the event you want to react to using the documentation and then use widget.Bind(...) to create a link between the event generated by that widget and your custom handler.
Step 2.1 will be most of the GUI related work you'll do on your app. Most of the problems you'll encounter will be here. It will be either that your widgets don't look/behave as you want them to or maybe they aren't laid out properly, this is why it is a good idea to have the docs and the demo open at all time and to learn at least one way of laying out widgets.
The recommended way of laying out widgets is by using sizers. The sizer is a class that will compute the location and size a widget should have. Of course it requires some hints like the order in which the widgets are laid out, if a widget should enlarge as more space is available or if it should stay the same size. Sizers will automatically handle situations where the layout of the frame/dialog/window needs to change. For example, when you resize the frame and more space becomes available, the sizer will automatically recompute widgets size and location and distribute the available space to the managed widgets according to the hints that were given.
wx.BoxSizer is one of the most used sizers. It is very easy to use and with several BoxSizers you can create quite complex layouts. It simply lays out the widgets one after the other from top to bottom if the orientation is wx.VERTICAL or from left to right if the orientation is wx.HORIZONTAL. Adding widgets to the sizer means that the sizer will manage their size and position. When adding the widgets you can also give the sizer certain hints about how the widget should react to changes in layout. The first hint is the proportion, it could be 0 or 1 and above and it tells the sizer if it should expand the widget in the direction of the layout as more space is available. So if you use a BoxSizer with wx.VERTICAL as orientation a proportion of 1 will cause the widget to use all the available vertical space. If more than one widget has 1 as proportion they will share (divide equally) the available space. Using different numbers for proportions will cause the space to be divided proportionally between the widgets. The next attribute is the flag. Here you could tell the sizer if the widget should expand in the other direction, if the widgets should be aligned in the available space or if there should be a border around the widget. More about it in the wxDocs, look for wxSizer, the parent of BoxSizer. Also look at the sizer examples from the Demo.
Hello Sizers App:
1 import wx 2 import random 3 4 def GetRandColor(): 5 red = random.randint(0,255) 6 green = random.randint(0,255) 7 blue = random.randint(0,255) 8 return (red, green, blue) 9 10 class ColorPanel(wx.Panel): 11 def __init__(self, parent, color='rand'): 12 wx.Panel.__init__(self, parent) 13 if color == 'rand': 14 color = GetRandColor() 15 self.Bind(wx.EVT_MOTION, self.OnMouseMove) 16 self.SetBackgroundColour(color) 17 18 def OnMouseMove(self, evt): 19 self.SetBackgroundColour(GetRandColor()) 20 self.Refresh() 21 evt.Skip() 22 23 class MyFrame(wx.Frame): 24 def __init__(self): 25 wx.Frame.__init__(self, None, title="Hello Sizers") 26 vSizer =wx.BoxSizer(wx.VERTICAL) 27 hSizer = wx.BoxSizer(wx.HORIZONTAL) 28 red = ColorPanel(self, 'red') 29 blue = ColorPanel(self, 'blue') 30 green = ColorPanel(self, 'green') 31 rand = ColorPanel(self) 32 33 red.SetSize((100, 100)) 34 rand.SetSize((100, 100)) 35 green.SetSize((-1, 100)) 36 blue.SetSize((-1, 100)) 37 38 vSizer.Add(rand, 0, wx.EXPAND) 39 vSizer.Add(green, 1, wx.EXPAND) 40 vSizer.Add(blue, 2, wx.EXPAND) 41 42 hSizer.Add(red, 0, wx.EXPAND) 43 hSizer.Add(vSizer, 1, wx.EXPAND) 44 45 self.SetSizer(hSizer) 46 self.Fit() 47 48 self.Show() 49 self.Bind(wx.EVT_CLOSE, self.OnClose) 50 51 def OnClose(self, evt): 52 dlg = wx.MessageDialog(self, 'Are you sure you want to close My World?', 53 'Closing...', wx.YES_NO | wx.ICON_QUESTION) 54 ret = dlg.ShowModal() 55 dlg.Destroy() 56 57 if ret == wx.ID_YES: 58 evt.Skip() 59 60 app = wx.App(0) 61 MyFrame() 62 app.MainLoop()
Now, for some clarifications on the above app:
ColorPanel is a custom wx.Panel that can receive a color at initialization, color that will become its background color. If it doesn't receive a color it defaults to 'rand' and beside setting the background color to some random color it also installs a handler for mouse movement so that when the mouse moves OVER the panel the panel changes its background color. As you can see SetBackgroundColour is not fussy, it accepts a named color, a wx.Color object, a predefined wx.Color object like wx.RED or a tuple of 3 integers representing the intensity of red, green and blue.
The layout is taken care of in the Frame constructor. There we create 4 ColorPanels and with the help of 2 BoxSizers we place them in a configuration with the red panel on the left, expanding as the frame grows vertically. The next 3 panels are placed on the right one on top of the other with green and blue expanding vertically in a proportion of 1:2.
The sizer talks to the children (the widgets it manages) and to the parent. From the parent it gets information about available space and from the children it gets information about what sizes they prefer. When you have problems with the layout the first step in debugging is to look for what information the sizer receives. The information about the size is ALWAYS overwritten by the hints you give to the sizer. In the above example we set the size of the red panel to 100x100 pixels but when we add it to the sizer we say that proportion should be 0 and that it should EXPAND. So the sizer (being a HORIZONTAL BoxSizer) honors the information about the width that it receives from the panel due to proportion 0 (as the widget wants) BUT it overrides the information about height due to wx.EXPAND hint.
The green panel has both the proportion set to 1 and the wx.EXPAND flag so it looks like none of its size hints are gonna be honored by the sizer... well not true, having proportion of 1 makes it a reference so when we later ask the frame to Fit the following happens:
- the frame looks for its sizer, we set it to be the hSizer so it finds it.
- the frame asks the sizers for a "best" size
- the hSizer asks its children about size
- red says (100, 100) ; we have for now (100x100)
- next child is vSizer so hSizer asks it about what size it would like to have
- vSizer asks its children about size
- rand says (100, 100) so we have so far (200x100) (same height as red)
- green says it doesn't care about its width ( we used -1 to communicate that) but it would like to be 100 pixel height; it gets 100 pixels for height and 100 for width because it wanted to wx.EXPAND and the current available width was already set by rand so we have (200x200); red gets 100 more pixels in height because it too has wx.EXPAND
- blue says it doesn't care about width (gets 100 just like green) but it would like to have a height of 100... well tough luck! the proportion of 2 dictates that it should be twice the height of green so it gets 200 pixels as height; red of course complies again with the new increase in height and becomes (100x200) so... the final size is (200x400); this information gets back to the frame and the frame shows itself on the screen with that size.
Let's see now what happens when we resize the frame:
- hSizer gets the size of the frame and then starts asking its children about what they need
- red says it wants 100x100, red gets 100 pixels from width and all the available height (wx.EXPAND)
- vSizer receives what width remains and all the height
- rand wants 100 height and it gets it (if available) along with all the width vSizer received
- green and blue both get ignored about their needs and get all the available width with the height divided proportionally according to the proportion arguments (blue gets twice the height green gets)
Well that's about it for this Crash Course. You should be able to create a frame and place widgets inside it with BoxSizers, also you should be able to bind the events so that your app could react to user input.
Next steps are the Demo and the wxDocs. I recommend reading for starters the documentation of wxWindow (parent to all widgets) and the "Event handling overview" page from the wxDocs. For example I found out:
about EVT_MOTION via wxDocs -> "Event handling overview" -> wxMouseEvent.
about the parameters of sizer.Add(...) via wxDocs -> wxBoxSizer -> wxSizer -> Add
The Demo has a very nice feature called "Demo Code". On that notebook page you can safely modify the code and try out stuff. You can switch between your modified version and the original and try them on "Demo" page.
Here, do the following:
- open the Demo
go to "Window Layout" -> "Sizers" on Demo page and try "Simple horizontal boxes"
- switch to "Demo Code" page, scroll down to the definition of "makeSimpleBox1" (you can search for it with Ctrl+F)
modify the proportion of SampleWindow "four" from 0 to 1
- hit the "Save Changes" button and switch to Demo page
- try "Simple horizontal boxes" again, see the difference? :o)
- now you can get back to "Demo Code" and switch to "Original", see how simple is to compare them
Good Luck! and remember... Have Fun! You'll learn at least twice as fast if you're trying to have fun than if you're trying to rush and learn everything.