Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Programming Windows Using State Tables


SP91: PROGRAMMING WINDOWS USING STATE TABLES

PROGRAMMING WINDOWS USING STATE TABLES

Michael A. Bertrand and William R. Welch

Mike teaches mathematics and programming at Madison Area Technical College, Madison, WI 53704. Bill is a freelance writer and programmer who holds a Ph.D. in biological science. He can be reached at 201 Virginia Terrace, Madison, WI 53705.


This article presents a Windows-based program called "Draw" that uses state tables to implement interactive drawing tools in an economical, consistent fashion. Draw renders four kinds of geometric figures: rectangles, rounded rectangles, ellipses, and lines. Each type is associated with a drawing tool that's accessed by means of a menu choice (see Figure 1). Our implementation uses state tables to encapsulate program control flow in a single data structure (an array of pointers to functions). Using this technique, you can easily extend the program to support other kinds of geometric figures, as long as the user interaction for the new types is similar to the types described here.

Before discussing the details of our implementation, it is useful to review some of the basic concepts behind Windows programs.

Event-Driven Programming

As more and more programmers are finding out, writing programs for Microsoft Windows and other event-driven GUIs is very different from writing traditional DOS programs. In a Windows program, your program does not have a single line of control, flowing from beginning to middle to end. Rather, it responds to all manner of events (or, in Windows parlance, messages) that are sent by the system to all applications, at arbitrary or unpredictable times. This event-driven structure follows the pattern of interaction of a real-world user driving an interactive graphics application: Any one event, such as a mouse movement, is about as likely to occur as any other (say, a keystroke or a menu choice).

Using window procedures (called Wndprocs), your application is able to respond to all of these events or messages as they occur. This is not merely a suggestion, but an implementation requirement. Each type of window in a Microsoft Windows application must have a procedure associated with it that receives all messages sent by environment to that class of window. The messages correspond to external events (mouse movements, mouse clicks, keystrokes) as well as internal events (for example, the message that asks the application to redraw its screen display, or a message sent by another application, and so on).

With this bit of background, we can now discuss the Draw program. Draw consists of a single header file, Draw.h (Listing One, page 45), and a single C-language source file, Draw.c (Listing Two, page 45). There is also a makefile (Listing Three, page 46) and two files required by Windows: the definition file, Draw.def (Listing Four, page 46), and the resource file, Draw.rc (Listing Five, page 46).

The Main Window Procedure

In general, every application has a main window and an associated main window procedure. If the application has other kinds of windows (known as child windows), each of these kinds will have a window procedure defined for it as well. Draw creates only one kind of window, so it has only a single window procedure, WndProc.

The function WndProc contains code to respond to Windows messages such as selecting a drawing tool from the menu, responding to mouse events, and repainting the window when it is moved or resized. WndProc passes mouse-button and mouse-move events to the function Tool, which manages drawing. Tool provides a template for interactive drawing tools and is the real heart of Draw.

When using Draw, you interactively display geometric figures by invoking three mouse events: left-button-down, mouse-move, and left-button-up. These three events produce the Windows messages WM_LBUTTONDOWN, WM_ MOUSEMOVE, and WM_LBUTTONUP, respectively. As is common in Windows programs, Tool uses these messages as case constants in a switch statement. With the rectangle tool, for example, you first depress the left mouse button (WM_LBUTTONDOWN) to define the x and y coordinates (x1 and y1) of the initial corner of the figure. Then, as you move the mouse without releasing the button (dragging the cursor and producing a series of WM_MOUSEMOVEs), the program repeatedly erases and redraws the rectangle while the current mouse position defines the x and y coordinates (x2 and y2) of the rectangle corner opposite the initial corner. The final figure appears when you release the left mouse button (WM_LBUTTONUP).

A Simplifying Technique

Draw's four tools require a minimal amount of code. The key to this economy is the data structure DrawFig, which is an array of pointers to functions --one for each tool. All four tools work in exactly the same way (that is, left-button-down, mouse-move, left-button-up), and their functions have the same parameters and return a value of the same type. In choosing a tool through the menu, the program sets the value of the DrawFig index, iFigType. This value, in turn, determines which function is pointed to by the DrawFig array and used for the actual drawing in Tool.

Two of the functions that the DrawFig array points to, Rectangle and Ellipse, are standard Windows functions, that is, part of the native Application Program Interface (API). The other two functions that the DrawFig array points to, DrawRoundRect and DrawLine, are our own. This is because the native Windows functions to draw rounded rectangles (RoundRect) and lines (MoveTo and LineTo) have different parameters than Rectangle and Ellipse. To deal with this difference, we wrote the DrawRoundRect and DrawLine functions. These two have the same parameters as Rectangle and Ellipse, so all four functions can be included in the same array of pointers to functions, the DrawFig array.

