GUI Programming: A Paint Program

We've given you an extremely primitive paint program, comprising a few files:

The first three modules---Gctx, Widget, and Eventloop---are our GUI toolkit; main.ml is where we implement our paint program. We've covered the GUI toolkit in class. Your job in this homework will be to extend both the GUI toolkit and the paint program itself. You'll definitely want to use the Ocaml library, which has documentation online. The documentation for the Graphics module will be particularly useful. Feel free to use any function in the standard library.

To use the graphics library, you have to change your project settings in Eclipse. Add graphics.cma (for bytecode) or graphics.cmxa (for native code) to the "Ocaml Build Flags" under the Project > Properties menu item. (You can see whether you're using bytecode or native code in the "Ocaml Project" tab of that same menu item.)

A note on grading

It's notoriously difficult to automatically test user interfaces, especially graphical user interfaces. We'll be testing your homework by hand. You can submit as many times as you like; we'll only look at your last submission.

Our grading will involve both looking at your code and running your GUI to make sure it works. The grade breakdown is as follows:

Since there are so many files in this assignment, we're going to have you submit your homework in a compressed ZIP file. We'll take off points if we can't get your program to compile, so make sure you include every file: both ml files and mli files. (If you need help with this, feel free to ask a TA.)

The GUI

Here's what our finished GUI looks like:

Yours looks a little simpler at first. And you have to draw your own dinosaur. At first, your GUI consists of a number of controls and a drawing canvas, but it's pretty minimal.
You can draw lines with two clicks: the first click sets the start point of the line; the second click sets the end point. The color can be changed by clicking one of the color buttons; the other two buttons, undo and quit, have their usual interpretations.

An overview of main.ml

Before explaining your tasks in this homework to you, we briefly describe how main.ml works. The core of the painting lives in the first section, labelled CANVAS LOGIC.

At first, our paint program can only draw lines. The draw type has one constructor, Line, which takes a color, a start point, and an end point. We record what drawings the user has made in actions, which is of type draw list ref.

We do the actual drawing in the repaint function. Every time the canvas is repainted, we go through the list of actions, drawing each line on the canvas in reverse order. (List.iter is like List.map, except that it doesn't return anything.) We use reverse order because the canvas adds events to the front of the list, so the first drawings are at the back of the list. Since later drawings are "on top of" older drawings, they are "higher in the z-order", where "z" refers to the "z-axis" of a three-dimensional coordinate system.

We handle clicks inside of the canvas using the canvas_action function, which is added as a mouse_listener to the canvas's notifier controller, nc_canvas At first, users draw lines with two clicks, one for each endpoint. To implement this, we define two drawing modes: LineStartMode and LineEndMode. The user of our paint program draws a line with two clicks: the first click sets the start point of the line; the second click sets the end point. We keep track of the first point by sticking it into the LineEndMode constructor; after we get the second click, we can go back to LineStartMode after adding the line to actions. We look at the color variable to see which color to store in the Line value we add to actions.

The rest of the file, marked TOOLBARS, sets up the various buttons and their event handlers. To undo drawings, we simply remove entries from actions; this is implemented in the undo function. To quit, we call the exit function. (The integer argument can be used to indicate that we're exiting with some kind of error---0 means there was no problem.)

The color buttons aren't like normal buttons: we don't want to draw a label, but instead we want to draw a colored box. To accomplish this, we define img_button and color_button. When a color_button is clicked, it sets the color variable appropriately.

The last bit of this section just creates the toolbars and sets them up with hpair layouts. Once this is all done, we just need to run the eventloop on the top-level layout widget.

But don't take our word for it: run it; draw a dinosaur, if you have the patience!

Task 1: better layout

Suffice to say, the paint program we've designed so far is pretty ugly. The first problem is with the layout: we only have hpair, so everything just goes off to the right. Half of the canvas is cut off!

We can solve this in a few steps: first, we'll add a new vertical layout, vpair; then we'll extend both of our pair layouts to list layouts.

Define vpair

For your first task, define vpair in widget.ml. It should have the same type as hpair---that is, t -> t -> t---though the logic in vpair's implementation will be slightly different.

In particular, the translations in the repaint and handle functions will need to be swapped: instead of translating on the x-axis, we should translate on the y-axis. We'll also need to change the size function: instead of summing along the x-axis and taking the greater y-axis value, we should take the greater x-axis value and sum along the y-axis.

Once you've added these definitions, you be sure to add an entry for vpair in widget.mli, or you won't be able to use it in main.ml!

Define list_layout

Before going ahead and changing main.ml, though, let's go one better: why not have horizontal and vertical list layouts, that take more than just a pair of widgets? We'll define a function list_layout : (t -> t -> t) -> (t list) -> t that takes a pair layout and a list of widgets, arranging the widgets in order.

That is, list_layout hpair [w1;w2;w3;...;wn] should be same as hpair w1 (hpair w2 (... (hpair wn-1 wn) ...)). That is, we can implement list layouts as a fold over the list of widgets. As our base case, we can use a trivial spacer, space (0,0), which takes up no space and won't change anything.

Create vlist and hlist

Using list_layout, then we can define:

let vlist (ws: t list) : t = list_layout vpair ws

let hlist (ws: t list) : t = list_layout hpair ws
Add these definitions to widget.ml and their declarations to widget.mli.

Change the toolbars to use vlist and hlist

Now we can lay out our paint program much more cleanly. First, change the color toolbar to use an hlist. Then layout all of the widgets using a vlist so that the canvas is above the undo and quit buttons (which can be in an hlist themselves), which are above the color buttons. (You can use the screenshot of our complete program as a guide. It doesn't have to look exactly the same!)

Task 2: drawing points

Our primitive program only allows us to draw lines, so we will now extend it to draw points as well. To make things simple, points will always be one pixel in size. An overview of the steps we'll take:

  1. Add a function in Gctx that allows us to draw points on a canvas.
  2. Add a new mode, which allows us to figure out if we should be drawing points or lines when the user clicks.
  3. Update the type draw, so we can store the color and location of drawn points.
  4. Update repaint to link the points to be drawn (step 3) with the drawing operation (step 1).
  5. Update canvas_action so when we click in the new mode (step 2), it stores the desired point by creating a new draw item (step 3) and adding it to the history.
  6. Add a button to the toolbar so we can toggle to the new mode (from step 2).

Implement draw_point

First define draw_point in gctx.ml. This is a function that takes in a graphics context and a point, and draws a point in that location in the context. The type is therefore t -> int * int -> unit.

You might be asking yourself: what color do we draw the point in? Remember that the graphics context keeps an "active" color, so the point should be drawn using that active color. Look at Gctx.draw_line for an example.

To do the actual point drawing, use Graphics.plot, a function in the native Ocaml Graphics library. Remember that calling any native drawing method requires translation of the coordinates from our top-left-origin system to the bottom-left-origin system that Graphics library assumes. The translate_point function will be helpful here.

Add the new function's declaration to gctx.mli.

Adding point modes

We now have a function that draws a single point at some (x, y) on the canvas. Our next job is to hook these into the paint program, by adding a mode to distinguish when we want to draw lines versus (our new) points. Add a PointMode constructor to the mode type.

The PointMode constructor doesn't need to carry any data: unlike line drawing, we can draw a point in a single click, so we don't have to track anything in our mode.

Adding point actions

Recall that we are storing the "history" of the canvas in the actions list, where each item in the list represents some kind of basic drawing command. We need to make it so that "drawing a point" is one of these possible commands.

Open main.ml and augment the draw type with a constructor Point of Gctx.color * point. (A command to draw a point just requires knowledge of the current color, and the point location.)

Updating repaint

Now update repaint so that when it sees Point (c,p) in actions, it will appropriately call the Gctx.draw_point function we defined earlier and actually draw the point on the canvas.

Updating canvas_action

We want to create a Point(c, p) and add it the "history" (actions) each time we click (while in PointMode, but not if we're in some other mode, like LineStartMode).

Update canvas_action so if a click was detected and we are in PointMode, a Point draw command with the correct color and location is added to the actions list.

Adding a button to the toolbar

The final step is to add buttons to the toolbar that allow us to toggle between Point and Line modes.

You will need to create two buttons: one for Lines and another for Points (see the reference image above). When you click on the Line button, it will set the current mode to LineStartMode; when you click on the Point button, it will set the current mode to PointMode. You can use mouseclick_listeners to set up these actions.

Once the buttons have been added and you can switch modes, you should be able to draw points when running your application!

Task 3: preview and drag-and-drop

The next step in making the paint program usable is adding drag-and-drop drawing of lines. This is the way most programs draw lines (not the click-two-endpoints method we currently have).

We'll make this change in two steps. First, we'll add previewing. Currently when drawing a line, you don't see anything until both clicks have been made. By adding previewing, we will draw a "preview" line from the first click's location to the current mouse position.

After we've added previewing, we'll make it so that lines are drawn by dragging.

Preview

Previewing for line drawing shouldn't be hard: if the first click in a line drawing has already been made, then !mode = LineEndMode (x,y) for some point (x,y). We just need to change canvas_action so that we know the mouse's position---the preview line should be drawn from (x,y) to the current position of the mouse over the canvas.

There are two questions: (1) how do we get the current position of the mouse over the canvas, and (2) how do we draw the preview line?

We can find the current mouse position by changing canvas_action. Right now, the event handlers for the canvas only respond to clicks. We simply need to extend the else case of the event handler to do something else when in LineEndMode.

The "something else" is setting up previewing. In order to draw the preview line on the screen, we have to change the repaint function. We don't want to add the preview line to actions, though, since we need to draw a different preview line every time the mouse moves, and it will be difficult to know when to add and remove items from actions. Our solution will be to add:

let preview : draw option ref = ref None
We need to make two changes. First, we need to make sure repaint draws whatever is being previewed. (It should draw it last, since it's the most recent action and highest in the z-order.) Second, we need to change canvas_action to manage the preview a variable. When we get the second click, we need to set preview := None and update actions. When the mouse moves (without clicking) in LineEndMode, we need to set preview to an appropriate value.

Drag-and-drop line drawing

To add drag-and-drop line drawing, we just need to change the logic for previewing and drawing. A "drag" is when the user clicks and holds the mouse button; a "drop" is when the user releases the mouse button after dragging. We want to draw a line by clicking at the start point, dragging the line, and dropping at the end point.

The logic here is simple:

Task 4: drawing ellipses

For your next task, add drag-and-drop ellipse drawing. You'll need to make a number of changes, which follow a similar pattern as we used to add point drawing:

There are different ways to draw ellipses. We'd like you to implement ellipse drawing via "bounding boxes": the first click fixes one corner of a box that fits exactly around the ellipse; the location where the user drops fixes the opposite corner. (Most drawing programs work this way.)

Task 5: checkboxes and line thickness

In this task, you must define a checkbox widget. It should have the following signature:

val checkbox : bool ref -> string -> t * label_controller
When the checkbox is clicked, the bool ref should be toggled. The string is a label for the checkbox. You can draw the checkbox a number of different ways, but make it clear when it is and isn't selected. (Ours draws an "X" in the box when the checkbox is selected.)

Add a checkbox labelled "Thick" to the GUI that controls line thickness when drawing lines and ellipses: when the "Thick" box is checked, the lines should be thick; when unchecked, the lines should be thin. You can control line thickness in Ocaml with the Graphics.set_line_width function. (Hint: you'll want to treat line thickness like colors.)

Task 6: something cool

Your final task is to do something cool! Here are a few things of appropriate coolness and difficulty:

Our implementation has all three of these. You should pick just one. If there's something else you want to do, run it by us by emailing cis120e@cis.upenn.edu---it should be hard but not too hard.