[top] [up] [next]

Writing custom controls


This tutorial uses the Tic-Tac-Toe example, this example is located in the folder 'examples\tictac', it can also be started by pressing the menu items 'Examples | Start...' and selecting the tictactoe example.

The Tic-Tac-Toe example demonstrates writing a custom control and drawing on a canvas. A custom control is derived from the class customControl, custom controls are not implemented by Windows but by Trinc-Prolog, the complete control is thus written in prolog.

Declaring the custom control

The purpose of the Tic-Tac-Toe control is to be able to play the Tic Tac Toe game. the control draws 9 rectangles in it's client area and the user can click on one of the nine rectangles to fill it with a cross or a circle. A single rectangle can be cleared if the right-mouse button is clicked over it. Below there is an picture of the Tic-Tac-Toe control that is empty and one that is filled.

    pl_img19.gif (4553 bytes)    pl_img20.gif (5292 bytes)

The control class is called tttControl and it is derived from customControl. The class has two attributes, the first is called State and it will contain a list of values representing each of the nine fields, the second is called Last and is used to remember the value that was last assigned to a field. If the attribute Last contains a 'x' then the next field will be assigned an 'o'.

  class tttControl.
    inherit customControl.
    parts State, %Stores the state of all the fields of the control
               Last. %Stores type of the last value added
  public.
    init/0.
    giveCell/3.
    putCell/3.
    isEmpty/2.
    clearCell/2.

    doOnPaint/1.
    doOnMouseDown/1.
  private.
    drawLines/3.
    drawState/3.
    drawField/6.
    clientToCell/4.
    giveCell2/3.
    putCell2/3.
    isEmpty2/2.
    putCellOpposite/3.
  endclass tttControl.

The class declares two methods which will respond to external events, doOnPaint/1 and doOnMouseDown/1, these methods must be declared public because else they cannot be called by the system. The class further declares and hides 8 private methods that are used by the public methods.

Initializing the control

The control must respond to paint events and to mouse events, the init/0 method of the class initializes an instance and adds the necessary callbacks, the source of the init/0 method is:

  tttControl::init :-
    this(T),
    State := [[A1, B1, C1], [D1, E1, F1], [G1, H1, I1]],
    Last := o,
    add_callback(T, onPaint, T, doOnPaint), %onPaint events are handled by doOnPaint/1
    add_callback(T, onMouseDown, T, doOnMouseDown),
    putAllowDisplayInfo(false),
    !.

The this/1 predicate is used to retrieve the class instance for which the method is being executed, the variable T will contain the clause of the instance after this/1 has exited. The next two lines initialise the attributes of the instance with default values, the State attribute is assigned 3 three lists which each contain three elements, each variable contains the value of a single field.
By using the add_callback/4 predicate the control will be called when either a paint event occurs or a mouse button is pressed. The last method window::putAllowDisplayInfo/1, is to prevent a popup menu item from appearing over the control if the right mouse button is clicked. The default behavior of each window is to show a popup menu when the right mouse button is clicked.

Painting the control

The public method doOnPaint/1 is called whenever the control, or a part of it, must be (re)painted. The parameter of each call to doOnPaint/1 is an onPaint structure which has two members, a Sender variable which refers to the instance to (re)paint (the sender of the event) and a Rect variable which contains a rectangle defining the area that must be painted.

To make the drawing code in this example easier the rectangle to draw is ignored and the complete control is redrawn each time, even if this is not necessary.

The four steps for drawing the control are:

  1. Determine the area that must be painted, use the Rect variable of the onPaint event structure or determine it in another way. The tttControl retrieves its width and height by calling the method clientRectangle/1.
  2. Create an instance of the canvas class and initialise it, call the method startDraw/0 to allocate resources for drawing.
  3. Draw the control using the canvas, to fill the background area a brush is created and initialized.
  4. Call the method stopDraw/0 to release the allocated resources for drawing and delete the canvas instance. In this example the brush used to fill the background must also be deleted.
  tttControl::doOnPaint( onPaint(Sender, Rect) ) :-
    this(T),
    %Step 1
    clientRectangle(rectangle(_, _, Width, Height)),
    %Step 2
    C new_obj canvas,
    C<-init(T, true),
    C<-startDraw,
    %Step 3
    B new_obj brush,
    B<-createSolid(10, 100, 200),
    C<-putBrush(B),
    C<-drawFillRect(rectangle(0, 0, Width, Height)),
    drawLines(C, Width, Height),
    drawState(C, Width, Height),
    %Step 4
    C<-stopDraw,
    del_obj C,
    del_obj B,
    !.

The cut operator is used to prevent a redo of the methods called during the handling of the paint event, this increases the speed of painting.

Responding to mouse events

By pressing on the left mouse button the user can fill a field with either a 'x' or an 'o' and by pressing the right mouse button a field can be cleared. There are two event methods for these two types of actions, Prolog matching is used to select the correct method implementation.

The onMouseDown/5 event structure contains an atom that describes the button clicked, the values of that parameter can be left, middle and right. By stating the button in the head of the clause the matching process selects the appropriate implementation.

  tttControl::doOnMouseDown( onMouseDown(Sender, point(X,Y), left, keys(Shift, Ctrl), Dragging) ) :-
    clientToCell(X, Y, CellX, CellY),
    isEmpty(CellX, CellY),
    putCellOpposite(CellX, CellY, Last),
    invalidateClient(false),
    !.

  tttControl::doOnMouseDown( onMouseDown(Sender, point(X,Y), right, keys(Shift, Ctrl), Dragging) ) :-
    clientToCell(X, Y, CellX, CellY),
    clearCell(CellX, CellY),
    invalidateClient(false),
    !.

After the correct method is selected by the matcher a private method of the tttControl class is called to convert the client coordinate of the event to a field coordinate of the control, for instance: the pizel point (200, 125) is converted to row 2, field 1.

To have the control redraw itself the invalidateClient/1 method is called, this methods adds the complete client area of the window to the update rectangle of the window and makes sure that a paint event is send to the control. The cut operator is used to prevent backtracking.

Creating a custom control is not a simple task, so it is advisable to add small increments to the code and to test these thoroughly before proceeding with the rest of the control.

[top] [up] [next]

 

info@trinc-prolog.com