
An example of using WxPython to create a node interface. Part 1: Learning to Draw
- Tutorial
In a short series of articles we will describe the use of WxPython to solve a very specific task for developing a user interface, and how to make this solution universal. This tutorial is intended for those who have already begun to study this library and want to see something more complex and holistic than the simplest examples (although it starts with relatively simple things).
And it all started like this: I needed to make a UI for one project, where I need to edit the message processing sequence. Something like Simulink. Accordingly, it was useful to look for ready-made libs / frameworks. At first I thought that the task was popular and someone had already done this bike, searched, searched and ... didn’t find it. More precisely, I found a lot of antique bicycles, but who will use someone else's old bicycle, if you can make your own new one. But since making a new bicycle, why not make it universal, you never know where it will come in handy.
So I’ll try in a few articles to describe the development process from scratch to a working example. Well, to make it interesting, and the farmwork was universal, the first task for him would be not like Simulink, but software for drawing flowcharts a la Visio, but with its blackjack and other participants :)
Part 1: Learning to draw
Part 2: Handling mouse events
Part 3: Continuing to add features + keyboard processing
Part 4: Implementing Drag & Drop
Part 5: Connecting nodes
Who cares, welcome to the ...
A little comment: I will use English names in the code and often use transliteration, i.e. instead of writing “canvas” or “canvas”, I will write “canvas”. Yes, I know that this may look bad and not right, but there is no ideal way to mix Russian and English, all methods have their drawbacks (my personal opinion).
Let's start with simple commonplace things. Since this is a framework, it means that someone will make applications based on it or parts of applications that will make them. Those. for the simplest test, we need to make the simplest application. Without thinking too long, I decided that I would draw the rectangles connected to each other and started with this code (it will live in the “ConnectedBoxes.py” file):
Everything here is quite trivial and obvious, except for a couple of points: MoveMe is the name of our framework, and Canvas is the main class of our framework that is responsible for rendering the whole thing.
Actually with canvas, our framework begins. He is responsible for storing objects (we will call them nodes), their rendering and processing user interaction. Accordingly, we start with a simple drawing.
Here everything becomes a little more interesting:
iPhones of rounded rectangles in the window, which can be scrolled.

We figured out the simplest drawing, it's time to somehow streamline this process and introduce the concept of an object on the stage. All visible objects will be stored in a list, which will determine the order of objects. To support this, we will add the "_canvasObjects" field to the canvas class and slightly change the rendering process, i.e. instead of drawing directly, we will call the Render method of all the objects in the scene. Now the code looks like this:
By the way, the list of scene objects immediately contains several objects of the SimpleBoxNode class, which for now will simply draw rectangles with the coordinates of the objects.
Everything seems pretty trivial here. Unless it is necessary to specify the font, since the "GraphicsContext" has no default settings (we will return to this fact and its correction). At the moment, our code draws this picture:

No miracles, but enough for a start. This ends the first part, and in the next part we add the processing of mouse and keyboard events and enable the user to move, connect, and delete our rectangles.
To be continued ...
PS1: The code can be found on GitHub;
PS2: Write about typos in PM.
And it all started like this: I needed to make a UI for one project, where I need to edit the message processing sequence. Something like Simulink. Accordingly, it was useful to look for ready-made libs / frameworks. At first I thought that the task was popular and someone had already done this bike, searched, searched and ... didn’t find it. More precisely, I found a lot of antique bicycles, but who will use someone else's old bicycle, if you can make your own new one. But since making a new bicycle, why not make it universal, you never know where it will come in handy.
So I’ll try in a few articles to describe the development process from scratch to a working example. Well, to make it interesting, and the farmwork was universal, the first task for him would be not like Simulink, but software for drawing flowcharts a la Visio, but with its blackjack and other participants :)
Part 1: Learning to draw
Part 2: Handling mouse events
Part 3: Continuing to add features + keyboard processing
Part 4: Implementing Drag & Drop
Part 5: Connecting nodes
Who cares, welcome to the ...
A little comment: I will use English names in the code and often use transliteration, i.e. instead of writing “canvas” or “canvas”, I will write “canvas”. Yes, I know that this may look bad and not right, but there is no ideal way to mix Russian and English, all methods have their drawbacks (my personal opinion).
1. The first test
Let's start with simple commonplace things. Since this is a framework, it means that someone will make applications based on it or parts of applications that will make them. Those. for the simplest test, we need to make the simplest application. Without thinking too long, I decided that I would draw the rectangles connected to each other and started with this code (it will live in the “ConnectedBoxes.py” file):
import wx
from MoveMe.Canvas.Canvas import Canvas
class CanvasWindow(wx.Frame):
def __init__(self, *args, **kw):
wx.Frame.__init__(self, *args, **kw)
s = wx.BoxSizer(wx.VERTICAL)
s.Add(Canvas(self), 1, wx.EXPAND)
self.SetSizer(s)
if __name__ == '__main__':
app = wx.PySimpleApp()
CanvasWindow(None).Show()
app.MainLoop()
Everything here is quite trivial and obvious, except for a couple of points: MoveMe is the name of our framework, and Canvas is the main class of our framework that is responsible for rendering the whole thing.
2. Learning to draw
Actually with canvas, our framework begins. He is responsible for storing objects (we will call them nodes), their rendering and processing user interaction. Accordingly, we start with a simple drawing.
import wx
class Canvas(wx.PyScrolledWindow):
"""
Canvas stores and renders all nodes and node connections.
It also handles all user interaction.
"""
def __init__(self, *args, **kw):
super(Canvas, self).__init__(*args, **kw)
self.scrollStep = kw.get("scrollStep", 10)
self.canvasDimensions = kw.get("canvasDimensions", [800, 800])
self.SetScrollbars(self.scrollStep,
self.scrollStep,
self.canvasDimensions[0]/self.scrollStep,
self.canvasDimensions[1]/self.scrollStep)
self._dcBuffer = wx.EmptyBitmap(*self.canvasDimensions)
self.Render()
self.Bind(wx.EVT_PAINT,
lambda evt: wx.BufferedPaintDC(self, self._dcBuffer, wx.BUFFER_VIRTUAL_AREA)
)
def Render(self):
"""Render all nodes and their connection in depth order."""
cdc = wx.ClientDC(self)
self.PrepareDC(cdc)
dc = wx.BufferedDC(cdc, self._dcBuffer)
dc.Clear()
gc = wx.GraphicsContext.Create(dc)
gc.SetPen(wx.Pen('#000000', 2, wx.SOLID))
gc.DrawRoundedRectangle(12, 34, 56, 78, 10)
gc.DrawRoundedRectangle(112, 134, 156, 178, 10)
Here everything becomes a little more interesting:
- First, we inherit “wx.PyScrolledWindow” so that our window can be scrolled and set the scroll parameters in “self.SetScrollbars”.
- Secondly, we will not draw directly, but into the buffer, so that all this happens faster and without flicker. To do this, use “wx.BufferedDC” and a buffer, which is a bitmap.
- And thirdly, we will use “wx.GraphicsContext” for convenient saving of state. It has the “PushState” and “PopState” methods, which save the settings for brushes, fonts, etc. etc., which is especially useful, since the user code will draw blocks on the screen and no one will guarantee that the user will return everything to its place.

