Here is the first in a new series of articles called “From the Couch”. As far as I’m concerned, the best part of a laptop is not that you can travel out of town with it, but that you can travel to the couch with it. I wouldn’t recommend doing any really serious programming while you watch TV, but with the quality of shows and the quantity of commercials these days, I seem to be able to follow a show and poke around on the laptop at the same time fairly easily. Of course, I’d probably get more done if I turned off the TV, but then I wouldn’t be “relaxing” 🙂 Not to mention, the heat output from my laptop keeps me warm on those chilly winter evenings. The only annoying thing is that my battery no longer lasts an evening on the couch so I have to trail a power cord.
Anyway, back to the purpose of this first installment of “From the Couch”. I’ve had several requests for a “Canvas” control to support drawing graphics in Suneido. I haven’t got around to it because I haven’t run into any pressing need for it. But it seemed like a suitably fun task for a Friday night (if you’re a programming junkie like me).
I’ll present a slightly simplified version here and incorporate the complete code in stdlib in the next release. I’ll assume a basic understanding of Windows GUI programming. Here is CanvasControl itself:
WndProc { Xstretch: 1 Ystretch: 1 New() { .CreateWindow("SuWhiteArrow", "", WS.VISIBLE) .SubClass() .items = Object() } AddItem(item) { .items.Add(item) .Repaint() } PAINT() { hdc = BeginPaint(.Hwnd, ps = Object()) foreach (value item in .items) item.Paint(hdc) EndPaint(.Hwnd, ps) return 0 } }
Since we want to respond to (implement) Windows messages, we derive the class from WndProc (which in turn derives from Hwnd which wraps a Windows window handle). (Note: WndProc { … } is equivalent to class : WndProc { … } ) I’ve set Xstretch and Ystretch to 1 so the CanvasControl will fill its window and be resizable.
In the New method (the constructor) we create a window using the SuWhiteArrow window class (registered in Init). This class, as the name implies, has a white background and an arrow mouse cursor. We also call SubClass to subclass the window so we can respond to messages like PAINT (WM_PAINT). Finally, we initialize an instance member (.items) to be an Object. This will hold the list of items (lines, shapes) that we draw on the canvas.
My first inclination was to have methods in Canvas for “drawing” different things – for example, Line, Rectangle, Ellipse, and Text methods. But the problem with this is that any time someone wants to add a new type of canvas item they have to modify CanvasControl. And pretty soon, CanvasControl has a zillion methods in it. Instead, I decided it would be more flexible to simply have an AddItem method that would add any kind of “drawable” item to the canvas. The only requirement on these items is that they have a Paint method. This approach makes it easy to create new types of canvas items without bloating CanvasControl.
The AddItem method allows us to add items to the canvas. Note: You can’t call this method “Add” because that’s a built-in method that can’t be redefined. Note: In keeping with Windows GUI programming AddItem does not actually draw the item. Instead it calls Repaint (defined in Hwnd) to “invalidate” the window, causing a paint message to be generated. (A more efficient implementation would only invalidate the area of the added item.) If you add multiple items, the invalidates will be combined so only one paint message is issued.
Finally, the heart of CanvasControl is the PAINT method which is called in response to the WM_PAINT message from Windows. As usual, the paint method is bracketed by BeginPaint and EndPaint. To draw the items, we simply loop through them and call a Paint method on each, passing it the handle to the device context. Obviously, each item has to have a suitable Paint method or this scheme will fail miserably! Lastly, we return 0 to indicate we handled the message.
You can now run this from the WorkSpace with:
CanvasControl()
That’s it for CanvasControl. But it’s pretty boring without anything to draw on it. Here are a few basic canvas items that are simply wrappers around some simple GDI functions. I’ve prefixed all the names with “Canvas” to avoid any name clashes.
CanvasLine class { New(x1, y1, x2, y2) { .x1 = x1 .y1 = y1 .x2 = x2 .y2 = y2 } Paint(hdc) { MoveTo(hdc, .x1, .y1) LineTo(hdc, .x2, .y2) } } CanvasRectangle class { New(left, top, right, bottom) { .left = left .top = top .right = right .bottom = bottom } Paint(hdc) { Rectangle(hdc, .left, .top, .right, .bottom) } } CanvasEllipse class { New(left, top, right, bottom) { .left = left .top = top .right = right .bottom = bottom } Paint(hdc) { Ellipse(hdc, .left, .top, .right, .bottom) } }
Obviously, it’s pretty straightforward to create additional types of items.
We can now exercise CanvasControl a little more. From the WorkSpace try running:
c = CanvasControl().Ctrl c.AddItem(CanvasRectangle(100, 100, 200, 200)) c.AddItem(CanvasEllipse(150, 150, 250, 250)) c.AddItem(CanvasLine(50, 50, 300, 300))
Notice the “.Ctrl” after CanvasControl(). This is needed because calling a control creates a top level window to contain the control and it is this top level window that is returned. To get a reference to our control we need to get the window’s control.
You should see something like this. (But with a different border style if you’re not on XP.)
One thing you may be wondering about is how you would add support for different pens and brushes. You could add pen and brush arguments to all the canvas items but you’d end up duplicating a lot of code. Instead, we can use the Decorator design pattern and make an item that “wraps” another item and sets the pen or brush for it. Since Windows treats pens and brushes very similarly, we can even use the same decorator for both. Here is a decorator that uses stock objects. These are a little simpler to handle because we don’t have to worry about destroying the objects.
CanvasStockObject class { New(stockobject, item) { .ob = GetStockObject(stockobject) .item = item } Paint(hdc) { oldob = SelectObject(hdc, .ob) .item.Paint(hdc) SelectObject(hdc, oldob) } }
So now if we want a black ellipse we can simply wrap a CanvasStockObject around a CanvasEllipse:
c = CanvasControl().Ctrl
c.AddItem(CanvasStockObject(SO.BLACK_BRUSH,
CanvasEllipse(50, 50, 250, 150))
You should see something like:
We can also apply the Composite design pattern to compose canvas items from other canvas items:
CanvasX class { New(x1, y1, x2, y2) { .line1 = CanvasLine(x1, y1, x2, y2) .line2 = CanvasLine(x1, y2, x2, y1) } Paint(hdc) { .line1.Paint(hdc) .line2.Paint(hdc) } } You can try it out with: c = CanvasControl().Ctrl c.AddItem(CanvasX(50, 50, 150, 150))
You should see something like:
That’s as far as I’ll go today, after all there’s a limit to what you can accomplish in one sitting of From the Couch. Here’s a few ideas for extending and using CanvasControl:
- wrap it in a ScrollControl to allow viewing a large canvas in a smaller window
- add printing support (hint: the same Paint method will also work with a printer hdc)
- create wrappers for the rest of the GDI functions e.g. RoundRect
- create a CanvasImage item using the built-in Image class
- create composite canvas items to draw useful symbols e.g. UML classes
- generate UML diagrams from Suneido source code (advanced)
Stay tuned for the next installment of From the Couch that will add user interaction on top of CanvasControl to create a simple drawing application.
PS. If you want to try these examples without copying and pasting, you can download canvaslib.zip, extract canvaslib.su, load it from the command line with:
suneido -load canvaslib
and then Use the library from LibView.
Andrew McKinlay
Suneido Software
PPS. As of the March 13 Release, a slightly enhanced CanvasControl is included in stdlib.