This scheme of using the DrawFig array to point to tool functions that use the same three mouse events to draw figures has an important ramification: Other similarly behaving tools can be added to Draw by simply including pointers to the appropriate functions in the list of the DrawFig array initializers and including them in the menu. Additions might, for example, be tools for isosceles triangles, regular polygons, and parabolic segments.

Storing Figure Coordinates

In any Windows application, whenever the user moves a window or changes its size, Windows sends a WM_PAINT message to the application to erase and redisplay the entire output are of the window. Any figures produced by Draw will be erased, and Draw must redraw them if they are to stay on the screen as the location or size of the window is changed.

This restoration of the window contents can be accomplished only if Draw in some way saves the figures. This it does, in the externally defined structure faList, which is an array of structures of type FIGURE. Each FIGURE is faList contains a field (name iType) that indicates the type of figure (rectangle, rounded rectangle, ellipse, or line) and a structure (rsCoord) that contains the x and y coordinates of the two end-points which define the location of the figure. Values for these variables are assigned -- a new figure is saved -- in this case block WM_LBUTTONUP of function Tool. Whenever WndProc gets a WM_PAINT message, it traverses faList, a simple graphics database, to restore the screen. The array faList is characteristic of the graphics programming approach known as vector-based or display-list oriented approach. (This technique is also sometimes loosely called object-oriented.) In this approach, a geometric figure is represented in the database, or display list, by a set of drawing commands and endpoint coordinates that determine how the list is traversed to display the figures. In Draw, the drawing command in the list is the type of figure (iType); more elaborate systems include attributes such as line width and line color.

Rubber Banding Figures

Draw uses the standard Windows raster operation (ROP2) codes in order to "rubber band" a figure as the mouse cursor is dragged. This occurs in Tool, in the case block WM_MOUSEMOVE, which calls the Windows function Set-ROP2 with argument R2_NOTXORPEN. This argument sets the XOR (eXclusive OR) drawing mode. Using the previous values of x2 and y2, the XOR mode causes the tool function called by the DrawFig array to erase the existing figure. DrawFig calls the tool function again, using the current values of x2 and y2 to draw the new figure. When the same figure is drawn twice in the same place in XOR drawing mode, figures in the background are left unchanged. Case WM_LBUTTONUP calls ROP2 with argument R2_COPYPEN, setting the COPY drawing mode. In COPY mode, the background color fills the interior of the figure, erasing overlapped portions of any underlying figures.

System State Tables

The concept of "system state" is central to understanding Draw. It is the current state of the application that determines the response of the program to a mouse event. We use a single variable (iState, in Tool) to represent the system state.

Table 1 shows Draw's state table. It shows how the two system states (WAITING and DRAWING) are related to the three mouse events (WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_LBUTTONUP) that DRAW's tools use to display figures. When you use DRAW, you send a series of mouse events to Tool. Tool's response to a given mouse event depends not only on that event, but also on the sequence of previous events. Tool records this sequence of mouse events as transitions in system state, and the state table documents these transitions.

Table 1: State table shows the changes from one system state to another (Waiting to Drawing and back), as triggered by the mouse events (left-button-down, move, left-button-up).

                           Mouse Event
  System State  WM_LBUTTONDOWN  WM_MOUSEMOVE  WM_LBUTTONUP
  --------------------------------------------------------

  WAITING         DRAWING            --           --
  DRAWING            --              --        WAITING

To use Table 1, enter at the initial system state, WAITING, and read across to see the effect of mouse events. WM_MOUSEMOVE and WM_LBUTTONUP have no effect, but WM_LBUTTONDOWN causes a transition to a new system state, DRAWING, and starts the tool. Reenter the table at the new system state, DRAWING, and again read across. Now WM_LBUTTONDOWN and WM_MOUSEMOVE have no effect, but WM_LBUTTONUP causes a state transition back to WAITING, and stops the tool. Tool responds to only one sequence of mouse events: WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_LBUTTONUP. This sequence is reflected in only one path through the state table: WAITING --> DRAWING --> WAITING.

System state can both determine the response to a mouse event and be determined by a mouse event. For example, in case WM_MOUSEMOVE of Tool, if iState equals DRAWING, the old figure is erased and the new one is drawn; if iState equals WAITING, there is no effect (break). By contrast, in case WM_LBUTTONDOWN, if iState equals WAITING, iState is changed to DRAWING and the endpoint coordinates are assigned.

Draw's tools are simple; thus Table 1 is correspondingly simple. Table 2 is a slightly more involved example that describes what would happen if two mouse events, right-button-down (WM_RBUTTONDOWN) and right-button-up (WM_RBUTTONUP), were added to translate (that is, change the location of) the figure being drawn. In this example, when you depress the right button while drawing, mouse moves translate the figure without changing its shape.