3. Arranging the scene
We figured out the simplest drawing, it's time to somehow streamline this process and introduce the concept of an object on the stage. All visible objects will be stored in a list, which will determine the order of objects. To support this, we will add the "_canvasObjects" field to the canvas class and slightly change the rendering process, i.e. instead of drawing directly, we will call the Render method of all the objects in the scene. Now the code looks like this:
import wx
from MoveMe.Canvas.Objects.SimpleBoxNode import SimpleBoxNode
class Canvas(wx.PyScrolledWindow):
"""
Canvas stores and renders all nodes and node connections.
It also handles all user interaction.
"""
def __init__(self, *args, **kw):
super(Canvas, self).__init__(*args, **kw)
self.scrollStep = kw.get("scrollStep", 10)
self.canvasDimensions = kw.get("canvasDimensions", [800, 800])
self.SetScrollbars(self.scrollStep,
self.scrollStep,
self.canvasDimensions[0]/self.scrollStep,
self.canvasDimensions[1]/self.scrollStep)
self._canvasObjects = [SimpleBoxNode([20,20]), SimpleBoxNode([140,40]), SimpleBoxNode([60,120])]
self._dcBuffer = wx.EmptyBitmap(*self.canvasDimensions)
self.Render()
self.Bind(wx.EVT_PAINT,
lambda evt: wx.BufferedPaintDC(self, self._dcBuffer, wx.BUFFER_VIRTUAL_AREA)
)
def Render(self):
"""Render all nodes and their connection in depth order."""
cdc = wx.ClientDC(self)
self.PrepareDC(cdc)
dc = wx.BufferedDC(cdc, self._dcBuffer)
dc.Clear()
gc = wx.GraphicsContext.Create(dc)
for obj in self._canvasObjects:
gc.PushState()
obj.Render(gc)
gc.PopState()
By the way, the list of scene objects immediately contains several objects of the SimpleBoxNode class, which for now will simply draw rectangles with the coordinates of the objects.
import wx
class SimpleBoxNode(object):
"""
SimpleBoxNode class represents a simplest possible canvas object
that is basically a rectangular box.
"""
def __init__(self, pos):
self.position = pos
self.boundingBoxDimensions = [90, 60]
def Render(self, gc):
gc.SetPen(wx.Pen('#000000', 2, wx.SOLID))
gc.DrawRoundedRectangle(self.position[0],
self.position[1],
self.boundingBoxDimensions[0],
self.boundingBoxDimensions[1], 10)
gc.SetFont(wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT))
gc.DrawText("(%d, %d)"%(self.position[0], self.position[1]), self.position[0]+10, self.position[1]+10)
Everything seems pretty trivial here. Unless it is necessary to specify the font, since the "GraphicsContext" has no default settings (we will return to this fact and its correction). At the moment, our code draws this picture:

No miracles, but enough for a start. This ends the first part, and in the next part we add the processing of mouse and keyboard events and enable the user to move, connect, and delete our rectangles.
To be continued ...
PS1: The code can be found on GitHub;
PS2: Write about typos in PM.