Fenestra User's Guide (Chapter 1) -- contents | previous | next
As an introduction to the library, this chapter presents a complete small application with a main window, a dialog box, and a mouse interface. It will show how to use windows, draw in them, and how to build dialog boxes. It is a practical and realistic example: it has code for the non-GUI part of the application, and thus illustrates how the user interface interacts with the body of the program.
The sample application is an implementation of the Tic-tac-toe game (where the players try to make a line of three pieces on a 3 x 3 board). The following image shows the main window:
The mouse will be used to position a new piece on board, or to start a new game if the previous game was finished.
The `Game' menu will have three items:
The program will be made of three main classes: one for the game model, another for the main window, and one for the options dialog. The whole program is made entirely of Eiffel code, but for the program's icon which is the only external resource, which has to be compiled with the Win32 SDK's resource compiler and linked into the program executable file.
Please note that this chapter does not explain how to build a system description file -- the Eiffel's system `makefile' -- as this depends on the compiler used; section (see section 2.5) gives details on how to build programs with the supported compilers. The samples/tutorial directory in your installation includes the appropriate files for all supported compilers.
The tic-tac-toe game itself is implemented in the class TICTACTOE. Here is its flat-short form:
class interface TICTACTOE creation make feature specification -- Constant(s) max_level : INTEGER is ??? -- Maximum computer intelligence. feature specification -- Reset reset -- Create/initialise. feature specification -- Option(s) set_level (lvl : INTEGER) -- Level (1: Easy, Max_level: Difficult). require valid : (lvl > 0) and (lvl <= max_level); ensure done : level = lvl; feature specification -- Status is_square (col, row : INTEGER) : BOOLEAN -- Is this (col,row) correct? ensure done : Result = ((((col >= 1) and (col <= 3)) and (row >= 1)) and (row <= 3)); is_empty (col, row : INTEGER) : BOOLEAN -- Is this square empty? require in : is_square (col, row); is_user (col, row : INTEGER) : BOOLEAN -- Is this a user's square? require in : is_square (col, row); is_computer (col, row : INTEGER) : BOOLEAN -- Is this a computer's square? require in : is_square (col, row); is_finished : BOOLEAN -- Finished? is_user_winner : BOOLEAN -- Who wins? require ok : is_finished; is_computer_winner : BOOLEAN require ok : is_finished; feature specification -- Action move (col, row : INTEGER) -- User plays on (col,row). require notend : not is_finished; ok : is_square (col, row); empty : is_empty (col, row); computer_move -- Computer plays. require notend : not is_finished; end interface -- class TICTACTOE
As the functionality of this class is completely independent of the user interface, the implementation details are not shown here (the full source code of the example is of course provided with the library distribution). This same class can be used independently of the user interface -- it could be used with another GUI system or a TTY version of the game. As far as possible, it is good practice to separate the user interface code from what the program actually does.
After the model, it's time to see the principal user interface class, the application's main window. The class, VIEW, inherits from APPLICATION, an heir of WINDOW through OVERLAPPED_WINDOW.
The principal class of this system is VIEW. Every application needs a main window which inherits from APPLICATION. The make procedure of APPLICATION launches the event loop (in the class NOTIFIER) which will distribute the incoming messages from the operating system to all windows and other objects receiving messages. Therefore, an heir of APPLICATION is usually the root class of a system using FenestraIt is not required to be the root class though, another class can be used to start the program and then will create an heir of APPLICATION when starting the user interface.. While it is usually the root class, it is not necessarily where the main part of the interface will be implemented, unlike this simple example.
Besides being an APPLICATION, VIEW is also a kind of WINDOW. A window can receive messages from the operating system, such as, among other cases, when the window is to be redrawn or when it is resized or when the mouse is clicked while the pointer is in its client area -- this is the area which is managed by the user program, as opposed to the border, menu bar, etc which are handled by the system. Object-oriented programming's emphasis on data works well with the event oriented model, as an object can have a state which will be altered by the flow of events (actually procedure calls).
With this library, procedures which correspond to events can be redefined for specialised behaviour using inheritance and are generally exported to the classification class PUBLIC_NONE (see Appendix (see section A.2) for details on this class). This simplifies and clarifies the documentation of classes which are designed to be used through inheritance. Most of these procedures names begin with the prefix `when_'. They are not usually deferred as the library provides default behaviour, so only the procedures who need specialisation will need to be actually redefined.
Incidentally, inheritance is not the only method available to reply to events, as the introduction of the menu system will show. The inheritance method is used when it makes sense to group several events which are all associated with a particular kind of objects. The processing of, say, keyboard entry, or mouse events, would not make much sense when isolated from the particular window they are associated with.
Let's now start to look at the actual class text. The class starts with its inheritance and creation clauses:
class VIEW inherit APPLICATION redefine when_created, when_mouse_clicked, when_drawn, when_resized end creation make
The class redefines four events from APPLICATION -- actually from WINDOW: the procedure when_started is called when the class is initialised and it is recommended to put all initialisation code here instead of having a new make procedure which would have to call the parent's make -- the make procedure in the creation clause above is the one provided by the library.
feature { NONE } -- Private attribute(s) model : TICTACTOE board_size : INTEGER painting : BOOLEAN
First, there are a few attributes: board_size and painting are state variables common to several of the drawing procedures, model is a reference to an instance of the class TICTACTOE which is associated with this VIEW.
feature { PUBLIC_NONE } -- Events when_created is -- Window initialised. local ico : ICON do -- Initialise model. !!model.make set_level(1) -- Set title. set_text("Tic-tac-toe") -- Set icon !!ico.make_resource("AppIcon") set_icon(ico) -- Menu bar. create_menu_bar end when_mouse_clicked(pt : POINT) is -- Mouse has been clicked. local col,row,cell : INTEGER sys : F_SYSTEM do if model.is_finished then -- Last game is finished, start a new one. new_game else -- Retrieve square. cell := board_size//3 col := (pt.x - cell) // cell + 1 row := (pt.y - cell//2) // cell + 1 if model.is_square(col,row) and then model.is_empty(col,row) then -- User clicked on a free square. model.move(col,row) if not model.is_finished then model.computer_move end paint else -- Bad place. !!sys sys.beep end end end when_drawn is -- Window drawn. local win_size : INTEGER r : RECTANGLE do -- Initialise window size. win_size := (client_size.width).min(client_size.height) board_size := win_size * 3 // 5 !!r.make r.set_width(win_size) r.set_height(win_size) -- Resize. (`painting' avoids recursion) painting := true set_client_size(r) painting := false -- draw grid draw_board -- draw square draw_squares -- draw winner if model.is_finished then draw_winner end end when_resized is -- Window has been received. do if not painting then paint end end
The first redefined event, when_started, is called when the window is initialised, before being shown. It is used to setup the window, create the menu bar, and initialise the class attributes. The title is set as well as the application's icon, which is loaded from the resource section of the program executable file. The window will automatically be shown (made visible on the screen) after the creation event has been processed.
Mouse clicks are processed by when_mouse_clicked. The position of the mouse, a parameter of the event procedure, is retrieved and converted to a square on our board, and the appropriate command is sent to the model. After the model has been modified, the paint command starts a complete redraw of the client area. This could be optimised to redraw only the modified square.
The use of the F_SYSTEM class to beep if the user clicked outside the board is an example of a mixin class. A mixin class is a set of utility functions in a class with no parameters that can be used as a local variable, an expanded attribute, or through implementation inheritance, though the latter may not be recommended if careful usage of namespace is a concern.
All drawing is handled inside the when_drawn event. This event occurs when the window first appears, or when a part of the window becomes visible after it has been hidden and thus has to be redrawn, or after the window has been minimised or maximised. All the drawing in a normal window should be done during this event. It is possible if required to draw outside this event, but is not usually recommended: it is then necessary to synchronise the drawing done outside the event with the redrawing taking place during the event. When only a portion of the client area need be redrawn, the system prepares the device which will be used for graphical output by setting a clipping rectangle, so that output appears only in the required rectangle. This simple application depends on this feature -- the entire window is always logically redrawn -- it is possible to optimise this and draw only the visible rectangles.
The drawing procedure first sets the window size so that the client area is a square. Note the use of the painting variable to prevent infinite recursion. This is necessary because changing the window size triggers a when_resized event. This procedure is redefined to allow an update of the board size when the user changes the window size.
The remainder of the when_drawn procedure calls the function drawing the board itself and pieces, and, when the game is finished, the name of the winner.
This section is used to keep the options associated with the view, and the corresponding access methods used by the options dialog box to change them.
feature -- Public attribute(s) firstmove_computer : BOOLEAN -- Is the computer going to move first? level : INTEGER -- Current level. feature -- Options set_firstmove_computer is -- Computer moves first. do firstmove_computer := true end set_firstmove_user is -- User moves first. do firstmove_computer := false end set_level(lvl : INTEGER) is -- Set computer intelligence. do level := lvl model.set_level(lvl) end
The following code is the instantiation of the menu bar and associated actions. The procedure create_menu_bar installs the menu bar, and binds the menu items with the actions, implemented by the procedures new_game, options, and quit.
feature { NONE } -- Menu create_menu_bar is -- Initialise the menu bar. local bar : MENU_BAR popup : POPUP_MENU item : MENU_ITEM command : GUI_COMMAND do !!bar.make(Current) !!popup.make(Current) popup.set_name("&Game") !CMD_NEW!command.make(Current) !!item.make("&New") item.set_command(command) popup.add_item(item) !CMD_OPTIONS!command.make(Current) !!item.make("&Options...") item.set_command(command) popup.add_item(item) popup.add_separator !CMD_QUIT!command.make(Current) !!item.make("&Quit") item.set_command(command) popup.add_item(item) bar.add_popup(popup) bar.attach end feature { NONE } -- Dialog dialog : GAME_DIALOG feature { GUI_COMMAND } -- Menu item(s) new_game is -- Start new game. do model.reset if firstmove_computer then model.computer_move end paint end quit is -- Exit application. do close end options is -- Set options. do -- Modeless dialog, menu may be used when dialog open. if dialog /= Void and then dialog.is_valid then dialog.activate else !!dialog.make(Current) end end
The menu bar is first created. Then, the popup menu is prepared, and menu items are associated with it. Once ready, the popup menu is added to the bar, which is in turn attached to its parent window, that is, displayed -- immediately if the window is visible or later otherwise. A menu bar or a menu popup is associated with an overlapped window, it is not possible for other type of windows to have a menu bar -- it would allow poor and confusing user interface designs. An application usually has one single menu bar. The popup menus may be used inside a bar, inside another popup, or independently (associated with the right mouse button for example).
Each menu item can be associated with one event, which occurs when the menu item is chosen by the user. Although it is possible to inherit from MENU_ITEM and redefine the event procedure, it is usually preferred to use command classes. Command classes -- the Eiffel equivalent of other languages' function pointers, or functions as first class variables, or functors -- are small classes which redirect an event from a class to a method in another one. The basic class GUI_COMMAND has a single deferred feature, execute, which will be redefined by the descendant. If only one event is associated with a class implementing the reply, it can inherit directly from GUI_COMMAND, and use Current as the instance of the command. Usually, though, several events are associated with a class, and Eiffel does not permit direct repeated inheritance and polymorphism at the same time. It is therefore more convenient to create a small class which redirects the event.
The typical event handling class will look like CMD_NEW in this sample, which is associated with the procedure new_game of class VIEW.
-- Fenestra functor (automatically generated) class CMD_NEW inherit BOOMERANG_COMMAND[VIEW] creation make feature execute is do parent.new_game end end
The class BOOMERANG_COMMAND is a convenience class providing a parent and a constructor to set it. The class above was generated with a small utility available in the distribution. It may be convenient to setup a macro or template in your favourite environment to produce those simple classes.
While the approach of having an independent class do such a small work may appear at first excessive, it is the normal Eiffel way to do this and it has some benefits: it scales well when implementing each command in its own class makes sense -- for an undo facility or a customisable user interface for instance. When these classes are used for redirection only, the amount of code is minimal, and these classes can be kept with their associated target class if the environment allows to put several Eiffel classes in a file. This approach has several benefits over redefining MENU_ITEM for a similar purpose -- though the latter is possible, as it has already been mentioned.
After it has been created, the command class is associated with the menu item thanks to the procedure set_command. The implementation of new_game and quit is rather straightforward. The procedure options is more interesting. It is used to create the modeless options dialog box. As the dialog is modeless -- that is the user can switch back and forth between the main window and the dialog -- this command can be called when the dialog is already open, this is the reason why a state variable with the current dialog. When a dialog is closed it becomes an invalid window, hence the test dialog.is_valid.
The remaining features of the class VIEW are the actual drawing procedures, called during the when_drawn event. Drawing is done on a graphic device (base class GRAPHIC_DEVICE). The physical device associated with a device object can be anything from the screen to a printer page or a metafile. It can be seen as a rectangle of a definite size, where graphics commands can be issued.
A window has an associated device -- the attribute device -- which enables drawing in the client area of the window. A device must be ready to be used. The library prepares the device for the when_drawn event. When there is a need to draw or change the status of the device outside this event, it is then necessary to prepare, and then release the device.
Every device has a current status made of current settings, such as a font, a pen or a brush. A pen is used by the graphics subsystem to decide how to draw lines (colour, thickness, etc). The brush is used to decide how to draw filling patterns, such as the inside of a filled rectangle. A device also has an associated font, palette, drawing mode (how the new pixels may be combined with the existing), and mapping mode (units to use and where to place the origin of the coordinates system). The status persists for a given device even when the device is released Unlike what happens with GDI devices programmed at the API level., for instance, between two occurences of the when_drawn event.
It should be noted that the window background is painted by the system using the system's default background window colour -- for overlapping windows, not for child windows (see section (see section 3.3.2) on background painting).
feature { NONE } -- Drawing draw_board is -- Draw row. local clr : RGB_COLOR spen : SIMPLE_PEN cell,x,y,i : INTEGER dln : DRAWABLE_LINE pt : POINT do !!pt.make !!dln.make cell := board_size//3 -- set color/pen !!clr.black !!spen.make spen.set_color(clr) spen.set_width(2) device.set_pen(spen) -- draw board from i := 0 variant 4 - i until i > 3 loop -- draw row y := (cell//2) + (i*cell) pt.set_xy(cell,y) dln.set_start(pt) pt.set_xy(4*cell,y) dln.set_final(pt) device.draw(dln) -- draw colum x := (i+1) * cell pt.set_xy(x,cell//2) dln.set_start(pt) pt.set_xy(x,cell//2 + 3*cell) dln.set_final(pt) device.draw(dln) -- forth i := i + 1 end end
The method draw_board draws a black grid on the device. As it can be seen in the code above, a black pen -- made from a black colour object -- is first set; then, lines are drawn on the window background.
Concerning the colour choice, various devices (be it the screen or a printer) have diverse colour capabilities, and it may be necessary to manage a colour palette or check the device colour depth depending on the application. The operating systems nevertheless provides a set of about twenty standard colours that can be used for simple application and should render acceptably on most devices. These colours can be selected thanks to the standard (creation) procedures of classes like RGB_COLOR. More information on colour management is provided in chapter (see section 5).
Every graphics primitive has an associated class inheriting from DRAWABLE. Each class associated with a basic shape or object includes all the parameters associated with the primitive. Devices have a draw procedure which takes a DRAWABLE object to actually display the figure.
The classes POINT and RECTANGLE are general classes used to hold a set of coordinates -- they are not descendants from DRAWABLE -- and are used throughout the library whenever coordinates are required.
The grid can now be filled:
draw_squares is -- Draw all squares. local i,j : INTEGER do from i := 1 variant 4 - i until i > 3 loop from j := 1 variant 4 - j until j > 3 loop if not model.is_empty(i,j) then draw_square(i,j) end j := j + 1 end i := i + 1 end end draw_square(col,row: INTEGER) is -- Draw one square. require ok: model.is_square(col,row) local pt : POINT rect : RECTANGLE cell,subcell : INTEGER dcircle : DRAWABLE_ELLIPSE dline : DRAWABLE_LINE clr : RGB_COLOR gpen : GEOMETRIC_PEN gr : GRAPHICS_ROUTINES do cell := board_size // 3 subcell := cell * 7 // 10 -- cell centre !!pt.make_coord(col*cell + cell//2, row*cell) !!rect.make rect.set_width(subcell) rect.set_height(subcell) rect.translate_centre(pt) if model.is_user(col,row) then -- set color/brush/pen !!gr !!clr.normal_red device.set_brush(gr.color_brush(clr)) device.set_pen(gr.color_pen(clr)) -- draw circle !!dcircle.make dcircle.set_bounding_rectangle(rect) device.draw(dcircle) elseif model.is_computer(col,row) then -- set color/brush/pen !!clr.normal_blue !!gpen.make gpen.set_color(clr) gpen.set_end_round gpen.set_width(cell//8) device.set_pen(gpen) -- draw cross !!dline.make dline.set_start(rect.lower) dline.set_final(rect.upper) device.draw(dline) pt.set_xy(rect.lower.x,rect.upper.y) dline.set_start(pt) pt.set_xy(rect.upper.x,rect.lower.y) dline.set_final(pt) device.draw(dline) end end
We know draw a circle -- a kind of ellipse -- and a cross depending on which player owns the square. The appropriate red and blues pens are used. The circle is filled and so an extra brush is required to fill it correctly.
The class GRAPHICS_ROUTINES is a mixin class including some functions useful for common patterns, such as setting both a device's pen and brush with a pen and brush built from a simple solid colour. It is useful for simple shapes, for example a rectangle will be drawn using the current pen for its border while using the current brush for actually filling it.
Finally, the name of the winner is drawn if the game is finished:
draw_winner is -- Draw winner banner. require ok: model.is_finished local txt : STRING rect : RECTANGLE dtext : DRAWABLE_TEXT pt : POINT clr : RGB_COLOR do !!clr.black if model.is_user_winner then txt := "You win!" elseif model.is_computer_winner then txt := "Computer wins!" else txt := "Nobody wins." end !!dtext.make dtext.set_color(clr) dtext.set_text(txt) !!pt.make_coord(5*board_size//6,3*board_size//2) rect := device.text_extent(dtext) rect.translate_centre(pt) dtext.set_position(rect.lower) device.draw(dtext) end end -- class
It is important to note that the text object (DRAWABLE_TEXT) does not use the device's pen to draw the letters, but has a simple colour associated with it. The text_extent procedure in GRAPHIC_DEVICE allows to retrieve the size the text would occupy if displayed using the current settings, so that formatting can be done. A possible improvement would be to customise the font and height instead of using the default system font.
The main window class has now been completely reviewed.
The user should be able to set the game options: who starts each game, the user or the computer, and how clever the computer is going to be. The options menu command displays the following simple dialog:
Dialog boxes using Fenestra are dynamic. They are actually normal windows in which control child windows (controls thereafter) are dynamically created. Dialogs being normal windows, they are naturally modeless, that is they can be used concurrently with their parent windows. A message is usually sent to the target window, if there is one, to apply the settings when the user clicks on the `OK' button or similar. Modal dialogs also exist, but are simply modeless dialog which disable the parent window.
As the dialogs are dynamic, there is no need for a resource file. Dialogs are laid out by creating controls during initialisation -- usually during the when_started dialog box event. Controls can be positioned using absolute coordinates or, more conveniently, using the positioning facility. Thus controls can be adequately positioned relatively to each other.
A more complete discussion of the dialog and controls system can be found later in chapter (see section 4).
class GAME_DIALOG inherit MODELESS_DIALOG redefine when_started, when_applied end creation make feature { NONE } -- Controls computer : HEAD_RADIO_BUTTON user : RADIO_BUTTON list : LISTBOX[GAME_ITEM] feature { NONE } -- Client client : VIEW
The dialog class inherits from MODELESS_DIALOG which introduces the events when_started and when_applied among others. This class implements the behaviour and aspect of a dialog box, including the standard keyboard interface (shortcut keys, <TAB> to move to the next control, etc).
The controls whose status has to be tracked are class attributes. client is a reference to the associated instance of the application's class VIEW.
feature { PUBLIC_NONE } -- Start when_started is -- Initialise dialog. local group : STD_GROUP_BOX label : STD_LABEL ok,cancel : STD_PUSH_BUTTON listitem : GAME_ITEM do -- Set title set_text("Game options") -- Initialise controls !!group.make(Current) group.set_label("First move") !STD_HEAD_RADIO!computer.make(Current) computer.set_label("Com&puter") computer.indep_set_width(50) group.arrange_first(computer) !STD_RADIO_BUTTON!user.make(Current) user.set_label("&User") user.set_leader(computer) user.place_just_under(computer) user.same_width(computer) group.arrange(user) !!label.make(Current) label.set_label("&Level:") label.indep_set_width(25) label.place_under(group) !STD_DROPDOWN_LISTBOX[GAME_ITEM]!list.make_unsorted(Current) list.place_right(label) group.align_right(list) !OK_BUTTON!ok.make(Current) ok.place_right(group) ok.set_label("&OK") !CANCEL_BUTTON!cancel.make(Current) cancel.set_label("&Cancel") cancel.place_under(ok) -- Resize dialog to enclose all controls arrange -- Initialise controls client ?= parent check good_client: client /= Void end -- Initialise radio buttons if client.firstmove_computer then computer.check_mark else user.check_mark end -- Initialise listbox !!listitem.make(1,"Easy") add_item(listitem) !!listitem.make(2,"Normal") add_item(listitem) !!listitem.make(3,"Difficult") add_item(listitem) !!listitem.make(4,"Expert") add_item(listitem) end feature { NONE } add_item(litem : GAME_ITEM) is require ok: litem /= Void do list.add_item(litem) if litem.level = client.level then list.select_item(litem) end end
After setting the title of the dialog, each control is created. The default position of a control is in the upper left corner of the dialog, so the first control need not be positioned. The other controls are positioned relative to each other, including the radio buttons inside their group box. The first radio button is positioned at the first position of the group box which is resized to enclose the last radio button. A final call to arrange ensures that the dialog nicely encloses its controls.
Two types of radio button are used, the `head' radio button allows the buttons to be managed automatically; that is, only one button can be selected in the group of buttons associated with a head button. This works for both types of selection: those made interactively by the user, and those made on a programmed call to procedures such as computer.check_mark, which unchecks user.
The `OK' and `Cancel' buttons are implemented by the classes OK_BUTTON and CANCEL_BUTTON. These classes are simple push buttons which redirect their main event to, respectively, the dialog box features apply and finish.
The dialog is then initialised with the previous configuration. We first use an assignment attempt to retrieve our client from the parent window reference. It seems the most straightforward way to do it. An alternative way to do it is to redefine parent as a VIEW -- this could be done easily because MODELESS_DIALOG's constructor parent parameter is anchored to parent.
After initialising the radio buttons, the items in the listbox are added. A list box is made of items inheriting from LISTBOX_ITEM which has a deferred feature representing the name of the item, to be displayed in the box. An effective class, LISTBOX_STRING, is provided for simple cases when a simple string is used as an item. In this case, a simple GAME_ITEM class was created to associate an integer, the level, with this string. The method add_item checks the items for the default choice to be correctly presented to the user.
When the user clicks on the `OK' button, apply is called and then the when_applied event in the dialog class occurs. The processing is a fairly straightforward transfer of the dialog state to the corresponding VIEW.
feature { PUBLIC_NONE } -- OK when_applied is -- Apply settings. do check selected: list.has_selection end client.set_level(list.selected_item.level) if computer.is_checked then client.set_firstmove_computer else client.set_firstmove_user end end end -- class