wxLED matrix editor
19 May 2019Since a significant portion of my electronics projects have been dot-matrix LED displays, an item on my to-do was to write a desktop application to create the display data-sets. Control was an after-thought with the original LED matrix display, and the third-generation matrix tiles really deserved something more than hand-crafted test sequences. It is also a pivot back to a software focus, after having spent a significant amount on PCBs and components — and an opportunity to get back up to speed with wxWidgets, which I had practically forgotten how to use having last seriously used it around five years ago
The code is available from my Bitbucket account — the repository was originally a private development one which I used since I was often switching between development on my laptop and desktop, hence why a lot of the commits are ones that would normally be kept on a private branch and squashed before being pushed to the main branch.
Design outline
My original idea was to support displays of arbitrary dimensions with the LED units being tiled vertically as well as horizontally, but in trying to figure out a work-flow the rough design was starting to look like a reimplementation of GIMP, in particular the latter's layer system. I had the general idea of a scratch-space larger than what the LED panels would actually show, as illustrated below, but had no idea how to easily script all the display components. This level of flexibility would be better served using a tool that took an animated GIF as input, and then perhaps allowing some basic adjustments to the final display data, rather than trying to implement my own half-baked image editor. Such a mistake affected IconSharp, which in the end never actually achieved its original goal, in part due to getting bogged-down by shortcomings with GTK#.
To make the problem tractable I ended up targeting the basic use-case of fixed-height horizontally scrolling sequences of symbols, with the flexibility coming from making it easy to add extra glyphs in addition to the typeface symbols. There would also be some flexibility in the changeover between sequences, mainly how the start of the sequence enters and the end exits, but the overriding priority would be expedience.
Back to wxPython
I actually started wxLedMatrix shortly before I started wxCatalogue, but the latter for various reasons I decided to prioritise and it was only about six months later that wxLedMatrix once again got my proper attention. The most notable difference between the two, aside from the differing purposes, is that wxCatalogue constructed the GUI entirely in Python code, whereas wxLedMatrix's GUI layout (covered below) is done in XRC files built using wxFormBuilder. In both cases coming back to wxPython after a long absence was not smooth, and the sub-sections below highlight particular pain points.Layout control
The “proper” way to handle window resizing is to use sizers, which in all honesty are a lot harder to get working properly than they should be — in particular it seems that some parameters such as minimum size are not actually honoured. However what is really asking for trouble is mixing sizers and any sort of scrollable viewing area if you want a change in one dimension to cause a change in the other. OriginallyGlyphEdit.GlyphList
, the panel that provides icon representations of glyphs, was implemented using wx.GridSizer
with each glyph icon being its own wx.Panel
. The idea was to change the number of columns based on the top-level window width, and then the containing wx.ScrolledWindow
to expand its vertical virtual area to accommodate all the columns. In the end I did all the layout manually in a single drawing callback.
Unfortunately in the notionally simpler case of the sequence list, there were also problems using sizers — each sequence entry would take its width from the parent sequence list, but for the height it would be the other way round with the list being the summed height of the entries. Making the sequence list scrolled as well resulted in something that never worked properly, and I ended up throwing the whole lot away and reimplementing it as a self-contained class with no child objects. I probably would not have taken the first approach had I not forgotten the pit-falls I faced previously using wx.GridSizer
for the glyph list.
XRC pit-falls
Getting up-and-running with with XRC-built GUIs is significantly harder than it needs to be, the trickiest thing being working out how to get it working with derived classes, but once you get through the processing of banging your head against the keyboard an work it out it is quite nice to work with. The first thing is correctly specifying the sub-class name — if it isGlyphList
within Glyph.py
then you should insert Glyph.GlyphList
into the wxFormBuilder classname field. This is a little unintuitive if you are not otherwise splitting a program into modules/files. The other tricky bit is that two-stage object creation has to be used, because the super-class part of the object is created by XRC, and the sub-class customisations done later. In practice it means, for instance, that a wx.Panel
sub-class is declared as follows:
class FontLED(wx.Panel): def __init__(self): pre = wx.PrePanel() self.PostCreate(pre) self.Bind(wx.EVT_WINDOW_CREATE, self.OnCreate)
The self.PrePanel()
and self.PostPanel()
calls are specific to wx.Panel
, and for something else such as wx.TreeCtrl
they are self.PreTreeCtrl
and self.PostTreeCtrl()
— this approach is one of the things that wxPython Phoenix does away with. A nasty pit-fall is that OnCreate seems to get deferred until the sub-class is actually shown, so with dialogs some setup code that really ought to be in the creator has to instead be grafted into OnShow. I chose for logistical reasons to have complete dialogs in XRC, which was more painful than it should have been to get working.
Dialog handling
On the whole the way dialogs interface with the rest of the program could be a lot better. Internally they work well but passing in and out of data is basically a hack that exploits Python's ability to add arbitrary class variables on the fly, which is done because I am unaware of an alternative way of passing data. There is also the headache with the dialogs not being idempotent so program logic is needed to work out whether it is the first time a dialog has been used, in which case one-time initialsiation code needs to be run, or whether it is a subsequent invocation in which case internal state needs to be scrubbed down. Not a problem once you know the pit-falls, but a major source of bugs when trying to figure things out.Bubbling up events
As far as I can tell if you want events such as mouse clicks to be processed by a parent widget rather than the widget that directly received it, the only sure-fire way of doing it is to include a small event handler — such as the one shown below — in the child that passes on the event. Apparentlywx.CommandEvent
will automatically bubble up , although it is unclear why this is a special rather than a general case. I would have liked it to be either bubble-by-default, or at least be controlled by a style flag.
def evtRightClick(self, event): self.GetParent().evtRightClick(event)
In many cases a parent or grandparent control is where actual handling occurs, and having to manually include code to pass information one of the things that contributes to the next problem: inter-object communications.
Cross-object communications
While object-orientation works well for constructing user interfaces, there is then headache of then getting program data and events to the right places, which if good software engineering practices are followed means quite often traversing multiple hopes between parent and child elements. These problems are not specific to wxPython and Python's dynamic data system helps a lot in coping with data & event propagation, but they are one of the main reasons I have historically not been fond of writing user interfaces. The code fragment below shows the type of chained reference-following required, at least before some of the GUI hierarchies were flattened.def __init__(self, parent): wx.glcanvas.GLCanvas.__init__( /* ... */ ) self.app = parent.GetParent().GetParent().app
Using wxFormBuilder & XRC
Getting started with XRC is a major pain but with that aside the trade-off compared to programmatic GUI creation is relative ease of laying things out versus headaches wiring things together for program control. On the whole I have found that once over the up-front overheads of working things out the balance is in favour of using XRC, although at times it is tempting to get wxFormBuilder to generate the program code and simply copy that into the code-base. However while XRC is great for static layouts such as the Sequence Properties panel and the basic dialog layouts, it does not seem suited for situations where GUI fragments are created and destroyed on the fly. I think multiple instances of XRC-defined fragments can be used, but I have yet to try it out.
A significant disadvantage with wxFormBuilder is that it only includes a sub-set of wxWidgets/wxPython components, and one such component I intended to use was what I later found out to be wx.StaticBox
which is a panel that includes a border and a title. The choice is between either adding hacks to the application code to use them once the XRC resources are loaded, which may well involve having to split a single XRC hierarchy into multiple smaller ones that are then assembled by the application, or to try and obtain a similar effect using alternative available components. To me this is choosing the lesser of two evils — the latter means an interface differing from what the program is supposed to look like, and the former involves work split between multiple places. Ultimately I chose the former because wx.StaticBox
also has a bug — size calculation does not account for the space needed for the frame, resulting in child components getting clipped.
Although wxFormBuilder is also capable of generating program code instead of XRC resource markup — it offers C++, Python, PHP, and Lua — I have found that in cases where I did copy the generated code I would at the very least heavily refactor the code, and often as not this would border on a complete rewrite. For me a major push factor in having as much of the GUI as possible as XRC resources rather than program code is the prospect of implementing the same application in both Python and C++ — the XRC markup would be unchanged but any programmatic GUI would have to be rewritten.
Technical changes mid-project
When I first started on the project last year I got as far as getting the two dialogs — the glyph editor and the sequence editor — close to fully operational, but when I came back to the project after six months I had very different ideas as to how the application should be implemented. This was partly down to forgetting how the existing code-base was laid out, and hence I found it easier to rewrite things than retrofit changes such as how glyph data was internally structured.This entailed two major enviornmental changes for the project — a switch to a newer version of wxPython, and use of OpenGL.Switching over to wxPython Phoenix
I made the switch to wxPython Phoenix for the simple expedient that, unlike Classic wxPython, it is not an utter pain to use with virtualenv. These days if I need to install any extra Python modules I create a virtualenv for the project, the latter providing a bare-bones environment and assumes that everything else is pulled in via pip as needed — and Phoenix practically mandates installation via pip. In this case I needed to pull inpyopengl
in order to get OpenGL support, so I decided to bite the bullet and switch over to Phoenix. The one headache of switching over is that installing via pip — at least on Slackware — requires recompiling, which in turn also involves installing webkitgtk3
which had to be done via SlackBuilds. These together took several hours, although when it came to installing on my laptop as well as my desktop I could at least pull across the binary for the latter.
wx.PaintDC vs. OpenGL
Originally drawing of the LED displays and preview was done only using wxPython drawing routines, but coming back to the code after a few months away I felt that OpenGL's transformation matrices would do a cleaner job of drawing the same shape repeatedly than the drawing code I had written previously. I have no idea if there is any hardware acceleration of whatever drawing routines are used behind the scenes by wxPython, but I do know that OpenGL hardware acceleration is a standard thing on modern systems. There is also the notional advantage that OpenGL-based drawing routines would not be tied to wxPython, but I never got serious with any prospective switch to a different GUI toolkit. While the OpenGL drawing code is indeed a lot neater, I felt that gotchas related to setting up OpenGL viewports made it unsuitable for using in lots of small cases, so when I reimplemented the sequence list I actually went back to using the wxPython drawing functions. As a result the only part that used OpenGL in the end was the big LED display on the main window, and the only reason I did not rewrite that to remove OpenGL entirely was a desire to get the whole program finished.OpenGL is something I have used a lot in the past, but one thing I rapidly realised with this software project was how much of the OpenGL API I had forgotten, even if I still remembered the capabilities I wanted to use, so I ended up looking at various code samples to get back up to speed. In the past I even wrote a few pixel shaders to do colour-space conversions, but these days I would not even know where to start to do such a thing. All this makes me wonder whether OpenGL is one of those things I ought to drop from my CV on the basis of how rusty I am with it.
Potential switch back to Classic wxPython
Wanting to use OpenGL was the root cause of a shift to wxPython Phoenix in the first place, and with the prospect of dropping OpenGL I did think about whether I would want to revert to wxPython Classic. From what I remember there were not that many API changes between the two, but most of the changes were related to making custom classes available in wxFormBuilder, and I decided I did not want to go back to what in comparison look like nasty hacks.I soon decided that if I was to make another switch it would be to wxPython and C++ since much of the arguments for going back to wxPython Classic are somewhat undermined by the program being written in Python v2, the latter which is due to be end-of-life next year.Remarks
In terms of goals I feel this software project was a success, namely having written a program to edit LED display datasets and in the process gained experience using wxPython, but the stop-start nature of the project meant that development was far from smooth. Program GUI design followed wxPython idiosyncrasies rather than the other way round that bit more often than I found comfortable, and twice I ended up ditching a nice hierarchy of widgets for a manually-handled monolithic widget. Aside from two major periods of development — a few weeks back in October and an intense week in April — it has had to compete with more pressing calls on my time, but somehow I scraped together motivation to bring it to completion despite setbacks:-- When not to make code common
- While it was well-meaning to re-use classes from the glyph editing within the sequence dialog, the result was contrived interfaces that I had forgotten the thinking behind coming back to the code after six months. For prototype code such as this I would have been better off keeping the two dialogs completely separate, an then factor out the common code into common class that both then import.
- General dislike of GUIs
- GUI programming is something I historically felt was not really my thing. The interfaces themselves are increasingly meta-data rather than program code, and writing the actual application to wire everything together ends up being very messy — and this is before considering that in my experience GUI APIs require a higher proportion of work-arounds and than other programming interfaces. Nice to do once in a while but not as a career.
- The object-orientated mindset
- Encapsulation works well for widgets themselves, but the object-orientated mindset that encourages hierarchies of widgets rather than something flatter causes trouble down the road. Quite often it is better to do things manually rather than the “proper” way.