How to begin building GUIs with Python Contents:

Transcription

How to begin building GUIs with Python Contents:
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
.
[email protected]
Contents:
Part 1 – wxGlade: a primer on drag and drop GUI construction without programming
page 2
Part 2 – wxPython: a primer and best practice for animating a wxGlade generated GUI
page 5
Part 3 – wxPython: a best practice for dealing with long running tasks
page 11
Part 4 – wxPython: a template for a threading application library global module
page 15
GUI in Python_primer_bvn.odm
Page 1 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
Part 1 – wxGlade
wxGlade is a
GUI (Graphical User Interface) designer. This document is an attempt to provide the very basic
and core information assumed, and left out, by every other web document claiming to be an introduction to
wxGlade. It is modeled after GUI development with wxGlade by Johan Vromans, of Squirrel Consultancy,
at <[email protected]>.
Words in the format shibboleth are introductions to technical terms used by other documents and
references. Subsequent references appear in italics. Example – shibboleth.
O What wxGlade (as of October 2009) is and is not:
wxGlade is not an IDE (Integrated Development Environment). This is a good thing, because best practice requires
the graphical user interface (GUI) design to be as independent of the application code specifics as possible.
} wxGlade can be used effectively by non-programmers to describe the look and feel of an application.
} wxGlade can be used to rapidly prototype an application without the expense and delay of programming.
}
Technical backgrounder:
O
wxGlade borrows it's design, but not it's implementation, from Glade - a GUI designer for the Gnome desktop
environment based on the GTK+ library. WxGlade is 100%, un-enhanced, wxPython code.
} wxGlade is platform (operating system) independent.
} wxGlade generates application code for numerous application programming languages.
} Version 0.6.3 is dependent upon Python v2.5.2 and wxPython v2.8.9.1.
} Part 2, (also a primer) describes how to animate a wxGlade prepared GUI design with wxPython based application
code.
}
How to create a layout
Vocabulary:
O
Everything visible, or potentially visible, on the GUI screen or workspace is a widget.
If a widget can contain other widgets, then it may have one or more sizers – the sole means of containment for
sub-widgets. There are more kinds of sizers than are described here.
}
}
How to:
O
}
stake out some screen real estate for an application – add a frame for the application.
Frame
Component view
Tree view
There are 5 views (non MDI frames) in wxGlade: component, tree, property, pre, and design.
GUI in Python_primer_bvn.odm
Page 2 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
Design view
Empty slot
Pre view
Property view
(Layout tab)
get to the Property view of a widget? Double click it's frame in the Tree view to open the Design
view.
‚ add a sub-frame? You don't, you add a Panel/ScrolledWindow to the sizer of a frame, read the
following:
‚ carve up the area of a frame – understanding the Property view displayed in the layout tab:
| Physically
– add a wxBoxSizer and drop in some sub-widgets,
| Visually
– add a wxStaticBoxSizer and drop in some sub-widgets.
A wxStaticBoxSizer wraps a thin line, with label (static text), around the the container widget's real
estate.
‚ Sub-widgets – sub-widgets occupy slots in a sizer, one sub-widget per slot.
| Slots can be added, inserted, deleted, and re-ordered.
| An empty sizer slot appears as a hatched area in the Design view.
| Vertical sizers order slots from top to bottom and horizontal sizers, from left to right.
‚
GUI in Python_primer_bvn.odm
Page 3 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
Positioning sub-widgets.
| Part 1 – add a slot to the sizer. Right click the sizer in the Tree view to add/delete slots.
~ To place a widget in the empty slot, select a widget from the Component view, then select the empty
slot.
~ To delete an empty slot, select it, use the <Delete> key, or right click and select remove.
~ A sizer arranges (packs) it's sub-widgets into it's parent widget either top to bottom (wxVertical) or
left to right (wxHorizontal). The grid sizers have other packing rules.
G The slot order is determined by the sub-widget's property | layout position value, previously called
option.
G How much real estate does a sub-widget get? It's controlled by the sub-widget's proportion value.
Q By default (all sub-widgets have a proportion value of 0), the area is divided equally, or
Q The proportion values are summed and each sub-widget gets it's proportion out of the total.
G The border value is in pixels and relates to how much area around the sub-widget could be set aside
for aesthetic reasons, a green belt, so to speak. The sides of the border that get this set aside area are
chosen the the border group of properties.
| Part 2 – the other dimension, use the alignment group of properties.
~ If the sizer is wxVertical then the sub-widget can be:
full width – wxExpand,
left adjusted – default,right adjusted – wxALIGN_RIGHT
~ If the sizer is wxHorizontal then the sub-widget can be:
full height – wxExpand,
top adjusted – default, bottom adjusted – wxALIGN_BOTTOM
~ wxSHAPED – a means to keep the aspect ratio of the sub-widget unchanged.
‚
wxGlade restrictions and limitations:
WxGlade is limited to appearance issues. Behavior issues must be communicated to the application
programmer by other means.
} Menus:
‚ The top level menus (the ones visible on the menu bar) only expose the sub-menu items – which can
actually do something for your application.
‚ Keyboard accelerators cannot be defined in wxGlade – only hinted at. The & in front of a menu item
character will inform application code requirements.
} Statusbar field sizes are measured in pixels. They can be viewed as static in size. If other sizing rules
(such as proportional to the frame width) inform the application programmer.
} Timed behaviors: Timers, like accelerator keys, are not visible. The application code can easily add
timers to the GUI design, see the wxPython Timer event.
} Idle or spare time behaviors: Like timers, the application code can implement these. See the wxPython
idle event.
} Widget interactions: The design may require that some widgets enable and disable others. Also, some
widgets cascade to the behaviors of other widgets.
O
GUI in Python_primer_bvn.odm
Page 4 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
Part 2 – wxPython
This document is both a best practice and a primer.
O Assumptions:
} The GUI design is complete and ready to be animated by an application program.
} wxGlade will be used to assign programmer friendly names to it's graphical elements (widgets).
} A working knowledge of Python.
} Access to http://www.wxpython.org/docs/api/wx-module.html. The official reference for wxPython.
Author's comments.
My ~40 years of professional programming, prior to retirement, never involved human interface.
} I have no talent for, nor patience with GUI design, and for those 40 years, no interest as well.
} My approach to GUI design is as how a small child approaches picture design when using finger paints.
O
}
The documentation approach.
This document is very lightweight and superficial. It serves, mostly as a guide or practice, for what to
look-up in the wxPython reference document.
} The structure of this document is in the following order.
‚ The generic, no frills, application.
‚ The frame widget (there is no GUI without a frame – AKA Device Context or Canvas)
‚ Other, common, widgets and events are in alphabetic order.
} This document contains a small, introductory, subset of wxGlade's capability.
} wxGlade generates only a small subset of wxPython's full capability.
O
}
Implementation approach.
} To keep the application development process as simple and rapid as possible, no permanent application
code will be introduced into the wxGlade generated code. The generated code can be deleted and
regenerated from the wxg without loss, excepting, perhaps, the path to the application's icons.
} The application code is as independent of the GUI design particulars as possible, so that the GUI can be
modified with minimal change to the application and vice versa.
} This practice requires a Python library style of implementation. Python libraries are the CWD (Current
Working Directory) and the sub-directories of the PYTHONPATH directory. To make a directory into a
Python library, incorporate an __init__.py file (module). It can be empty. Modules (files ending in .py)
in a Python library can be imported. Directory and file names are subject to the Python import syntax.
} Most widgets have events. The Bind method of the frame instance, connects the event to the eventhandler code, which always has an event object as it's sole argument.
} In this primer, the wxGlade generated code will be called gui.py and the application is app.pyw.
} app.pyw is not object oriented. There is little use for polymorphic event handlers, the value of inheritance
is dubious, and integration with the frame class encumbers application evolution.
O
O
The generic application app.pyw. Note: the pyw signifies that it runs in a window and not a console.
#!/usr/bin/python
signifies that this is the entry point module
import wx
import wxPython
import gui
import the wxGlade generated code
TheApp = None
TheFrame = None
GUI in Python_primer_bvn.odm
create the basic globals to contain the GUI instances
not required, but helps with code readability
Page 5 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
def guiInit():
global TheApp, ...
TheApp = wx.PySimpleApp()
wx.InitAllImageHandlers()
...
TheApp.MainLoop()
instantiate the generic application
“
instantiate the frames and bind events to them
start the windows event (message) loop
if __name__ == "__main__": guiInit()
main program
Programming style considerations.
The programmer should modify the wxGlade GUI definition by assigning app.pyw friendly names and
then re-generate the GUI code – File | Generate gui.py.
‚ The names are defined on the Common tab of each widget.
‚ These names do not add value to the GUI design, but are critical to the readability of app.pyw.
| Generally, the widget's name is important. The wxGlade default name is prefixed with the widget type.
A very useful convention. The event handler names should also use this convention.
| With a frame widget, the class name is important.
| With tools and menu (command) items the integer id value is important. Assigned id's should be
unique within the application scope. This facilitates moving widgets between frames. The sharing of
event handlers should be defined in app.pyw (via Bind), not in gui.py.
} Widget properties, not modified by the application, should be set in wxGlade to minimize app.pyw code
clutter. The owner of a property ( gui.py or app.pyw) should never be in doubt. If app.pyw owns the
property, it should not rely on gui.py for property initialization.
} The application's GUI support code app.py should be in it's own module, separated from any significant
engine code. The event handlers should be in alphabetic, rather than logical, order. This makes them easy
to locate and removes any visual binding to the GUI design.
} Event handlers should use Event.GetEventObject() rather than hard-code the widget name.
} Notes on documentation style:
‚ Python code is represented in a mono-spaced, bold, font – def Init():.
‚ Externally defined names are italicized – gui.FrameClass1.
‚ Ellipses (...) are used to indicate a so-on-and-so-forth situation.
O
}
Advanced subjects.
} Cascading events – where one event triggers another.
‚ In complex applications, there can be need to cascade events. This becomes more likely when the
application involves long running tasks.
‚ The practice is to use wx.CallAfter() to schedule the cascaded events.
} The need for wx.Yield() or TheApp.Yield() is a warning that the application is probably using an
inappropriate architecture. There are many web articles and forum threads that discuss this issue.
} For a practice on using wxPython with threading see Part 3 of this document.
O
The Frame widget.
} WxGlade defined frames should be instantiated during GUI initialization. Especially, if you need to use
wx.NewId()for accelerator and timer events.
O
def guiInit():
global TheApp, TheFirstFrame, TheSecondFrame,
TheFirstFrame = gui.aFrameClass(None, -1, "")
TheFirstFrame.Bind(wx.EVT_xxx, EventHandler, WidgetName)
GUI in Python_primer_bvn.odm
Page 6 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
Every application has a top window (a main frame :-). This needs to be established before the MainLoop
begins execution. Assuming that the TheTopFrame instance is the top window:
}
TheApp.SetTopWindow(TheTopFrame)
TheApp.SetExitOnFrameDelete(True)
TheFirstFrame.Show()
} Quick and very dirty application kill: TheApp.ExitMainLoop(),
} Some useful frame methods are:
‚ TheFrame.Hide()
‚ TheFrame.SetSize(aTuple)
‚ TheFrame.SetPosition(aTuple)
better to use TheTopFrame.Destroy().
TheFrame.Show()
TheFrame.GetSize()
TheFrame.GetPosition()
causes exit on top window
Frame events {TheFrame.Bind(...)} – EVT_CLOSE {pre-.Destroy()}, EVT_SIZE {re-size} and
EVT_MOVE {relocate}.
} The frame's MenuBar widget.
TheFrame.Bind(wx.EVT_MENU, EventHandler, id=id)
works for sub-menus only
TheFrame.menubar.Enable(id, boolean)
enables/disables sub-menus
TheFrame.menubar.EnableTop(id, boolean)
enables/disables top level menus
} The frame's ToolBar widget.
‚ TheFrame.Destroy()
}
TheFrame.toolbar.EnableTool(id, boolean)
TheFrame.Bind(wx.EVT_TOOL, EventHandler, id=id)
}
The frame's StatusBar widget.
index: the status field offset.
The statusbar field widths are essentially hard coded by wxGlade. To re-size:
‚ TheFrame.TheStatusbar.SetStatusText(Message, index)
‚
TheFrame.TheStatusbar.SetStatusWidths([100, 200, ...])
The keyboard accelerator table.
Since the accelerator table is not visible, wxGlade provides no widget to Bind.
} The most critical statement is the last – SetFocus().
‚ If TheFrame has the application's focus, it will not get any keyboard or mouse events. Some input
oriented widget within TheFrame must have the application's focus.
‚ Each platform (Windows, Linux, Mac) has a different default for what has focus and each has it's own
unique problems. Do not rely on the default, if portability is a goal.
unusedID = wx.NewId()
how to get an id that does not interact with gui.py
This only works after all the frames have been instantiated and gui.py's id's have been reserved.
O
}
aTable = wx.AcceleratorTable([
(wx.ACCEL_ALT,
ord('X'), exitID),
(wx.ACCEL_CTRL,
ord('H'), helpID),
(wx.ACCEL_CTRL,
ord('F'), findID),
(wx.ACCEL_NORMAL, wx.WXK_F3, findnextID)])
TheFrame.SetAcceleratorTable(aTable)
TheFrame.Bind(wx.EVT_MENU , EventHandler, id = ID)
...
TheFrame.SomeInputWidget.SetFocus()
GUI in Python_primer_bvn.odm
Page 7 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
The Button widget.
O
TheButton = TheFrame.ButtonName
TheFrame.Bind(wx.EVT_BUTTON, EventHandler, TheButton)
TheButton.Enable()
TheButton.Disable()
TheButton.Hide()
TheButton.Show()
TheButton.SetLabel(NewText)
The Choice widget – used to select one of a list of strings.
O
TheChoice = TheFrame.ChoiceName
TheChoice.Clear()
# remove all data
TheChoice.Append(NewString, MyData)
TheChoice.Enable(Boolean)
TheFrame.Bind(wx.EVT_CHOICE, EventHandler, TheChoice)
where
def EventHandler(Event):
TheChoice = Event.GetEventObject()
TheSelection = TheChoice.GetStringSelection()
TheIndex = TheChoice.GetSelection()
TheSelection = TheChoice.GetString(TheIndex)
MyData = TheChoice.GetClientData(TheIndex)
The ComboBox widget – is like a combination of an edit control and a list-box. It can be displayed as static
list with editable or read-only text field; or a drop-down list with text field. A combo-box permits a single
selection only. Combo-box items are indexed from zero.
O
TheCombo = TheFrame.ComboBoxName
TheCombo.Clear()
# remove all data
TheCombo.Append(NewString, MyData)
TheCombo.Enable(Boolean)
TheFrame.Bind(wx.EVT_COMBOBOX, EventHandler, TheCombo)
where
def EventHandler(Event):
TheCombo = Event.GetEventObject()
TheSelection = TheCombo.GetStringSelection()
TheIndex = TheCombo.GetSelection()
TheSelection = TheCombo.GetString(TheIndex)
MyData = TheCombo.GetClientData(TheIndex)
The Gauge widget.
O
TheGauge = TheFrame.GaugeName
TheGauge.SetRange(MaxValue)
TheGauge.SetValue(Value)
The gauge range from 0 to MaxValue
The amount done from 0 to MaxValue
The Idle event – triggers when TheApp.MainLoop() becomes idle.
} Idle events can be added to a frame widget with TheFrame.Bind(wx.EVT_IDLE, EventHandler).
} To flush the event queue and force an idle event, use wx.WakeUpIdle()
} To delay an event until the idle condition happens, use (in the premature event handler):
O
from collections import deque
EventQ = deque()
GUI in Python_primer_bvn.odm
Page 8 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
def AnEventHandler(Event):
global EventQ
if TheApp.Pending(): EventQ.append((AnEventHandler, Event))
else: ...
in the idle event handler
if len(EventQ):
(AnEventHandler, AnEvent) = EventQ.popleft()
wx.CallAfter(AnEventHandler, AnEvent)
return
one per idle event, or they will just queue up again and
queues a 2-tuple
again, ad-nauseum
The Label or StaticText widget.
O
TheFrame.LabelName.SetLabel(Message)
O
The ListBox widget – a proxy of the C++ ListBox control. Recommend the use of ListCtrl instead.
O
The ListCtrl widget – a proxy for the C++ ListCtrl. A grid control.
TheList = TheFrame.ListCtrlName
}
A ListCtrl is essentially a grid. To insert a column
TheList.InsertColumn(ColumnIndex, “col_title”)
TheList.SetColumnWidth(ColumnIndex, pixels)
} To insert a row at the end use an index (Index) of -1
RowIndex = TheList.InsertStringItem(sys.maxint, ListItemText, RowIndex)
TheList.SetStringItem(RowIndex, ColumnIndex, NewString)
}
To respond to an item selection
TheFrame.Bind(wx.EVT_LIST_ITEM_SELECTED, EventHandler, TheList)
where
def EventHandler(Event):
global TheSelectedListItemText
TheList = Event.GetEventObject()
RowIndex = TheList.GetFocusedItem()
TheSelectedListItemText = TheList.GetItemText(RowIndex)
O
The Timer event – triggers when the elapsed time, in milli-seconds, expires.
Useful statements for manipulating the timer event:
TheFrame.Bind(wx.EVT_TIMER, EventHandler)
There can be only one per application
TheFrameTimer = wx.Timer(TheFrame, Id)
Multiple timers OK, Id of -1 is the default
TheFrameTimer.Start(mSec, boolean)
boolean is True if one shot
Id = Event.GetId()
can be used in the event handler to identify the timer
TheFrameTimer.Stop()
stops timer events, eventually.
TheFrameTimer.Destroy()
stops timer events, now.
}
O
The TextCtrl widget.
TheText = TheFrame.TextCtrlName
TheText.ChangeValue(NewString)
TheText.Enable(Boolean)
TheFrame.Bind(wx.EVT_TEXT_ENTER, EventHandler, TheText)
where
def EventHandler(Event):
global NewText
TheText = Event.GetEventObject()
NewText = TheText.GetValue()
GUI in Python_primer_bvn.odm
Page 9 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
}
To figure out how many lines can be displayed without a scroll bar in TextCtrl use:
Width, Height = TheText.GetSize()
Trash, TextLineHeight = TheText.GetTextExtent('MjgI')
DisplayableLines = Height / TextLineHeight
}
Always use a ring buffer for displayable (last n messages) log messages.
The TreeCtrl widget.
} How to build a tree with events:
‚ Create a data structure that can be used to identify each node in the tree. If the number of levels is
known, use a list [] with one fewer members as there are levels in the tree. Use -1 or None as sub-branch
data to identify upper levels. Otherwise, the list may be of variable length.
‚ Create a root (may be hidden and there can be only one per control). The root data structure is
constructed from either -1 or None values or is an empty list, depending on the prior decision.
O
TheTree = TheFrame.TreeCtrlName
Data = wx.TreeItemData(data_list)
convert the list to wx format
Root = TheTree.AddRoot(RootLabel, -1, -1, Data)
‚ Create branches – aBranch = TheTree.AppendItem(Root, aLabel, -1, -1, Data)
‚ Recursively – aSubBranch = TheTree.AppendItem(aBranch, aSubLabel, -1, -1, Data)
‚ To use an id instead of Data try:
aSubBranch = TheTree.AppendItem(aBranch, aSubLabel)
aSubBranch.SetId(id)
‚ To expand a branch – TheTree.Expand(aBranch)
‚ To collapse a branch – TheTree.Collapse(aBranch)
‚ To reset a tree – TheTree.CollapseAndReset(Root) keeps the root and prunes all branches
‚ To remove a sub-branch – TheTree.CollapseAndReset(aBranch)
}
Some tree event handling:
TheFrame.Bind(wx.EVT_TREE_SEL_CHANGED, EventHandler, TheTree)
where
def EventHandler(Event):
global DataTuple or Id
TheTree = Event.GetEventObject()
TreeCtrlItemSel = TheTree.GetSelection()
TreeCtrlItem = TheTree.GetItemData(TreeCtrlItemSel)
DataTuple = TreeCtrlItem.GetData()
returns a tuple that
resembles the original data_list.
Or
Id = TreeCtrlItem.GetId()
O
Great techno-pun – how to add filling to a shell in Python? Import wx.py.crust
GUI in Python_primer_bvn.odm
Page 10 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
Part 3 – long running tasks
This document is a best practice and it takes a primer approach to the subject.
SMP (Symmetric Multi-Processing) includes multiple CPU's, multiple cores, and hyper-threaded processors.
The threading mechanisms, of our computing platforms, are getting more efficient at exploiting SMP hosts.
The Python engine is, itself, not threaded. It interprets one byte-code at a time, without concurrency. The
engine protects itself from the platform's concurrency optimization with the GIL (Global Interpreter Lock).
Benchmarks of alternatives to the GIL have proven to be, so far, unsatisfactory.
This restriction does not extend to the Python standard libraries, which seem to be, exclusively, wrappers
for well tested and widely used C and C++ libraries – as are thread, threading, and wx. The host's threading
mechanism and the needs of the Python engine are in conflict. This practice exploits this situation.
There is a serious risk of threading deadlocks, not to mention thread thrashing, on SMP hosts. My first
wxPython SMP threading application used 80% of a processing core. Following these practices, got this down
to under 7% - without reducing the amount of Python code being executed. This same application used ~20%
of a uni-processor host with the same computational power as one of the SMP cores.
There are a number of SMP exploitive modules, such as multiprocessing and select, that can be used in
lieu of thread. They are a better choice for processor intensive, long running tasks.
O A threaded implementation, using this practice, extends the Python library style of implementation. A
typical set of modules in the application library containing two long running tasks would be: __init__.py,
app_main.pyw (parent thread), app_gui.py (wxPython GUI thread), app_wx.py (from wxGlade),
app_task1.py, app_task2.py, and app_global.py (global). These file and folder names must comply
with Python naming conventions, because of the import statement.
O Avoid wx.Yield() and it's variants. Especially on SMP hosts. time.sleep(n) statements provide sufficient
thread switching capability. If wx.Yield() is required, it is a sign that something, very subtle, is wrong.
O One of the attractive aspects of threading is the opportunity for the close coupling of diverse, long running,
tasks, not available in multiprocessing solutions. However, the close coupling, is still a bad practice, and
often has dire consequences on SMP hosts – even with the GIL.
O To prevent unintended thread interactions, the code should be divided up into independent modules based
on thread use. Note: this is not about logic errors nor readability. Code debuggers are useless for tracking
down semantic timing errors that can change, appear, and disappear, with the number and speed of the host's
processors. It's best to leave such errors out of the code from the very beginning :-)
} The main module (parent thread) is responsible for child thread initiation, recovery, shutdown, and other,
high level, housekeeping functions. In short, it should be brief, or is that; in brief, it should be short?
} The GUI thread modules (app_gui and app_wx) should execute asynchronously of the main thread and of
the long running tasks. This GUI thread should, ideally, be the only modules that import wx. To avoid
deadlocks, all interactions between threads should be buffered – especially on SMP hosts.
} A common (global) module is a best practice. One that can be imported by all the other modules. It has no
thread of it's own. It is used for inter thread communications ( ITC – as opposed to IPC, Inter Process
Communication) that loosely couple the application's threads. See the template at the end of this
document.
} Use thread-safe, critical section like, mechanisms for global functions. A single, global lock for global
functions, is a good practice. It reduces the tendency to put lots of code into the global module and reduces
the chances of deadlock causing calls between the asynchronous threads.
TheLock = thread.allocate_lock() how to create a lock for critical sections.
def TheGlobalFunction():
global TheLock
TheLock.acquire(); ...;
GUI in Python_primer_bvn.odm
TheLock.release(); return
Page 11 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
A thread-safe switching mechanism to keep the GUI current in conjunction with CPU intensive, long
running threads. Note: this is not a good practice. Consider some form of multiprocessing, which can
exploit SMP, instead of threading.
}
def YieldToGui():
global TheLock, GuiThreadIdent
TheLock.acquire()
if GuiThreadIdent <> thread.get_ident():
wx.Yield()
TheLock.release()
}
avoid deadlock creating calls into the GUI thread
A message passing queue:
from collections import deque
MsgQ = deque()
a common, thread safe message queue.
If these messages are destined for a widget – see my practices for GUI child threads.
} Soft, non-blocking, signaling mechanism to co-ordinate inter thread communication.
KeepRunningSwitch = False
A logging facility is an important technique for debugging a herd of free running threads. A rotating set of
log files prevents infinite file growth. See the example at the end of this document.
} Example of the use of gbl.LogException:
}
import the_long_module_name_of_global_module as gbl
try:
...
except:
gbl.LogException('Oh! Darn!')
The parent thread – encapsulated in the main (entry point) module.
} In my wxglade for python programmers, the main module ( app.pyw) managed the GUI. In threaded
use, the module name depreciates to app.py and it encapsulates only the GUI child thread:
thread.start_new_thread(app.GuiInit()) start GUI child thread
However, this is not a good practice.
} The parent thread should have a thread manager, which looks like:
O
def ThreadManager():
App_gui.Start()
App_Task1.Start()
long running task #1
App_Task2.Start()
long running task #2
while True:
try:
can insert thread restart code in here
time.sleep(CHECK_INTERVAL)
except KeyboardInterrupt:
catches the thread.interrupt_main() child
gbl.LogInfo('Starting orderly shutdown')
App_Task2.Stop()
App_Task1.Stop()
break
thread call
try:
ThreadManager()
gbl.Log('Thread Manager stopped')
except:
gbl.LogException('Thread Manager crashed')
App_Task2.Stop()
App_Task1.Stop()
App_gui.Stop()
GUI in Python_primer_bvn.odm
Page 12 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
Anybody should be able to call the Start or Stop of each thread management module. Start and Stop
need to be thread-safe.
} The importance of parent versus child thread is determined by the platform's current threading
mechanism, which is unpredictable.
} Use of import very_long_global_module_name as gbl keeps clutter out of the module.
}
The child threads – each encapsulated in it's own set of modules.
Use of import very_long_global_module_name as gbl keeps clutter out of the modules.
} General child thread practices shared with all long running threads – hopefully the GUI is one of them :-)
‚ To avoid deadlocks, coordination with and between child threads requires a very polite, if-you-please
and thank-you-for-your-cooperation signaling technique, best implemented in the gbl (global) module.
| Python has no explicit public / private declaration. The use of gbl communicates a public intent.
| The term is coordinate is used in lieu of synchronize because coordination is the intent and
synchronized is the hoped for, but never guaranteed, outcome.
| The process is:
~ Change the state of a thread request variable.
~ Use time.sleep(Sec) to wait patiently (poll) for an appropriate change in the state of a response
variable. This is how the Internet protocols work and is what is meant by “all Internet protocols are
defined as unreliable”. Threads should assume that all other child threads are unreliable.
| If things appear to be unresponsive, there is the logging facility in gbl to make note of that condition.
‚ It is often desirable to have some sort of activity counter in gbl, or in the thread module, so that the
main (parent) thread knows that the child thread is alive and well.
‚ In summary, each child thread should have something like:
| def Start():
which initializes the child thread environment
O
}
global ThreadRequestRun
ThreadRequestRun = True
thread.start_new_thread(Run()) some sort of thread run statement
while not gbl.ThreadIsRunning: time.sleep(1)
block return until running
| def Stop():
cleans up the environment so Start can be called again
global ThreadRequestRun
ThreadRequestRun = False
while gbl.ThreadIsRunning: time.sleep(1)
block return until not running
| A gbl.ThreadIsRunning boolean, in simple cases, or
| A set of state variables in gbl – request, response, and heart beat.
The GUI thread requires special consideration. Stops need to be routed through the ThreadManager, so
that the request is routed to all long running tasks. Also, recursive Stops cause havoc. Create two locks:
}
GuiStopLock = thread.allocate_lock();
‚
GuiStartLock = thread.allocate_lock()
The GUI run loop needs to be instrumented for termination:
def ExitApp(Event): typical GUI exit handler for menus, buttons, and keyboard accelerators
global GuiStopLock;
if GuiStopLock.acquire(0): thread.interrupt_main()
and in the GUI MainLoop run code
gbl.ThreadIsRunning = True
TheApp.MainLoop()
where the GUI thread spends most of it's time
gbl.GuiIsRunning = False;gbl.Log('GUI thread shut down')
if GuiStopLock.acquire(0):
thread.interrupt_main()
GUI in Python_primer_bvn.odm
Page 13 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
Start is quite simple. The GuiStartLock debounces (prevents
def Start():
global GuiStartLock, GuiStopLock
if GuiStartLock.acquire(0):
if GuiStopLock.locked(): GuiStopLock.release()
thread.start_new_thread(Run, ())
‚
‚
auto repeats of) the GuiStopLock.
and Stop needs (caution: Stop can be called multiple times without a Start).
if GuiStopLock.acquire(): thread.interrupt_main() force ThreadManager()
elif GuiStartLock.locked():
don't do this unless GUI has been started
TheTopFrame.Destroy()
terminates TheApp.MainLoop()
timer.sleep(1)
allow the GUI thread to process
GuiStartLock.release()
enables Start
From non-GUI threads, direct use of widgets must be buffered.
‚ Suitable buffer methods are wx.CallAfter() and wx.CallLater(). See the global module example.
‚ To pass widget destined data through gbl, the wx.Timer event is an effective tool.
| Use from collections import deque to generate thread-safe queues such as MsgQ = deque().
Also, use LclMsg = MsgQ.popleft() to process the thread-safe queue.
| The wx.Timer() event is relentless (many articles on the Web). Use a thread-safe lock. Example:
if TimerLock.acquire(0):
if running, let the next wx.Timer event do it
}
Msg = gbl.MsgQ.popleft()
... do widget processing
TimerLock.release()
To shut down the GUI, use the lock to gain well timed access and Destroy() the timer. Add this code
to Stop before TheTopFrame.Destroy()
TimerLock.acquire()
block until the timer event has completed & prevent another
~
TheTimer.Stop()
TheTimer.Destroy()
TimerLock.release()
| Alternatively, wx.MutexGuiEnter() and wx.MutexGuiLeave() can be used in lieu of TimerLock.
‚ The idle event is highly unpredictable in SMP hosted, threaded applications; but it can be useful.
‚
build a custom event with:
GenericEvent = wx.NewEventType()
EVT_CUSTOM = wx.PyEventBinder(GenericEvent, Id)
and use it from other threads with
wx.PostEvent(CustomEventHandler, EVT_CUSTOM)
Other performance oddities.
The ctypes library bypasses the GIL. A call to a ctypes member function, that does nothing, can
lubricate the platform's threading mechanism. But, then, this seems to be the common behavior of all of
the standard Python libraries. Long stretches and loops of pure Python code (not using standard library
functions) can be interspersed with these dummy calls with interesting performance results.
} The Python engine, regularly, gives the platform's threading mechanism a chance to do something. When
last I checked, this was about every 150 byte-code interpretations. The rules vary.
} As pointed out in the performance example at the beginning, this can improve or hinder (thread thrashing)
performance without any alteration to the application's code.
O
}
GUI in Python_primer_bvn.odm
Page 14 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
Part 4 – A template for the application global module
A copy & paste template of the app_global.py in threaded wxPython application library. Notes:
The GUI thread stuffs the callback:
gbl.GuiMsgCallback = MyRealMsgDisplay where def MyRealMsgDisplay(Msg):
Then any thread or module can use gbl.MsgToGui('message')
is defined.
Example of a main module call to initialize the logging facility
gbl.LogSetup('Foo.log', gbl.Info, Size = 50000)
'''
+-------------------------------------------------------------------+
| A Python module
Created by Bruce vanNorman on 15.Aug.10 |
| purpose: to provide global data and process for long running tasks|
+-------------------------------------------------------------------+
'''
import logging, logging.handlers, sys, thread, traceback, time, wx
## --- standard variables ------------------------------------------TheLock = thread.allocate_lock()
Logger = None
Debug = logging.DEBUG
Info = logging.INFO
Warn = logging.WARN
Error = logging.ERROR
Version = 'undefined'
## --- basic thread control info -----------------------------------GuiRunning = False
FirstThreadRunning = False
SecondThreadRunning = False
## --- custom thread global variables ------------------------------## --- the basic services ------------------------------------------def GuiMsgCallback(Msg):
print 'call back not supplied'
sys.exit()
def MsgToGui(Msg):
global GuiRunning
if GuiRunning:
wx.CallAfter(GuiMsgCallback, Msg)
else:
Log('Msg to GUI & GUI is not running')
Log(Msg)
def LogSetup (LogFileName, Level, Size = 10000, Count = 2):
global Logger
Logger = logging.getLogger('TheLogger')
Logger.setLevel(Level)
hLogger = logging.handlers.RotatingFileHandler(
LogFileName, maxBytes = Size, backupCount = Count)
Logger.addHandler(hLogger)
GUI in Python_primer_bvn.odm
Page 15 of 16
27. Aug. 2010
A Primer by Bruce vanNorman (2010)
How to begin building GUIs with Python
def LogException(Msg):
global TheLock
TheLock.acquire()
Log(Msg)
# --- clever code copied from web ----------------- BvN 29.Jun.09 Cla, Exc, Trbk = sys.exc_info()
ExcTb = []
ExcName = Cla.__name__
try:
ExcArgs = Exc.__dict__["args"]
except KeyError:
ExcArgs = "<no args>"
ExcTb = traceback.format_tb(Trbk, 5)
Log('Error: ' + ExcName)
Log('Arguments: ' + str(ExcArgs))
Log('At:')
for Line in ExcTb:
First, Trash, Last = Line.partition('\n')
if len(First): Log(' %s'%First.strip())
if len(Last): Log('
%s'%Last.strip())
for I in range(3):
Log(' %s'%sys.exc_info()[I])
# --- end of clever code -----------------------------------------TheLock.release()
def LogDebug(Msg):
global logger, TheLock
TheLock.acquire()
Logger.debug('DBG: ' + Msg)
TheLock.release()
def LogError(Msg):
global logger, TheLock
TheLock.acquire()
Logger.error('ERR: ' + Msg)
TheLock.release()
def LogWarn(Msg):
global logger, MsgQ, TheLock
TheLock.acquire()
Logger.warn('WRN: ' + Msg)
TheLock.release()
def LogInfo(Msg):
global logger, TheLock
TheLock.acquire()
Logger.info('INF: ' + Msg)
TheLock.release()
def Log(Msg):
global logger
Logger.critical('***: ' + Msg)
pass
GUI in Python_primer_bvn.odm
Page 16 of 16
27. Aug. 2010