Table 2: This state table extends the relationships in Table 1 by adding two mouse events and another system state.

                                      Mouse Event
  System WM_LBUTTONDOWN WM_MOUSEMOVE WM_LBUTTONUP WM_RBUTTONDOWN WM_RBUTTONUP
  State
  ---------------------------------------------------------------------------

  WAITING     DRAWING          --            --         --              --
  DRAWING        --            --         WAITING    TRANSLATING        --
  TRANSLATING    --            --         WAITING       --           DRAWING

To use Table 2, enter at the initial system state, WAITING, and read across. WM_LBUTTONDOWN causes a transition to DRAWING and starts the tool, as before. WM_LBUTTONUP causes a transition back to WAITING, as before, and stops the tool. An intervening WM_RBUTTONDOWN, however, changes the state to TRANSLATING. In state TRANSLATING, WM_MOUSEMOVEs cause translations rather than rubber banding. WM_RBUTTONUP changes the state back to DRAWING. You can alternate between rubber banding (state DRAWING) and translating (state TRANSLATING) until the final WM_LBUTTONUP.

This expanded tool responds to the mouse-event sequence: WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_RBUTTONDOWN, WM_MOUSEMOVE, WM_RBUTTONUP, WM_MOUSEMOVE, WM_LBUTTONUP, and this sequence is reflected in a path through the state table as: WAITING --> DRAWING --> TRANSLATING --> DRAWING --> WAITING.

In implementing the state tables, we coded them as two-dimensional switches, that is, nested switch statements. More elaborate tables might require an array-based approach. In Draw, the mouse event controls the outer switch statement, and the state variable controls the inner one. For Table 2, the skeleton for case WM_MOUSEMOVE is shown in Example 1. To fully flesh out the example, case blocks for WM_RBUTTONDOWN and WM_RBUTTONUP would have to be added to the switch statement that selects from iMessage in Tool. Also, the state variable iState would have to be changed accordingly.

Example 1: The skeleton for case WM_MOUSEMOVE

  case WM_MOUSEMOVE:
    switch (iState)
     {
     case WAITING:
      /* Tool not started; nothing to do. */
      break;  /* WAITING */
    case DRAWING:
      /* User is rubber banding.  Erase old figure and draw new
         figure.  Reset statics (x2,y2) to mouse coordinates. */

       ............ rubber banding code here ............
      break;  /* DRAWING */
    case TRANSLATING:
      /* User is translating.  Erase old figure and draw new
         figure.  Reset statics (x1,y1) and (x2,y2) to translated
         values. */

      ............. translating code here ............
      break;  /* TRANSLATING */
    }  /* switch (iState) */
  break;  /* WM_MOUSEMOVE */

Table 2 demonstrates that, as permissible sequences of mouse events and consequent system states are added to a program being developed, the complexity of the interactions increases rapidly. The code that describes these interactions necessarily becomes equally complex. Poorly managed complexity leads to intractability. State tables are a way to cut through this complexity. If state tables are first used to describe the interactions are constructed before the code is written they provide a guide for writing the code. New features can be added with only a minimal alteration of working code. State tables become a means of managing complexity and are therefore a valuable aid in writing and documenting Windows applications that make heavy use of mouse events.



_PROGRAMMING WINDOWS USING STATE TABLES_
by Michael A. Bertrand and William R. Welch


[LISTING ONE]
<a name="02c0_000d">

/***********DRAW.H : header file for DRAW.C **************************/

#define WAITING  0   /* the possible values for variable iState in  */
#define DRAWING  1   /* Tool() are WAITING and DRAWING              */

/*  These constants are the possible values for iMenuChoice, the variable
 *  recording the user's menu choice. The old menu choice must be stored
 *  so the check mark can be removed from the menu when a new menu choice
 *  is made. Do not change. */
#define IDM_RECT        100
#define IDM_ROUND_RECT  101
#define IDM_ELLIPSE     102
#define IDM_LINE        103
#define IDM_ABOUT       104

/* These constants are the possible values for iFigType, the variable
 *  recording the current FIGURE, as chosen through the menu. The value is
 *  also stored in the iType field in faList[] and is used to determine
 *  which drawing function is called upon from DrawFig[], the array of
 *  pointers to functions; since these values are indices into an array,
 *  starting at 0, they may not be changed. */
#define FT_RECT         (IDM_RECT       - IDM_RECT)
#define FT_ROUND_RECT   (IDM_ROUND_RECT - IDM_RECT)
#define FT_ELLIPSE      (IDM_ELLIPSE    - IDM_RECT)
#define FT_LINE         (IDM_LINE       - IDM_RECT)

