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