.. include:: headings.txt ==================================== Hands-on wxPython ==================================== .. raw:: html

Andrea Gavana - PythonBrasil8

.. contents:: :local: :depth: 3 Who Am I ======== .. figure:: figures/andrea_gavana.png :align: right :target: http://infinity77.net I am a Senior Reservoir Engineer at `Maersk Oil `_ in Copenhagen, Denmark, where I mix a 10-years reservoir engineering knowledge with the power of Python and its libraries to solve many everyday problems arising during the numerical studies of oil and gas fields. I am the author and the maintainer of the `AGW `_ (**Advanced Generic Widgets**) library for wxPython, a large collection of owner-drawn and custom widgets shipped officially with every new release of wxPython. Contacts: * E-mail: `andrea.gavana@gmail.com `_ * Twitter: `@AndreaGavana` Tutorial Description ==================== This tutorial is a quick introduction to wxPython, divided in two parts: * Installation and configuration * Build a skeleton of a wxPython application * Window layout management with Sizers * Learn how to use the most common core widgets * Basics of creating custom controls with wxPython * "Free style" coding - depending on time Sources are available `here <_sources/wxpython.txt>`_. Figures are in the `figures `_ directory and all scripts are located in the `scripts `_ directory. The tutorial is also online at `my web page `_. .. topic:: |exercise| Tutorial location :class: green * Full tutorial (zipped - **RECOMMENDED**): http://bit.ly/TgyfRN * Online version: http://bit.ly/TdeKq0 * Zipped archive of scripts: http://bit.ly/T5YzdL | All code and material is licensed under a Creative Commons Attribution 3.0 United States License (CC-by) http://creativecommons.org/licenses/by/3.0/us Introduction ============ wxPython is one of the most famous frameworks used to build graphical user interfaces (GUIs) in Python. It provides native look and feel widgets on all supported platforms (Windows, Linux/Unix, Mac) and it has a vast repository of owner-drawn controls. In addition, the wxPython demo is **the** place where to start looking for examples and source code snippets. Code editors ------------ If you plan to run the various scripts available in this tutorial directly from your preferred editor, you should check that it does not interfere with the wxPython `event loop`. Eclipse, Wingware IDE, Editra, Ulipad, Dr. Python and newest versions of IDLE (and many other editors) support this functionality. If your preferred editor does not - you can easily find out by running the `Hello World `_ sample and see if it hangs - you can still run the samples via the command line:: $ python hello_world.py Documentation ------------- This tutorial contains many links to the documentation referring to the next generation of wxPython - codenamed **Phoenix**. The reasons behind this choice are: * The quality of the **Phoenix** documentation is much higher * Backward-incompatibilities between **Phoenix** and the previous versions of wxPython are relatively few * **Phoenix** is going to take over the world in a few months :-) Installation and Configuration ============================== Windows Binaries ---------------- Choose an installer that matches the version of Python you will be using. If you are using a 64-bit version of Python then make sure you also get a 64-bit wxPython, otherwise choose a 32-bit installer even if you are on a 64-bit version of Windows. There is no longer a separate ansi and Unicode build, **it's all Unicode now**. .. csv-table:: :header: "Installer", "Architecture" :widths: 30, 10 "`wxPython2.9-win32-py26 `_", "32-bit Python 2.6" "`wxPython2.9-win64-py26 `_", "64-bit Python 2.6" "`wxPython2.9-win32-py27 `_", "32-bit Python 2.7" "`wxPython2.9-win64-py27 `_", "64-bit Python 2.7" wxPython Demo for Windows ------------------------- This `installer `_ contains the infamous wxPython demo, other samples, and wxWidgets documentation. Mac OSX Binaries ---------------- The wxPython binaries for OSX are mountable disk images. Simply double click to mount the image and then run the installer application in the image. Be sure to download the image that matches the version of Python that you want to use it with. The files with "carbon" in the name use the Carbon API for implementing the GUI, are compatible with PPC and i386 machines are will work on OSX 10.4 and onwards. The file with "cocoa" in the name use the Cocoa API for implementing the GUI, requires at least OSX 10.5, and supports either 32-bit or 64-bit architectures. * `wxPython2.9-osx-carbon-py2.6 `_ * `wxPython2.9-osx-carbon-py2.7 `_ * `wxPython2.9-osx-cocoa-py2.7 `_ wxPython Demo for Mac OSX ------------------------- These disk images contain the wxPython demo, the documentation, some other samples and some tools: * `wxPython2.9-osx-docs-demos-carbon-py2.6 `_ * `wxPython2.9-osx-docs-demos-carbon-py2.7 `_ * `wxPython2.9-osx-docs-demos-cocoa-py2.7 `_ Linux Binaries -------------- To get prebuilt binaries for Linux or other platforms, please search in your distro's package repository, or any 3rd party repositories that may be available to you. Ubuntu users can get information about the the wx APT repository here. If all else fails you can build wxPython yourself from the source code, see the `build instructions `_. A First Application =================== In this section, we are going to build step by step a skeleton of a wxPython application, enriching it incrementally. Every sub-section contains one or more exercises for you to familiarize yourself with the wxPython framework. Hello World ----------- .. admonition:: |documentation| Documentation * `wx.App instantiation `_ * `wx.Frame initialization `_ .. figure:: figures/hello_world.png :align: right :target: scripts/hello_world.py As in (almost) all every other language and library, this is the simplest "Hello World" application you can write in wxPython: :: # In every wxPython application, we must import the wx library import wx # Create an application class instance app = wx.App() # Create a frame (i.e., a floating top-level window) frame = wx.Frame(None, -1, 'Hello world') # Show the frame on screen frame.Show() # Enter the application main loop app.MainLoop() The last line enters what wxPython defines as "MainLoop". A "MainLoop" is an endless cycle that catches up all events coming up to your application. It is an integral part of any windows GUI application. Although the code is very simple, you can do a lot of things with your window. You can maximize it, minimize it, move it, resize it. All these things have been already done for you by the framework. .. figure:: figures/hello_world1.png :align: right :target: scripts/hello_world1.py .. topic:: |exercise| Exercises :class: green Using the `Hello World `_ sample: 1. Modify it to create two frames instead of one, setting their title as "Hello 1" and "Hello 2". 2. Using the modified script in (1), make the second frame a child of the first. Observe what happens in (1) and (2) when you close the first frame. Does the application terminate? Click on the figure for the solution. Menubar and statusbar --------------------- .. admonition:: |documentation| Documentation * `wx.MenuBar `_ * `wx.StatusBar `_ * `CreateStatusBar `_ Almost all the applications sport a menu bar and a status bar in their main window. A menu bar is a very powerful tool to let the user interact with your GUI as it displays (various levels of) cascading menus with multiple options. A status bar is a narrow window that can be placed along the bottom of a frame and it is mostly used to give small amounts of status information. .. figure:: figures/menubar_statusbar.png :align: right :target: scripts/menubar_statusbar.py :: import wx class MainWindow(wx.Frame): def __init__(self, parent, title): wx.Frame.__init__(self, parent, title=title) # A Statusbar in the bottom of the window self.CreateStatusBar() # Setting up the menu file_menu = wx.Menu() # wx.ID_ABOUT and wx.ID_EXIT are standard IDs provided # by wxWidgets. file_menu.Append(wx.ID_ABOUT, '&About', 'Information about this application') file_menu.AppendSeparator() file_menu.Append(wx.ID_EXIT, 'E&xit', 'Exit the application') # Creating the menubar menu_bar = wx.MenuBar() # Adding the 'file_menu' to the menu bar menu_bar.Append(file_menu, '&File') # Adding the menu bar to the frame content self.SetMenuBar(menu_bar) self.Show() app = wx.App(False) frame = MainWindow(None, 'Sample application') app.MainLoop() Notice the ``wx.ID_ABOUT`` and ``wx.ID_EXIT`` ids. These are standard ids provided by wxWidgets (see a `full list here `_). It is a good habit to use the standard ID if there is one available. This helps wxPython know how to display the widget in each platform to make it look more native. .. admonition:: |hint| Hints * For part (2) you will need to use the `SetStatusWidths `_ and `SetStatusText `_ methods. .. figure:: figures/menubar_statusbar1.png :align: right :target: scripts/menubar_statusbar1.py .. topic:: |exercise| Exercises :class: green Using the `MenuBar and StatusBar `_ sample: 1. Modify it to add an "Edit" top menu with "Cut", "Copy" and "Paste" sub-menus. 2. Create a status bar with 2 fields, set the second field to have double width with respect to the first and and display today's date in the second field. Click on the figure for the solution. | Event handling -------------- .. admonition:: |documentation| Documentation * `Events and event handling `_ * `wx.Event `_ * `wx.EvtHandler `_ Reacting to events in wxPython is called `event handling`. An event is when "something" happens on your application (a button click, text input, mouse movement, a timer expires, etc...). Much of GUI programming consists of responding to events. You link a wxPython object to an event using the `Bind()` method: :: class MainWindow(wx.Frame): def __init__(self, parent, title): wx.Frame.__init__(self, parent, title=title) # Other stuff... menu_item = file_menu.Append(wx.ID_EXIT, 'E&xit', 'Exit the application') self.Bind(wx.EVT_MENU, self.OnExit, menu_item) This means that, from now on, when the user selects the "Exit" menu item, the method `OnExit` will be executed. ``wx.EVT_MENU`` is the "select menu item" event. wxPython understands many other events (everything that starts with ``EVT_`` in the `wx` namespace). The `OnExit` method has the general declaration: :: def OnExit(self, event): # Close the frame, cannot be vetoed if force=True self.Close(force=True) Here `event` is an instance of a subclass of `wx.Event`. For example, a button-click event - ``wx.EVT_BUTTON`` - is a subclass of `wx.Event`. Working with events is straightforward in wxPython. There are three steps: 1. Identify the event binder name: ``wx.EVT_BUTTON``, ``wx.EVT_CLOSE``, etc... 2. Create an event handler. It is a method called when an event is generated 3. Bind an event to an event handler Sometimes we need to stop processing an event: for example, think about a user closing your main application window while the GUI still contains unsaved data. To do this, we call the method `Veto()` on an event, inside an event handler: :: class MainWindow(wx.Frame): def __init__(self, parent, title): wx.Frame.__init__(self, parent, title=title) # Other stuff... self.Bind(wx.EVT_CLOSE, self.OnClose) def OnClose(self, event): # This displays a message box asking the user to confirm # she wants to quit the application dlg = wx.MessageDialog(self, 'Are you sure you want to quit?', 'Question', wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION) if dlg.ShowModal() == wx.ID_YES: self.Destroy() else: event.Veto() .. admonition:: |hint| Hints * See the `Close `_ and the `Veto `_ methods. .. topic:: |exercise| Exercises :class: green Using the `Events `_ sample: 1. Modify it make the main frame "immortal", i.e., non-closable by the user. Can you close the main frame by pressing ``Alt`` + ``F4`` ? Or clicking on the "X" button in the titlebar? Click `here `_ for the solution. Adding widgets -------------- .. admonition:: |documentation| Documentation * `wx.TextCtrl `_ * `wx.TextCtrl styles `_ We are going to add an editable text box inside our frame (a `wx.TextCtrl`). By default, a text box is a single-line field, but the ``wx.TE_MULTILINE`` parameter allows you to enter multiple lines of text. :: class MainWindow(wx.Frame): def __init__(self, parent, title): wx.Frame.__init__(self, parent, title=title) self.control = wx.TextCtrl(self, style=wx.TE_MULTILINE) A couple of things to notice: * The text box is a **child** of the frame. * When a frame frame has exactly one child window, not counting the status and toolbar, this child is resized to take the entire frame client area. We can create a link between the `wx.TextCtrl` behaviour and the menu selected by the user by binding menu events (like "Cut", "Copy" and "Paste" menu selections) to the main frame and process the results in an event handler. For example: :: class MainWindow(wx.Frame): def __init__(self, parent, title): wx.Frame.__init__(self, parent, title=title) self.control = wx.TextCtrl(self, style=wx.TE_MULTILINE) # Other stuff, menubar creation, etc... # Bind the "copy menu event" to the OnCopy method self.Bind(wx.EVT_MENU, self.OnCopy, id=wx.ID_COPY) def OnCopy(self, event): # See if we can copy from the text box... if self.control.CanCopy(): # Actually copy the wx.TextCtrl content # into the clipboard self.control.Copy() In the `OnCopy` event handler we simply check if we can copy text from the text box (i.e., if something is selected and can be copied to the clipboard) and we actually copy what is selected into the clipboard. .. admonition:: |hint| Hints * See the `Copy `_, `Cut `_ and `Paste `_ methods for `wx.TextCtrl`. * If you wish to be fancy, in your exercise you can also check the values for `CanCopy `_, `CanCut `_ and `CanPaste `_. .. figure:: figures/widgets1.png :align: right :target: scripts/widgets1.py .. topic:: |exercise| Exercises :class: green Using the `Widgets `_ sample: 1. The "Edit" top menu has the "Cut", "Copy" and "Paste" sub-menus: bind the correct events to the main frame and add event handlers for these menus. 2. Check that you can copy/paste **text** to/from another application (i.e., a word processor or an IDE). 3. Look at another implementation of the `Widgets `_ sample: what did I do differently? Click on the figure for the solution. Window Layout Management ======================== **Layout management**: describes how widgets are laid out in an application's user interface. wxPython provides a few alternatives: * Absolute positioning (brute force): don't. No, really, **don't**; * Sizers: very powerful, not so simple at the beginning, but worth the effort; * SizedControls: add-on library to help simplify the creation of sizer-based layouts; * AUI (Advanced User Interface): docking windows and automatic layout management. Why sizers? ----------- Sizers, as represented by the `wx.Sizer `_ class and its descendants in the wxPython class hierarchy, have become the method of choice to define the layout of controls in parent windows because of their ability to create visually appealing frames independent of the platform, taking into account the differences in size and style of the individual controls. The idea behind sizers ---------------------- The layout algorithm used by sizers in wxPython is closely related to layout systems in other GUI toolkits, such as Java's AWT, the GTK toolkit or the Qt toolkit. It is based upon the idea of individual subwindows reporting their minimal required size and their ability to get stretched if the size of the parent window has changed. This will most often mean that the programmer does not set the start-up size of a window, the window will rather be assigned a sizer and this sizer will be queried about the recommended size. This sizer in turn will query its children (which can be normal windows, empty space or other sizers) so that a hierarchy of sizers can be constructed. .. admonition:: |note| Note `wx.Sizer `_ does not derive from `wx.Window `_ and thus does not interfere with tab ordering and requires very few resources compared to a real window on screen. What makes sizers so well fitted for use in wxPython is the fact that every control reports its own minimal size and the algorithm can handle differences in font sizes or different window sizes on different platforms without problems. For example, if the standard font as well as the overall design of Linux/GTK widgets requires more space than on Windows, the initial window size will automatically be bigger on Linux/GTK than on Windows. There are currently 6 different kinds of sizers available in wxPython: * `wx.BoxSizer `_ * `wx.StaticBoxSizer `_ * `wx.GridSizer `_ * `wx.FlexGridSizer `_ * `wx.GridBagSizer `_ * `wx.WrapSizer `_ Each represents a certain way to lay out window items or it fulfils a special task such as wrapping a static box around a window item (or another sizer). Common features --------------- .. admonition:: |note| Note Note that only some controls can calculate their size (such as a `wx.CheckBox `_) whereas others (such as a `wx.ListBox `_) don't have any natural width or height and thus require an explicit size. Some controls can calculate their height, but not their width (e.g. a single line text control). All sizers are containers, i.e. they are used to lay out one window item (or several window items), which they contain. Such items are sometimes referred to as the children of the sizer. Independent of how the individual sizers lay out their children, all children have certain features in common: - **A minimal size**: This minimal size is usually identical to the initial size of the controls and may either be set explicitly in the `wx.Size` field of the control constructor or may be calculated by wxPython, typically by setting the height and/or the width of the item to -1. .. figure:: figures/common1.png :align: center :target: scripts/common1.py - **A border**: The border is just empty space and is used to separate widgets in a parent window. This border can either be all around, or at any combination of sides such as only above and below the control. The thickness of this border must be set explicitly, typically 5 points. The following samples show frames with only one item (a button) and a border of 0, 5, and 10 pixels around the button: .. figure:: figures/common2.png :align: center :target: scripts/common2.py - **An alignment**: Often, a widget is given more space than its minimal size plus its border. Depending on what flags are used for the respective widget, this widget can be made to fill out the available space entirely, i.e. it will grow to a size larger than the minimal size, or it will be moved to either the centre of the available space or to either side of the space. The following sample shows a listbox and three buttons in a horizontal box sizer; one button is centred, one is aligned at the top, one is aligned at the bottom: .. figure:: figures/common3.png :align: center :target: scripts/common3.py - **A stretch factor**: If a sizer contains more than one child and it is offered more space than its children and their borders need, the question arises how to distribute the surplus space among the children. For this purpose, a stretch factor (`proportion`) may be assigned to each child, where the default value of 0 indicates that the child will not get more space than its requested minimum size. A value of more than zero is interpreted in relation to the sum of all stretch factors in the children of the respective sizer, i.e. if two children get a stretch factor of 1, they will get half the extra space each *independent of whether one control has a minimal sizer inferior to the other or not*. The following sample shows a frame with three buttons, the first one has a stretch factor of 1 and thus gets stretched, whereas the other two buttons have a stretch factor of zero and keep their initial width: .. figure:: figures/common4.png :align: center :target: scripts/common4.py `wx.Sizers` - the visual approach --------------------------------- ---------------------------- Basic way of adding a window ---------------------------- Let's take a look at `wx.BoxSizer `_. This is the most simple type of box sizer, and the way we add widgets to it is explained by looking at the `wx.BoxSizer.Add `_ signature: .. raw:: html
.. method:: Add(window, proportion=0, flag=0, border=0, userData=None) Appends a child to the sizer. :param `window`: a window, a spacer or another sizer to be added to the sizer. Its initial size (either set explicitly by the user or calculated internally) is interpreted as the minimal and in many cases also the initial size. :param int proportion: this parameter is used in `wx.BoxSizer` to indicate if a child of a sizer can change its size in the main orientation of the `wx.BoxSizer` - where 0 stands for not changeable and a value of more than zero is interpreted relative to the value of other children of the same `wx.BoxSizer`. For example, you might have a horizontal `wx.BoxSizer` with three children, two of which are supposed to change their size with the sizer. Then the two stretchable windows would get a value of 1 each to make them grow and shrink equally with the sizer's horizontal dimension. :param int flag: OR-combination of flags affecting sizer's behaviour. :param int border: determines the border width, if the flag parameter is set to include any border flag. :param object userData: allows an extra object to be attached to the sizer item, for use in derived classes when sizing information is more complex than the proportion and flag will allow for. :rtype: `SizerItem `_ .. raw:: html
Let's create a vertical sizer (children will be placed on top of each other) and place two buttons in it. All the "extra" parameters are set to 0; we'll worry about them later. .. figure:: figures/boxsizer1.png :align: right :target: scripts/boxsizer1.py :: sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(wx.Button(self, -1, 'An extremely long button text'), 0, 0, 0) sizer.Add(wx.Button(self, -1, 'Small button'), 0, 0, 0) self.SetSizer(sizer) You'll notice a couple of things about this: * The buttons are just big enough to accommodate the text in them. In fact, any control placed into a sizer this way will appear at its minimum size unless we change the parameters. * The window size is not changed to fit the sizer. This results in a lot of ugly empty space. Let's worry about the second issue first. To make the window size more appropriate, we can set the size hints to tell the enclosing window to adjust to the size of the sizer: .. figure:: figures/boxsizer2.png :align: right :target: scripts/boxsizer2.py :: sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(wx.Button(self, -1, 'An extremely long button text'), 0, 0, 0) sizer.Add(wx.Button(self, -1, 'Small button'), 0, 0, 0) sizer.SetSizeHints(self) self.SetSizer(sizer) This is particularly useful in circumstances like this one, in which the `wx.Frame`'s default size would otherwise be much too big or small to show most layouts in an aesthetically pleasing manner. -------------------------- The `proportion` parameter -------------------------- The first parameter to `wx.BoxSizer.Add `_ is obviously the `wx.Window` or `wx.Sizer` that you are adding. The second one (the `proportion`) defines how large the sizer's children are in relation to each other. In a vertical sizer, this changes the height; in a horizontal sizer, this changes the width. Here are some examples: .. list-table:: :header-rows: 1 :widths: 40 10 * - Code - Resulting Image * - :: sizer = wx.BoxSizer(wx.VERTICAL) # Second button is three times as tall as first button sizer.Add(wx.Button(self, -1, 'An extremely long button text'), 1, 0, 0) sizer.Add(wx.Button(self, -1, 'Small button'), 3, 0, 0) sizer.SetSizeHints(self) self.SetSizer(sizer) - .. figure:: figures/boxsizer3.png :align: left :target: scripts/boxsizer3.py * - Same code as above, with window resized. Notice that the bottom button is still three times as tall as the top button. - .. figure:: figures/boxsizer31.png :align: left :target: scripts/boxsizer3.py * - :: sizer = wx.BoxSizer(wx.VERTICAL) # First button is 3/2 the height of the second button sizer.Add(wx.Button(self, -1, 'An extremely long button text'), 3, 0, 0) sizer.Add(wx.Button(self, -1, 'Small button'), 2, 0, 0) sizer.SetSizeHints(self) self.SetSizer(sizer) - .. figure:: figures/boxsizer32.png :align: left :target: scripts/boxsizer32.py | If one of the `proportion` parameters is 0, that `wx.Window` will be the minimum size, and the others will resize proportionally: .. list-table:: :header-rows: 1 :widths: 40 10 * - Code - Resulting Image * - :: sizer = wx.BoxSizer(wx.VERTICAL) # Third button is twice the size of the second button sizer.Add(wx.Button(self, -1, 'An extremely long button text'), 0, 0, 0) sizer.Add(wx.Button(self, -1, 'Small button'), 1, 0, 0) sizer.Add(wx.Button(self, -1, 'Another button'), 2, 0, 0) sizer.SetSizeHints(self) self.SetSizer(sizer) - .. figure:: figures/boxsizer33.png :align: left :target: scripts/boxsizer33.py * - Same code as above, with window resized. The top button (proportion 0) is still the minimum height, and the third button is still twice the height of the second. - .. figure:: figures/boxsizer34.png :align: left :target: scripts/boxsizer33.py This is especially useful when you want, for example, a button at the bottom which is only as big as necessary, and some other control that occupies the rest of the frame. To do so, give the button proportion 0 and the other control a number greater than 0. Mac users in particular will appreciate you for not creating huge aqua-styled buttons. ----------------------------------- The `flags` and `border` parameters ----------------------------------- The `flag` argument accepted by `wx.Sizer.Add `_ is a ``OR``-combination of the following flags. Two main behaviours are defined using these flags. One is the border around a window: the `border` parameter determines the border width whereas the flags given here determine which side(s) of the item that the border will be added. The other flags determine how the sizer item behaves when the space allotted to the sizer changes, and is somewhat dependent on the specific kind of sizer used. +---------------------------------------------------------------------+-----------------------------------------------------------------------------+ | Sizer Flag | Description | +=====================================================================+=============================================================================+ | ``wx.TOP`` | These flags are used to specify which side(s) of the sizer | +---------------------------------------------------------------------+ item the border width will apply to. | | ``wx.BOTTOM`` | | +---------------------------------------------------------------------+ | | ``wx.LEFT`` | | +---------------------------------------------------------------------+ | | ``wx.RIGHT`` | | +---------------------------------------------------------------------+ | | ``wx.ALL`` | | +---------------------------------------------------------------------+-----------------------------------------------------------------------------+ | ``wx.EXPAND`` | The item will be expanded to fill the space assigned to | | | the item. | +---------------------------------------------------------------------+-----------------------------------------------------------------------------+ | ``wx.SHAPED`` | The item will be expanded as much as possible while also | | | maintaining its aspect ratio | +---------------------------------------------------------------------+-----------------------------------------------------------------------------+ | ``wx.FIXED_MINSIZE`` | If you would rather have a window item stay the size it started with then | | | use ``FIXED_MINSIZE`` | +---------------------------------------------------------------------+-----------------------------------------------------------------------------+ | ``wx.RESERVE_SPACE_EVEN_IF_HIDDEN`` | Normally `Sizers` don't allocate space for hidden windows or other items. | | | This flag overrides this behavior so that sufficient space is allocated for | | | the window even if it isn't visible. This makes it possible to dynamically | | | show and hide controls without resizing parent dialog, for example. | +---------------------------------------------------------------------+-----------------------------------------------------------------------------+ | ``wx.ALIGN_CENTER`` **or** ``wx.ALIGN_CENTRE`` | The ``ALIGN*`` flags allow you to specify the alignment of the item | +---------------------------------------------------------------------+ within the space allotted to it by the sizer, adjusted for the border if | | ``wx.ALIGN_LEFT`` | any. | +---------------------------------------------------------------------+ | | ``wx.ALIGN_RIGHT`` | | +---------------------------------------------------------------------+ | | ``wx.ALIGN_TOP`` | | +---------------------------------------------------------------------+ | | ``wx.ALIGN_BOTTOM`` | | +---------------------------------------------------------------------+ | | ``wx.ALIGN_CENTER_VERTICAL`` **or** ``wx.ALIGN_CENTRE_VERTICAL`` | | +---------------------------------------------------------------------+ | | ``wx.ALIGN_CENTER_HORIZONTAL`` **or** ``wx.ALIGN_CENTRE_HORIZONTAL``| | +---------------------------------------------------------------------+-----------------------------------------------------------------------------+ | Let's start with the simplest case: the alignment flags. These are pretty self-explanatory. .. list-table:: :header-rows: 1 :widths: 40 10 * - Code - Resulting Image * - :: sizer = wx.BoxSizer(wx.VERTICAL) # Second button is right aligned sizer.Add(wx.Button(self, -1, "An extremely long button text"), 0, 0, 0) sizer.Add(wx.Button(self, -1, "Small Button"), 0, wx.ALIGN_RIGHT, 0) sizer.SetSizeHints(self) self.SetSizer(sizer) - .. figure:: figures/boxsizer4.png :align: left :target: scripts/boxsizer4.py * - :: sizer = wx.BoxSizer(wx.VERTICAL) # Second button is center-aligned sizer.Add(wx.Button(self, -1, "An extremely long button text"), 0, 0, 0) sizer.Add(wx.Button(self, -1, "Small Button"), 0, wx.ALIGN_CENTER, 0) sizer.SetSizeHints(self) self.SetSizer(sizer) - .. figure:: figures/boxsizer41.png :align: left :target: scripts/boxsizer41.py Next is the ``wx.EXPAND`` flag. This is synonymous with ``wx.GROW``. .. list-table:: :header-rows: 1 :widths: 40 10 * - Code - Resulting Image * - :: sizer = wx.BoxSizer(wx.VERTICAL) # Second button expands to the whole parent's width sizer.Add(wx.Button(self, -1, "An extremely long button text"), 0, 0, 0) sizer.Add(wx.Button(self, -1, "Small Button"), 0, wx.EXPAND, 0) sizer.SetSizeHints(self) self.SetSizer(sizer) - .. figure:: figures/boxsizer5.png :align: left :target: scripts/boxsizer5.py You can see that the first button takes its minimum size, and the second one grows to match it. This affects controls in the opposite manner of the second parameter; ``wx.EXPAND`` in a vertical sizer causes horizontal expansion, and in a horizontal sizer it causes vertical expansion. Next is ``wx.SHAPED``. This flag ensures that the width and height of the object stay proportional to each other. It doesn't make much sense for buttons, but can be excellent for bitmaps, which would be contorted or clipped if not scaled proportionally. .. list-table:: :header-rows: 1 :widths: 30 10 * - Code - Resulting Image * - :: sizer = wx.BoxSizer(wx.VERTICAL) # Second button will scale proportionally sizer.Add(wx.Button(self, -1, "An extremely long button text"), 0, 0, 0) sizer.Add(wx.Button(self, -1, "Small Button"), 1, wx.SHAPED, 0) sizer.SetSizeHints(self) self.SetSizer(sizer) - .. figure:: figures/boxsizer51.png :align: left :target: scripts/boxsizer51.py * - Same code as above, with window resized. The width grew dramatically with the height. In fact, it didn't quite grow vertically the whole way because it couldn't maintain the correct ratio while doing so. - .. figure:: figures/boxsizer52.png :align: left :target: scripts/boxsizer51.py Finally, we have the border flags. These only make sense when the `border` parameter is greater than 0, and describe the sides of the control on which the border should appear. In order to demonstrate this most clearly, we'll keep the ``wx.EXPAND`` flag. .. list-table:: :header-rows: 1 :widths: 40 10 * - Code - Resulting Image * - :: sizer = wx.BoxSizer(wx.VERTICAL) # Border size effects sizer.Add(wx.Button(self, -1, "An extremely long button text"), 0, 0, 0) sizer.Add(wx.Button(self, -1, "Small Button"), 0, wx.EXPAND | wx.LEFT, 20) sizer.SetSizeHints(self) self.SetSizer(sizer) - .. figure:: figures/boxsizer53.png :align: left :target: scripts/boxsizer52.py * - :: sizer = wx.BoxSizer(wx.VERTICAL) # Border size effects sizer.Add(wx.Button(self, -1, "An extremely long button text"), 0, 0, 0) sizer.Add(wx.Button(self, -1, "Small Button"), 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 20) sizer.SetSizeHints(self) self.SetSizer(sizer) - .. figure:: figures/boxsizer54.png :align: left :target: scripts/boxsizer53.py * - :: sizer = wx.BoxSizer(wx.VERTICAL) # Border size effects sizer.Add(wx.Button(self, -1, "An extremely long button text"), 0, 0, 0) sizer.Add(wx.Button(self, -1, "Small Button"), 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 20) sizer.SetSizeHints(self) self.SetSizer(sizer) - .. figure:: figures/boxsizer55.png :align: left :target: scripts/boxsizer54.py * - :: sizer = wx.BoxSizer(wx.VERTICAL) # Border size effects sizer.Add(wx.Button(self, -1, "An extremely long button text"), 0, 0, 0) sizer.Add(wx.Button(self, -1, "Small Button"), 0, wx.EXPAND | wx.ALL, 20) sizer.SetSizeHints(self) self.SetSizer(sizer) - .. figure:: figures/boxsizer56.png :align: left :target: scripts/boxsizer55.py You can see that the button is offset from the specified edges of the sizer by the number of pixels that we specified in the `border` parameter. .. admonition:: |hint| Hints * See `wx.BoxSizer.Add `_ and the `wx.StaticText constructor `_ .. figure:: figures/sizers1.png :align: right :target: scripts/sizers1.py .. topic:: |exercise| Exercises :class: green Starting from the `Sizers `_ sample: 1. Modify it to add some aesthetically pleasing borders between the widgets (say, 10 pixels). 2. Make the text control expand to fill all the remaining available space. 3. Can you make the static text right-aligned? Click on the figure for the solution. | .. admonition:: |hint| Hints * See the Wiki page on the `Widget Inspection Tool `_, the docs on `wx.CheckBox `_ and `wx.SpinCtrl `_ constructors. .. figure:: figures/sizers_plus.png :align: right :target: scripts/sizers_plus.py .. topic:: |exercise| Exercises :class: green Run the `Sizers+ `_ sample (**without looking at the code!!**), and The **Widget Inspection Tool** will pop up. 1. Using the Widget Inspection Tool, can you work out what combination/hierarchy of sizers I have used for this sample? Which flags and borders did I use for all the widgets/sizers? 2. Can you reproduce the layout I created? Click on the figure for the solution. Fonts, Colours and Bitmaps ========================== This section will briefly show some basic information about graphic device interface objects, which include fonts, colours and bitmaps. `wx.Font` --------- A font is an object which determines the appearance of text, primarily when drawing text to a window or device context. A `wx.Font `_ is determined by the following parameters (not all of them have to be specified, of course): ======================== ========================================== **Point size** This is the standard way of referring to text size. **Family** Supported families are: ``wx.DEFAULT``, ``wx.DECORATIVE``, ``wx.ROMAN``, ``wx.SCRIPT``, ``wx.SWISS``, ``wx.MODERN``. ``wx.MODERN`` is a fixed pitch font; the others are either fixed or variable pitch. **Style** The value can be ``wx.NORMAL``, ``wx.SLANT`` or ``wx.ITALIC``. **Weight** The value can be ``wx.NORMAL``, ``wx.LIGHT`` or ``wx.BOLD``. **Underlining** The value can be ``True`` or ``False``. **Face name** An optional string specifying the actual typeface to be used. If ``None``, a default typeface will chosen based on the family. **Encoding** The font encoding ======================== ========================================== .. figure:: figures/fonts.png :align: right :target: scripts/fonts.py As an example, you can create a font object and assign it to a widget like this:: static_text = wx.StaticText(parent, -1, 'Some text here') font1 = wx.Font(10, wx.SWISS, wx.ITALIC, wx.NORMAL) static_text.SetFont(font1) text_ctrl = wx.TextCtrl(parent, -1, 'Some other text') # A font can be retrieved from the OS default font and modified font2 = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) font2.SetPointSize(12) text_ctrl.SetFont(font2) `wx.Colour` ----------- A colour is an object representing a combination of Red, Green, Blue and Alpha channel (RGBA) intensity values. Valid RGBA values are in the range 0 to 255. There are three ways for setting colours. We can create a `wx.Colour `_ object, use a predefined colour name or use hex value string:: text_ctrl = wx.TextCtrl(parent, -1, 'Some text') text_ctrl_SetBackgroundColour(wx.Colour(0, 0, 255)) # OR text_ctrl_SetBackgroundColour('BLUE') # OR text_ctrl.SetBackgroundColour('#0000FF') .. figure:: figures/colours.png :align: right :target: scripts/colours.py .. figure:: figures/colours1.png :align: right :target: scripts/colours.py As an alternative, you can also use the handy *colour database* provided in the `wx.lib.colourdb `_ module and comprising more than 600 named colours:: import wx import wx.lib.colourdb import random class MyFrame(wx.Frame): def __init__(self, parent): wx.Frame.__init__(self, parent, title='Colour database') # Show the selected colour in this panel panel = wx.Panel(self) wx.lib.colourdb.updateColourDB() # Create a colour list from the colourdb database colour_list = wx.lib.colourdb.getColourList() # Get one random colour between those 600 available random_colour = random.choice(colour_list) panel.SetBackgroundColour(random_colour) `wx.Bitmap` ----------- A bitmap is a collection of bits that form an image. It is a grid of individual dots stored in memory or in a file. Each dot has it's own colour. When the image is displayed, the computer transfers a bit map into pixels on monitors or ink dots on printers. The quality of a bitmap is determined by the **resolution** and the **color depth** of the image. The resolution is the total number of pixels in the image. The color depth is the amount of information in each pixel. An example construction to create a bitmap in wxPython starting from an existing image:: bmp = wx.Bitmap('wxpython.png', wx.BITMAP_TYPE_PNG) The same can be done with other file formats, such as GIF, JPG, BMP, TIFF, etc... The full list of supported formats is available in the `BitmapType `_ documentation. When you need to use standard (i.e., platform-provided) bitmaps, you should take a look at `wx.ArtProvider `_, which is a class used to customize the look of wxPython application. When wxPython needs to display an icon or a bitmap (e.g. in the standard file dialog), it does not use a hard-coded resource but asks `wx.ArtProvider` for it instead. A number of standard bitmaps are readily available, and in particular the following: ============================================ ======================================= ================================== - ``ART_ERROR`` - ``ART_GOTO_LAST`` - ``ART_FILE_SAVE_AS`` - ``ART_QUESTION`` - ``ART_PRINT`` - ``ART_DELETE`` - ``ART_WARNING`` - ``ART_HELP`` - ``ART_COPY`` - ``ART_INFORMATION`` - ``ART_TIP`` - ``ART_CUT`` - ``ART_ADD_BOOKMARK`` - ``ART_REPORT_VIEW`` - ``ART_PASTE`` - ``ART_DEL_BOOKMARK`` - ``ART_LIST_VIEW`` - ``ART_UNDO`` - ``ART_HELP_SIDE_PANEL`` - ``ART_NEW_DIR`` - ``ART_REDO`` - ``ART_HELP_SETTINGS`` - ``ART_FOLDER`` - ``ART_PLUS`` - ``ART_HELP_BOOK`` - ``ART_FOLDER_OPEN`` - ``ART_MINUS`` - ``ART_HELP_FOLDER`` - ``ART_GO_DIR_UP`` - ``ART_CLOSE`` - ``ART_HELP_PAGE`` - ``ART_EXECUTABLE_FILE`` - ``ART_QUIT`` - ``ART_GO_BACK`` - ``ART_NORMAL_FILE`` - ``ART_FIND`` - ``ART_GO_FORWARD`` - ``ART_TICK_MARK`` - ``ART_FIND_AND_REPLACE`` - ``ART_GO_UP`` - ``ART_CROSS_MARK`` - ``ART_HARDDISK`` - ``ART_GO_DOWN`` - ``ART_MISSING_IMAGE`` - ``ART_FLOPPY`` - ``ART_GO_TO_PARENT`` - ``ART_NEW`` - ``ART_CDROM`` - ``ART_GO_HOME`` - ``ART_FILE_OPEN`` - ``ART_GOTO_FIRST`` - ``ART_FILE_SAVE`` ============================================ ======================================= ================================== | In order to use `wx.ArtProvider` and retrieve a standard bitmap, you may do something like:: bmp = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_OTHER, (16, 16)) static_bitmap = wx.StaticBitmap(parent, -1, bmp) .. admonition:: |hint| Hints * See the `GetBitmap `_ method of `wx.ArtProvider` and the `wx.StaticBitmap `_ constructor. .. figure:: figures/bitmaps.png :align: right :target: scripts/bitmaps.py .. topic:: |exercise| Exercises :class: green 1. Using the `wx.ArtProvider`, a `wx.StaticBitmap` and a `wx.BoxSizer`, put 4 standard bitmaps stacked horizontally 2. Can you create a sizer/widgets hierarchy for which the 4 bitmaps are always centered in the main application window? Click on the figure for the solution. Core Widgets ============ We will now explore a few of the most frequently used core controls in wxPython. The actual number of core classes (100) is way too big to explore them all in this tutorial, but this introductory overview should get you started and whet your appetite for more... Common dialogs -------------- Common dialog classes and functions encapsulate commonly-needed dialog box requirements. They are all `modal`, grabbing the flow of control until the user dismisses the dialog, to make them easy to use within an application. Some dialogs have both platform-dependent and platform-independent implementations, so that if underlying windowing systems do not provide the required functionality, the generic classes and functions can stand in. For example, under Windows, `ColourDialog `_ uses the standard colour selector. There is also an equivalent called `GenericColourDialog` for other platforms. wxPython wraps pretty much all the standard dialogs: - `wx.ColourDialog `_, `wx.DirDialog `_, `wx.FileDialog `_ - `wx.FontDialog `_, `wx.PageSetupDialog `_, `wx.PrintDialog `_ - `wx.MessageDialog `_, `wx.ProgressDialog `_ - `wx.FindReplaceDialog `_, etc... --------------- `wx.FileDialog` --------------- `wx.FileDialog `_ pops up a file selector box. On Windows and GTK 2.4+, this is the common file selector dialog. Looking at its constructor: .. method:: FileDialog.__init__(parent, message=u'Select a file', defaultDir='', defaultFile='', wildcard=u'*.*', \ style=wx.FD_DEFAULT_STYLE) :param `parent`: Parent window. :param string `message`: Message to show on the dialog. :param string `defaultDir`: The default directory, or the empty string. :param string `defaultFile`: The default filename, or the empty string. :param string `wildcard`: A wildcard, such as "." or "BMP files (.bmp)|.bmp|GIF files (.gif)|.gif". :param long `style`: A dialog style. The path (`defaultDir`) and filename (`defaultFile`) are distinct elements of a full file pathname. If `defaultDir` is "", the current directory will be used. If `defaultFile` is "", no default filename will be supplied. The `wildcard` parameter determines what files are displayed in the file selector, and file extension supplies a type extension for the required filename. The most common `style` bits used are ``wx.FD_OPEN`` (for a "file open" dialog) and ``wx.FD_SAVE`` (for a "file save" dialog). `wx.FileDialog` implements a wildcard filter. Typing a filename containing wildcards (``*``, ``?``) in the filename text item, and clicking on ``Ok``, will result in only those files matching the pattern being displayed. The wildcard may be a specification for multiple types of file with a description for each, such as:: wildcard = "BMP files (*.bmp)|*.bmp|GIF files (*.gif)|*.gif" As an example, to give the user the possibility to choose one Python file in a folder:: # This is how you pre-establish a file filter so that the dialog # only shows the extension(s) you want it to. wildcard = 'Python source (*.py)|*.py' dlg = wx.FileDialog(None, message="Choose a Python file", defaultDir=os.getcwd(), defaultFile="", wildcard=wildcard, style=wx.FD_OPEN) # Show the dialog and retrieve the user response. If it is the OK response, # process the data. if dlg.ShowModal() == wx.ID_OK: # This returns the file that was selected path = dlg.GetPath() print(path) # Destroy the dialog. Don't do this until you are done with it! # BAD things can happen otherwise! dlg.Destroy() ------------------ `wx.MessageDialog` ------------------ This dialog shows a single or multi-line message, plus buttons that can be chosen from ``OK``, ``Cancel``, ``Yes``, and ``No``. Under Windows, an optional icon can be shown, such as an exclamation mark or question mark. It is mostly used as informational message window. As an example, if you want to inform the user that some process in your GUI has finished running, you may do the following:: dlg = wx.MessageDialog(None, message='Calculations finished', caption='Information', style=wx.OK | wx.ICON_INFORMATION) dlg.ShowModal() dlg.Destroy() .. admonition:: |hint| Hints * For part (1), see the `Append `_ method of `wx.Menu` * For part (3) you may need to look at the `SetValue `_ method of `wx.TextCtrl` * See also `the official Python docs on I/O `_, `wx.FileDialog `_ and `wx.MessageDialog `_ constructors. .. figure:: figures/common_dialogs.png :align: right :target: scripts/common_dialogs.py .. topic:: |exercise| Exercises :class: green Using the `Widgets `_ sample: 1. Add a new "Open..." menu item in the "File" menu 2. Upon user selection of this menu, launch a `wx.FileDialog` to let the user choose a Python file 3. Read the Python file and set its content as text in the text control 4. Inform the user that the file has been loaded successfully via a `wx.MessageDialog`. Click on the figure for the solution. Common widgets -------------- In this section we are going to explore a few of the most commonly used widgets -- although not the basic ones such as `wx.ComboBox` or `wx.SpinCtrl`. ------------- `wx.Notebook` ------------- .. figure:: figures/notebook1.png :align: right This class represents a notebook control, which manages multiple windows with associated tabs. To use the class, create a `wx.Notebook `_ object and call `AddPage `_ or `InsertPage `_, passing a window to be used as the page. As an example, this is how you may add a tabbed page to your notebook control:: notebook = wx.Notebook(parent) # Create the page windows as children of the notebook page = wx.Panel(notebook) # Add the pages to the notebook with the label to show # on the tab notebook.AddPage(page, 'Page 1') | .. admonition:: |hint| Hints * See the `AddPage `_ method of `wx.Notebook` * Don't forget that any page you add **must be a child of the notebook**, i.e. the notebook itself should be the `parent` parameter for the widget that will become a page in the notebook .. figure:: figures/notebook2.png :align: right :target: scripts/notebook1.py .. topic:: |exercise| Exercises :class: green Using the `Notebook `_ sample: 1. Look for the `OnOpen` menu event handler 2. Modify the `OnOpen` method to create a `wx.TextCtrl `_ and add the text ctrl as a new page in the notebook: the text control will display the content of the Python file selected in the `OnOpen` method 3. Set the notebook tab text to be the filename (without the directory information or file extension) 4. Set the notebook `style` to show tabs at bottom instead of at top Click on the figure for the solution. ------------------- `wx.SplitterWindow` ------------------- This widget enables to split the main area of an application into parts. The user can dynamically resize those parts with the mouse pointer. Such a solution can be seen in mail clients or in burning software. You can split an area vertically or horizontally. As an example on how to use it: .. figure:: figures/splitter.png :align: right :target: scripts/splitter.py :: import wx class SplitterFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, title='wx.SplitterWindow') splitter = wx.SplitterWindow(self, -1) panel1 = wx.Panel(splitter, -1) static = wx.StaticText(panel1, -1, 'Hello World', pos=(100, 100)) panel1.SetBackgroundColour(wx.LIGHT_GREY) panel2 = wx.Panel(splitter, -1) panel2.SetBackgroundColour(wx.WHITE) splitter.SplitVertically(panel1, panel2) self.Centre() .. admonition:: |hint| Hints * See the `SplitterWindow `_ constructor and its `window styles `_ * You may want to use different background colours for the split windows to make them more visible .. figure:: figures/splitter1.png :align: right :target: scripts/splitter1.py .. topic:: |exercise| Exercises :class: green Using the `Splitter `_ sample: 1. Modify it to make a "split-split" window, i.e., a window split vertically which contains 2 windows split horizontally. 2. Looking at the `wx.SplitterWindow` window styles, can you make their sash to update "live"? Click on the figure for the solution. ------------- `wx.TreeCtrl` ------------- A tree control presents information as a hierarchy, with items that may be expanded to show further items. It is implemented natively on Windows and it uses a generic implementation on GTK and Mac OS, although newer versions of wxPython (the 2.9 ones) provide a native implementation in `DataViewTreeCtrl `_. There is also a pure-Python implementation (with many more functionalities) in `wx.lib`, termed `CustomTreeCtrl `_. A simple application showing the usage of `wx.TreeCtrl `_ is as follows: .. figure:: figures/treectrl.png :align: right :target: scripts/treectrl.py :: import wx class TreeFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, title='wx.TreeCtrl') tree_ctrl = wx.TreeCtrl(self, -1, style=wx.TR_DEFAULT_STYLE) # Add the tree root root = tree_ctrl.AddRoot('Root item') for i in range(10): tree_ctrl.AppendItem(root, 'Item %d'%(i+1)) tree_ctrl.ExpandAll() self.Centre() .. admonition:: |hint| Hints * See the `TreeCtrl `_ constructor and its `window styles `_ .. figure:: figures/treectrl1.png :align: right :target: scripts/treectrl1.py .. topic:: |exercise| Exercises :class: green Using the `TreeCtrl `_ sample and the Python `keyword module `_: 1. Classify the Python keywords alphabetically, using the first letter of the keyword (i.e., ``and`` goes into ``a``, ``for`` goes into ``f`` and so on): * For each letter, add a child to the treectrl root * In each child of the root item, add its corresponding keyword(s) 2. Looking at the `wx.TreeCtrl` window styles, can you make the selection highlight extend over the entire horizontal row of the tree control window? How do you make the selection editable? Click on the figure for the solution. Creating Custom Controls - Basics ================================= In this section, we are going to look at few generalities about custom controls and how to draw custom objects on some wxPython windows. Unfortunately the entire "owner-draw" subject is way too big to be covered during a short tutorial, but this section should at least get you started and whet your appetite for more. **(If you want to hear more details, please feel free to contact me at any time during the PythonBrasil8 conference).** Device contexts and paint events -------------------------------- A DC is a `device context` onto which graphics and text can be drawn. It is intended to represent different output devices and offers a common abstract API for drawing on any of them. DCs have many drawing primitives: * DrawBitmap, DrawEllipse, DrawLine, DrawLines, DrawPoint, DrawPolygon, DrawRectangle, DrawRoundedRectangle, DrawSpline, DrawText, etc... And they work with GDI objects: * wx.Font, wx.Bitmap, wx.Brush, wx.Pen, wx.Mask, wx.Icon, etc... Some device contexts are created temporarily in order to draw on a window. This is true for some of the device contexts available for wxPython: * `ScreenDC `_: Use this to paint on the screen, as opposed to an individual window. * `ClientDC `_: Use this to paint on the client area of window (the part without borders and other decorations), but do not use it from within an `PaintEvent `_. * `PaintDC `_: Use this to paint on the client area of a window, but only from within a `PaintEvent `_. * `WindowDC `_: Use this to paint on the whole area of a window, including decorations. This may not be available on non-Windows platforms. Let's focus on the `PaintDC `_, which is one of the most commonly used. To use this device context, we want to bind a paint event for a window to an event handler, which will be responsible for drawing (almost) anything we want onto our window: .. admonition:: |documentation| Documentation * `wx.DC documentation `_ * `wx.PaintEvent documentation `_ .. figure:: figures/paint_events.png :align: right :target: scripts/paint_events.py :: class MainWindow(wx.Frame): def __init__(self, parent, title): wx.Frame.__init__(self, parent, title=title) # Bind a "paint" event for the frame to the # "OnPaint" method self.Bind(wx.EVT_PAINT, self.OnPaint) self.Show() def OnPaint(self, event): dc = wx.PaintDC(self) # Set a red brush to draw a rectangle dc.SetBrush(wx.RED_BRUSH) dc.DrawRectangle(10, 10, 50, 50) The `PaintEvent` is triggered every time the window is redrawn, so we can be sure that our red rectangle will always be drawn when the operating system wants to "refresh" the content of our window. | Similar things can be done using other graphical primitives, like `DrawPoint `_: .. figure:: figures/paint_events2.png :align: right :target: scripts/paint_events2.py :: def OnPaint(self, event): dc = wx.PaintDC(self) # Use a red pen to draw the points dc.SetPen(wx.Pen('RED')) # Get the size of the area inside the main window w, h = self.GetClientSize() # Draw a sequence of points along the mid line for x in range(1, w, 3): dc.DrawPoint(x, h/2) | .. admonition:: |documentation| Documentation * `wx.Font documentation `_ Or drawing text strings onto our window by using `DrawText `_: .. figure:: figures/paint_events3.png :align: right :target: scripts/paint_events3.py :: def OnPaint(self, event): dc = wx.PaintDC(self) # Use a big font for the text... font = wx.Font(20, wx.SWISS, wx.NORMAL, wx.BOLD) # Inform the DC we want to use that font dc.SetFont(font) # Draw our text onto the DC dc.DrawText('Hello World', 10, 10) | .. admonition:: |hint| Hints * See the `DrawLine `_, the `DrawText `_ and the `GetFullTextExtent `_ methods. .. figure:: figures/dc1.png :align: right :target: scripts/dc1.py .. topic:: |exercise| Exercises :class: green Modify the `DC `_ sample such that: 1. Using the `random module `_, draw a number (let's say 100) of random lines inside the `wx.Frame` client area. 2. Looking at the previous snippets of code which uses `DrawText`, and using the `width` and `height` returned by the `DC.GetFullTextExtent` method, can you draw a string centered in the frame client area? Click on the figure for the solution. Additional resources and tutorials ---------------------------------- * The `ZetCode tutorial on drawing `_ * Creating custom control with wxPython `Wiki page `_