/* maximum number of FIGUREs in faList[] */
#define MAX_FIGS 1000

/* FIGUREs in faList[]: rectangle, rounded rectangle, ellipse, line */
typedef struct
{ int  iType;
  RECT rsCoord;
} FIGURE;

/* global variables */
FIGURE faList[MAX_FIGS];   /* List of FIGUREs */
int    iListSize;          /* tally number of displayed FIGUREs */
HANDLE hInst;              /* current instance */
RECT   rClient;            /* client area in scr coords for ClipCursor() */

/* function prototypes */
long FAR  PASCAL WndProc(HWND hWnd, unsigned iMessage, WORD wParam,
                                                           LONG lParam);
void NEAR PASCAL Tool(HWND hWnd, unsigned iMessage, LONG lParam,int iFigType);
BOOL FAR  PASCAL DrawRoundRect(HDC hDC, int x1, int y1, int x2, int y2);
BOOL FAR  PASCAL DrawLine(HDC hDC, int x1, int y1, int x2, int y2);
BOOL FAR  PASCAL AboutDraw(HWND hDlg, unsigned message, WORD wParam,
                                                        LONG lParam);
/* DrawFig[] is an array of pointers to FAR PASCAL functions, each with parms
 * (HDC,int,int,int,int) and returning BOOL. Note Rectangle() and Ellipse() are
 * MS Windows GDI calls, while DrawRoundRect() and DrawLine() are our calls. */
BOOL (FAR PASCAL *DrawFig[4])(HDC hDC, int x1, int y1, int x2, int y2)
     = {Rectangle, DrawRoundRect, Ellipse, DrawLine};







<a name="02c0_000e">
<a name="02c0_000f">
[LISTING TWO]
<a name="02c0_000f">

/******* DRAW.C by Michael A. Bertrand and William R. Welch. *******/

#include <windows.h>
#include "draw.h"

int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpszCmdLine,
                                                                  int nCmdShow)
  /*    hInstance     : current instance handle
   *    hPrevInstance : previous instance handle
   *    lpszCmdLine   : current command line
   *    nCmdShow      : display either window or icon
   */
{ static char szAppName [] = "Draw";
  static char szIconName[] = "DrawIcon";
  static char szMenuName[] = "DrawMenu";

  HWND        hWnd;       /* handle to WinMain's window */
  MSG         msg;        /* message dispached to window */
  WNDCLASS    wc;         /* for registering window */

  /* Save instance handle in global var so can use for "About" dialog box. */
  hInst = hInstance;

  if (!hPrevInstance)              /* Register application window class. */
  { wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = WndProc;   /* function to get window's messages */
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(hInstance, szIconName);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName  = szMenuName;  /* menu resource in RC file */
    wc.lpszClassName = szAppName;   /* name used in call to CreateWindow() */
    if (!RegisterClass(&wc))
      return(FALSE);
    }
                                /* Initialize specific instance. */
    hWnd = CreateWindow(szAppName,              /* window class */
                        szAppName,              /* window caption */
                        WS_OVERLAPPEDWINDOW,    /* normal window style */
                        CW_USEDEFAULT,          /* initial x-position */
                        CW_USEDEFAULT,          /* initial y-position */
                        CW_USEDEFAULT,          /* initial x-size */
                        CW_USEDEFAULT,          /* initial y-size */
                        NULL,                   /* parent window handle */
                        NULL,                   /* window menu handle */
                        hInstance,              /* program instance handle */
                        NULL);                  /* create parameters */

  ShowWindow(hWnd, nCmdShow);   /* display the window */
  UpdateWindow(hWnd);           /* update client area; send WM_PAINT */

  /*  Read msgs from app que and dispatch them to appropriate win function.
   *  Continues until GetMessage() returns NULL when it receives WM_QUIT. */
  while (GetMessage(&msg, NULL, NULL, NULL))
  {  TranslateMessage(&msg);     /* process char input from keyboard */
     DispatchMessage(&msg);      /* pass message to window function  */
  }
  return(msg.wParam);
}
/****************************************************************/
long FAR PASCAL WndProc(HWND hWnd,unsigned iMessage, WORD wParam, LONG lParam)
  /*  IN:   hWnd     : handle to window
   *        iMessage : m^C




<a name="02c0_0010">
<a name="02c0_0011">
[LISTING THREE]
<a name="02c0_0011">

THIS LISTING IS CURRENTLY UNAVAILABLE



<a name="02c0_0012">
<a name="02c0_0013">
[LISTING FOUR]
<a name="02c0_0013">

THIS LISTING IS CURRENTLY UNAVAILABLE



<a name="02c0_0014">
<a name="02c0_0015">
[LISTING FIVE]


THIS LISTING IS CURRENTLY UNAVAILABLE


Copyright © 1991, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.