Last time we were on the couch, we added selection and deletion of drawing items. This time we will add the ability to select all the items using CTRL + A, to resize a selected item, to group and ungroup items, and to save and load drawings. We will also add a palette to the DrawControl.
First of all, for the palette, we will be using the ImageButtonControl. Buttons for each shape and function will be added. The code for a palette will look like:
PassthruController { Name: Palette Controls: (Vert #(ImageButton Select 'select.bmp', md_image: 'selectPushed.bmp', name: select) #(ImageButton RoundRectangle 'roundrect.bmp', md_image: 'roundrectPushed.bmp', name: roundrect) ... ) }
We are using PassthruController instead of Controller so that the messages from the buttons will be passed through to the parent (DrawControl).
We will also add a SetButtons() method used to press or release the buttons. This method will cycle through all the buttons on the palette and release all the buttons except the one specified. This is so we can show which drawing tool is currently selected. This method will look something like:
SetButtons(buttonPushed) { buttons = .Vert.GetChildren() foreach (value button in buttons) { button.Name is buttonPushed ? button.Pushed?(True) : button.Pushed?(False) button.Repaint() } }
We now have to add our palette to DrawControl. The controls method will now look like this:
controls() { return #(Horz (DrawPalette) (DrawCanvas)) }
Here is what it should look like:
We also have to add the methods needed to handle pressing the buttons on the palette. For this example, we will add the On_Select and On_Rectangle methods. These methods have to set the trackers, if needed, and call the SetButtons() method we implemented in the DrawPaletteControl so the button will appear depressed. They will look like this:
On_Select() { .palette.SetButtons('select') .canvas.SetTracker(DrawSelectTracker, .canvas) } On_Rectangle() { .palette.SetButtons('rectangle') .canvas.SetTracker(DrawRectTracker, CanvasRectangle) }
We will now add the resize functionality. A button will have to be added to the DrawPaletteControl and a function added to the DrawControl. In this function, we will get the selected item from the canvas and call a ResetSize() method on that item. The canvas will then be repainted and the focus set back to the DrawControl. This function will look like this:
On_Resize() { .palette.SetButtons(False) item = Object() item = .canvas.GetSelected() if (item.Size() isnt 1) { Alert('You must select one item to resize.') return } item[0].ResetSize() .canvas.Repaint() SetFocus(.canvas.Hwnd) }
The ResetSize() method must be implemented for each canvas item. The ResetSize() method for the CanvasRectangle will call the ResetSizeControl ( which we will have to implement after ), passing in the current coordinates and then set the points to the new coordinates. This method will look like this:
ResetSize() { result = Dialog(0, Object('ResetSize' Object(x1: .x1, y1: .y1, x2: .x2, y2: .y2)) title: 'Please enter the new coordinates') if (result is False) return .x1 = Number(result.x1) .y1 = Number(result.y1) .x2 = Number(result.x2) .y2 = Number(result.y2) }
The ResetSizeControl will display a field for each point passed in from the item. The prompts will be the member names. When the user clicks okay, the new values will be passed back to the item. This control will look like this:
Controller { New(fields) { super(.controls(fields)) .data = .Data } controls(fields) { ctrl = Object('Vert') foreach(field in fields) ctrl.Add(Object('Pair' Object('Static' field) Object('Field' name: field set: fields[field]))) ctrl.Add(#(OkCancel)) return Object('Record', ctrl) } On_OK() { .Window.Result(.data.Get()) } On_Cancel() { .Window.Result(False) } }
For example, if we resize a rectangle:
Now we will implement the select all using CTRL + A. The code necessary for this will be in the DrawCanvasControl. First we will catch the keyboard message. We will then select all the items using a SelectAll() method that will be implemented in the CanvasControl, then set the tracker to the DrawSelectTracker and then set the focus back to the DrawCanvasControl. The code to catch the keyboard message will look like this:
KEYDOWN(wParam, lParam) { ctrl = (GetKeyState(VK.CONTROL) & 0x80) is 0x80 if (ctrl and wParam is VK.A) { .SelectAll() .tracker = DrawSelectTracker(.Hwnd, this) SetFocus(.Hwnd) } return 0 }
We now need to add the SelectAll() method to the CanvasControl. This method will simply add all the items to the selected object and repaint the canvas. The code will look like this:
SelectAll() { .ClearSelect() foreach (value item in .items) .selected.Add(item) .Repaint() }
Now we will add group and ungroup. First we will need to make a new canvas item called CanvasGroup. The New() method for this item will take an object containing all the items to be grouped together. The Paint() method will loop through all the items and call their paint methods. The BoundingRect() method will call the BoundingRect() method for each item and return the minimum and maximum x and y coordinates. We will also need a GetItems() method for ungrouping. The resulting CanvasGroup code will look like:
CanvasItem { New(items) { .items = items } Paint(hdc) { foreach (value item in .items) item.Paint(hdc) } BoundingRect() { r = .items[0].BoundingRect() foreach (value item in .items) { br = item.BoundingRect() r.x1 = Min(r.x1, br.x1) r.x2 = Max(r.x2, br.x2) r.y1 = Min(r.y1, br.y1) r.y2 = Max(r.y2, br.y2) } return r } GetItems() { return .items } }
We will also need to add a button to the DrawPaletteControl and a function to DrawControl. The function will get all the selected items, remove them from the CanvasControl list of items, and then use them to create a single CanvasGroup item. The CanvasGroup item will then be added back to the CanvasControl list. This function will look like:
On_Group() { .palette.SetButtons(False) items = .canvas.GetSelected() if (items.Size() < 1) { Alert('You must select at least one item to group.') return } group = Object() foreach (value item in items.Copy()) { group.Add(item) .canvas.RemoveItem(item) } newitem = CanvasGroup(group) .canvas.AddItemAndSelect(newitem) }
The ungroup function will also need a button and a function. This function will simply do the opposite of the group function. Ungroup will retrieve all the items that are in the group from the CanvasGroup item, remove the group item from the CanvasControl list and add each item that was in the group back to the CanvasControl list.This function will look like:
On_Ungroup() { .palette.SetButtons(False) group = .canvas.GetSelected() if (group.Size() isnt 1) { Alert('You must select one item to ungroup.') return } foreach (value item in group[0].GetItems()) .canvas.AddItemAndSelect(item) .canvas.RemoveItem(group[0]) }
So now we have an image we would like to save. First of all, we will have to add a save button to the DrawPaletteControl. A save function will also have to be added to the DrawControl. This function will go through all the items on the canvas and write them out to a file using a ToString() method that will have to be implemented in all the canvas items. The save function will look like:
On_Save() { .palette.SetButtons(False) file = SaveFileName( filter: "Suneido Draw Files (*.sdr)\000*.sdr\000All Files (*.*)\000*.*\000", ext: '.sdr') if (file is '') return File(file, 'w') { |f| foreach (value item in .canvas.GetAllItems()) f.Writeline(item.ToString()) } }
We are using a default extension of sdr so that we can identify the files.
The ToString() method will return a string that can be evaluated to create a canvas item identical to the original one. The ToString() method for CanvasRectangle will look like:
ToString() { 'CanvasRectangle(x1: ' $ Display(.x1) $ ', y1: ' $ Display(.y1) $ ', x2: ' $ Display(.x2) $ ', y2: ' $ Display(.y2) $')' }
There is not a lot of point in being able to save your images if you can’t load them back in. Once again we have to add a button to the DrawPaletteControl and a function to the DrawControl. The function will prompt the user for a file name, then open that file, evaluate all the lines in the file, and add the items to the canvas. The open function will look like:
On_Open() { .palette.SetButtons(False) file = OpenFileName( filter: "Suneido Draw Files (*.sdr)\000*.sdr\000All Files (*.*)\000*.*\000") if (file is '') return .canvas.DeleteAll() File(file, 'r') { |f| while (False isnt (line = f.Readline())) .canvas.AddItem(line.Eval()) } .canvas.Repaint() }
If you want to try this code, you can download drawlib3.zip, extract drawlib.su, load it from the command line with:
suneido -load drawlib
You also have to load the supplied imagebook:
suneido -load imagebook
Use the library from LibView, and then run DrawControl().
Here are a few ideas for enhancements. I am sure you can think of additional ones:
Add a menu
CTRL + G for grouping and CTRL + U for ungrouping
Tooltips for the palette image buttons
Implement the printing.
Add a scroll bar to the canvas.
It has been a pleasure.
Jennie Hill