As promised, here is a second installment of From the Couch, covering the creation of a simple drawing program based on CanvasControl (included in the latest release). An interactive drawing program is more complicated than a simple CanvasControl, so this installment, Part 1, only covers initially drawing items on the canvas. Part 2 will cover selection, repositioning, resizing, and deleting drawing items.
Here’s a preliminary version of DrawControl:
Controller { Title: "SuneiDraw" Controls: (Vert (ChooseList (Line, Rectangle, Ellipse)) Canvas ) }
I made DrawControl a Controller since this is the simplest way to contain/control other controls. For now, the easiest thing seemed to be to use a ChooseList to select what kind of item to draw. A palette would be nicer, but would have required a lot more work so it’ll have to wait. So the layout, defined by Controls, consists simply of a ChooseList, with a DrawCanvas below it. Just to be cute, I defined the Title as “SuneiDraw”. If you run this, you’ll see something like:
The next step is that we need to respond to mouse actions. The easiest way to do this is to derive a control, based on CanvasControl, that handles WM_LBUTTONDOWN, WM_MOUSEMOVE, and WM_LBUTTONUP. Here is DrawCanvasControl:
CanvasControl { SetTracker(tracker, item) { .tracker = tracker(.Hwnd, item) } dragging: False LBUTTONDOWN(lParam) { .dragging = True x = LOWORD(lParam) y = HIWORD(lParam) .tracker.MouseDown(x, y) return 0 } MOUSEMOVE(lParam) { if (not .dragging) return 0 x = LOWORD(lParam) y = HIWORD(lParam) .tracker.MouseMove(x, y) return 0 } LBUTTONUP() { .dragging = False if (False isnt item = .tracker.MouseUp()) .AddItem(item) return 0 } }
DrawCanvasControl doesn’t do a lot itself. It receives LBUTTONDOWN, MOUSEMOVE, and LBUTTONUP, extracts x and y from lParam, and then passes them on to the current “tracker”, which is set with SetTracker. If the tracker MouseUp returns a canvas item we add it to the canvas. The “dragging” flag is used to ignore MOUSEMOVE’s when the left button isn’t down. As usual, we have to remember to return 0 (zero) from Window message methods.
Now we can flesh out DrawControl:
Controller { Title: "SuneiDraw" New() { .Vert.ChooseList.Set('Line') .canvas = .Vert.Canvas .NewValue('Line') } Controls: (Vert (ChooseList (Line, Rectangle, Ellipse)) DrawCanvas ) NewValue(value) // sent by ChooseList { switch (value) { case 'Line' : .canvas.SetTracker(DrawLineTracker, CanvasLine) case 'Rectangle' : .canvas.SetTracker(DrawRectTracker, CanvasRectangle) case 'Ellipse' : .canvas.SetTracker(DrawRectTracker, CanvasEllipse) } } }
In the Controls, Canvas has been replaced by DrawCanvas. In New we set ChooseList’s initial value to ‘Line’ and then manually call the NewValue method to set the appropriate tracker. We also obtain a reference to the Canvas (actually a DrawCanvas, but we didn’t override the name). Now if we change the Controls layout, we’ll only have to change the initialization of .canvas, rather than all the places it was used. NewValue is a standard “message” sent by controls that are compatible with RecordControl whenever the user enters a new value. We use it here to change drawing “tools” when the user changes the ChooseList. Note: If we had more controls we’d have to check the “source” of the NewValue message.
All we have left are the trackers themselves:
DrawRectTracker class { New(hwnd, item) { .hwnd = hwnd .item = item } rect: () MouseDown(x, y) { SetCapture(.hwnd) .rect = Object() .x0 = x .y0 = y } MouseMove(x, y) { hdc = GetDC(.hwnd) if (.rect.Size() is 4) DrawFocusRect(hdc, .rect) // erase previous rect .rect.left = Min(.x0, x) .rect.right = Max(.x0, x) .rect.top = Min(.y0, y) .rect.bottom = Max(.y0, y) DrawFocusRect(hdc, .rect) ReleaseDC(.hwnd, hdc) } MouseUp() { ReleaseCapture() item = False if (.rect.Size() is 4) { hdc = GetDC(.hwnd) DrawFocusRect(hdc, .rect) // erase previous rect item = (.item)(.rect.left, .rect.top, .rect.right, .rect.bottom) ReleaseDC(.hwnd, hdc) .rect = #() } return item } }
DrawRectTracker can take advantage of DrawFocusRect to draw the tracking rectangle. Since DrawFocusRect uses XOR (exclusive or) drawing, to erase it we can simply call it again. To achieve the same result in DrawLineTracker we have to call SetROP2 to set XOR drawing mode and then use LineTo. (Note: SetROP2 and the corresponding R2 defines are not defined in the current release of stdlib. They are included in the drawlib download explained below.)
DrawLineTracker class { New(hwnd, item) { .hwnd = hwnd .item = item } x1: False MouseDown(x, y) { SetCapture(.hwnd) .x0 = x .y0 = y } MouseMove(x, y) { hdc = GetDC(.hwnd) if (.x1 isnt False) .line(hdc) // erase previous line .x1 = x .y1 = y .line(hdc) ReleaseDC(.hwnd, hdc) } MouseUp() { ReleaseCapture() item = False if (.x1 isnt False) { hdc = GetDC(.hwnd) .line(hdc) // erase previous line item = (.item)(.x0, .y0, .x1, .y1) ReleaseDC(.hwnd, hdc) .x1 = False } return item } line(hdc) { oldrop = SetROP2(hdc, R2.NOTXORPEN) MoveTo(hdc, .x0, .y0) LineTo(hdc, .x1, .y1) SetROP2(hdc, oldrop) } }
Notice the line of code in MouseUp to create the drawing item:
item = (.item)(.x0, .y0, .x1, .y1)
If we had written:
item = .item(.x0, .y0, .x1, .y1) // INCORRECT!
it wouldn’t have worked because Suneido would have looked for an “item” method in the current class, which it wouldn’t have found. By writing (.item) we are saying we want to call the value of the .item member, rather than the .item method.
Now we have all the code we need to run DrawControl and produce masterpieces like:
If you want to try this code, you can download drawlib.zip, extract drawlib.su, load it from the command line with:
suneido -load drawlib
and then Use the library from LibView.
Here are a few ideas for enhancements. I’m sure you can think of additional ones:
- add support for saving and loading drawings (Hint: add a way to access CanvasControls item list, then add “save” and “load” methods to the canvas items.)
- add additional drawing “tools”, either with the existing line and rectangle trackers, or with new trackers e.g. arc or polygon
- add support for setting the pen (outline) and brush (fill) colors
- “constrain” drawing when the SHIFT key is held down, e.g. ellipses to circles, rectangles to squares, lines to horizontal and vertical
- add a “palette” for selecting drawing “tools” instead of the ChooseList
If you do work on any changes or improvements, be sure to share them with us!
In Part 2 I’ll look at adding support for selecting and deleting items.
Andrew McKinlay
Suneido Software