User Interfaces


A Portable User Interface Using curses

Matt Weisfeld


Matt Weisfeld is currently employed by the Allen-Bradley Company in Highland Heights, Ohio. He responsible for the design and development of test software on VAX/VMS, UNIX, DOS, and other platforms. Matt is currently working on a book entitled Building and Testing Portable Libraries in C, to be published by QED in 1993. He can be reached on Compuserve at [71620,2171].

The topics of portability and user interfaces are usually not mentioned in the same conversation, because the hardware-dependent nature of terminal devices makes user interfaces notoriously difficult to port. However, you don't necessarily need to write a separate user interface for each platform, even when you need maximum portability. This article presents a library of routines that allows a programmer to create a portable, text-based user interface using curses.

curses, the screen-handling package available as part of the C package on VMS and most flavors of UNIX or as shareware, allows you to update screens efficiently. curscr keeps an image of the current screen. You change this image by changing the standard screen, stdscr, or by creating a new screen. refresh or wrefresh change curscr to match stdscr.

The User Interface

The example user interface discussed here can be ported to VAX/VMS, MS-DOS/BORLAND C, Hewlett-Packard/HPUX, and SUN/GNU with revision. The display consists of a main window, a menubar, and a dialog box (see Figure 1) .

The main window occupies the entire screen and is the backdrop for all other constructs. At the top of the main window, a single-line menubar presents the user with the available program options. The user chooses an option either by entering the first letter of the option or by using the arrow and return keys. Choosing one of these options will activate a pulldown menu containing further options. The dialog box, used to print informational messages and accept additional user input, resides at the bottom of the main window.

Library functions to handle these user interface constructs, and other specific tasks, simplify the process of building screen applications. The separate library files can be linked into specific user applications.

There are three major reasons for using curses: portability, availability, and usability. Even if curses is inappropriate for a specific application, you can apply the methods presented here for creating a menubar and pulldown menus to any user interface. The libraries can be treated as shells, with the curses commands replaced by other user interface commands. The appropriate #ifdefs make the libraries portable to multiple platforms.

Primary curses Screen Structures

Listing 1 contains a simple curses application. Just as C defines stdout, stdin, and stderr for input and output, curses keeps a memory representation of the screen in stdscr. All window operations affect only this memory representation. To change the screen itself, kept in curscr, you execute refresh or wrefresh — even when deleting a window.

WINDOW, a data structure in curses.h required by all curses applications, contains information such as window location and size. Each window created must correspond to a pointer of this structure type. All curses programs create stdscr by default. Other curses constructs must be explicitly created. In most cases, curses treats stdscr differently from other windows. For example, to clear a window, curses performs the wclear(win) command, but to clear stdscr, curses uses the clear command, with no parameters.

Creating a Popup Window

Since most operations for any user interface involve windows, I built a library function called popup to create a popup window. (Listing 3 contains popup and all other library code presented in this article.) To create a window that entirely covers stdscr, I call popup with MAX_ROWS and MAX_COLUMNS.

WINDOW *mainwin;

mainwin = popup (MAX_ROWS, MAX_COLUMNS, 0,0);
The constants MAX_ROWS and MAX_COLUMNS represent the standard screen size. The header file menu.h (Listing 2) defines all the constants and structures for this user interface.

There are three popup windows in this application: the menubar, the dialog box, and a window used for pulldown menus. These are global to all functions and thus are declared as extern in most of the files.

Color presents a special problem when writing a portable routine for creating a popup window. Since PC curses has color capabilities, whereas VMS and UNIX do not, ifdefs are used to take advantage of this feature. The PC curses command wattrset controls color. The colors representing the foreground and background are ORed together with:

wattrset(mainwin, F_RED | B_BLACK);
PC curs es also has many more box characters to choose from. Many different effects can be obtained by using colors and box characters on the PC. However, if you desire a simple border around a window similar to VMS and UNIX, simply set the background to black and execute the box command on all platforms.

Special Keyboard Input

Accepting Single Keystrokes

The example application uses the arrow keys. This causes a portability problem when creating a windowed curses application. wgetch gets characters from a window. However, curses buffers this input and echoes it to the screen. To prevent line buffering and echo, making the program accept one keystroke at a time, you must call crmode to set the cbreak mode and noecho to unset the echo mode.

Using the arrow keys (from the keypad) on MS-DOS is very straightforward. Either the getch or the wgetch commands will return the necessary key code. (For all codes see Listing 2. )

Obtaining non-printable characters with VMS requires the use of low-level Screen Management (SMG) commands. (See the VMS SMG manual for a complete description.) The two SMG commands needed here are CREATE_VIRTUAL_KEYBOARD, which activates the program for keyboard input, and READ_KEYSTROKE, which returns a keystroke. (VMS returns a short.)

Interpreting Escape Sequences

On UNIX systems, use of the arrow keys, escape key, and return key presents another problem. Both the HP and SUN systems return keystrokes from the keypad with escape sequences. Some systems, such as the HP, include a function called keypad. This function activates the keypad and returns the proper key code directly, saving the programmer from having to interpret the escape sequences.

When the keypad function is not available, the method for dealing with the escape sequences depends on the system. For example, on the SUN system, entering a keypad character will return an escape sequence in three parts. When the first getch is recognized as an escape, two more getch commands must be called in succession. The third character contains the code needed.

The character codes used in these libraries are defined in the file menu.h as: UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, ESCAPE, and RETURN.

Creating a Menubar

In all environments, the same structure defines a menubar:

typedef struct mbar {
   char string[80];
   char letter;
   int pos;
} MENUBAR;
The first field holds the actual string that represents the particular option. For example, if one of the options across the top relates to printing, then the string print is a logical choice. The second field is the letter that invokes the option from the keyboard. The final field, called pos, holds the location of the string within the menubar.

This example uses the code

#define TCHOICES 3

MENUBAR menubar[TCHOICES] = {
   "file", 'f', O,
   "edit", 'e', O,
   "options", 'o', O,
};
to create a menubar (see
Listing 4) . This declaration creates a menubar with three different options, file, edit, and options, invoked by entering an f, e, or o respectively. The positions are initially set to zero, and calculated, when needed, by the menubar routine. All the libraries use the constant TCHOICES to identify the number of options available. To add or delete options, adjust TCHOICES and add or delete the appropriate number of lines in the menubar structure.

The routine topbar generates the menubar window (the window only, not its contents). In most cases, windows are created as separate entities. But since the menubar is a permanent part of stdscr, I made the menubar a subwindow of stdscr to improve efficiency. This technique allows you to refresh just stdscr, instead of refreshing both stdscr and the menubar. The code

WINDOW *swin;

if ((swin = subwin(win,3,(win->MAXX)-4,(win->BEGY)+1,
         (win->BEGX)+2)) == NULL)
   clean_up ();
creates the menubar. The parameter list for the subwin command includes the window pointer of the parent. curses uses this pointer to calculate where to position the menubar (see curses.h). Beware not to use a pointer until you create the actual window. Until a newwin or subwin command is invoked, the pointer is null and passing a null pointer to a function may cause unexpected results.

Even though the curses implementations for all the platforms is highly consistent, the variable names vary across different environments. (To position the menubar, you need the WINDOW structure coordinates of the parent window.) VMS uses _beg_x and beg_y while MS-DOS and UNIX use beg_x and beg_y. To make the code more portable, my code uses macros, such as BEGX.

Displaying a Menu

The function do_menubar performs all the tasks associated with selecting an option from a menubar. First, do_menubar prints the string in the window. The sample menubar includes three strings: file, edit, and options. You should space these three strings appropriately so that they fill the top of the screen evenly. This application uses a function called strmenu for spacing the strings. strmenu takes the menubar structure and the width of the parent window as parameters.

strmenu proceeds in three stages. First, strmenu calculates the number of spaces allocated to each string by dividing the width of the parent window by the number of strings in the menubar (in this case three). Next, strmenu enters a loop that builds the menubar by copying each string and padding it with the proper number of spaces, highlighting the current choice (initially the one on the left) in upper case. Finally, strmenu returns the string pointer to the calling function, and prints the menubar using the mvwaddstr function.

Detecting a Selection

Once the menubar is in place, the user must be able to select one of the options. The user will type either the first character of the option or the return key to activate the highlighted option. By typing the left and right arrow keys, the user can highlight different options. After receiving a keystroke, a switch statement controls the action. If the program encounters an arrow, it moves the highlight either to the left or right (with allowances for wrap-around). An escape terminates the program, while a return breaks out of the loop and invokes the option currently highlighted. By default the program sends all other sequences back to the calling program as a character code.

Creating a Pulldown Menu

A pulldown menu is basically a popup window, except the pulldown routine must be able to adjust for windows of different sizes. To define the choices contained in each pulldown menu, I created the structure CHOICES:

typedef struct choices {
   char string[20];
   char letter;
   int (*funcptr)();
} CHOICES;
As with the menubar structure, CHOICES contains the string that represents the option and the letter that invokes it. The third field, a function pointer, represents the function that will be executed when the option is chosen. The prototypes for these functions are in
Listing 5.

For example, suppose that invoking the menubar option file produces a pulldown menu with the options open, close, and exit. The application initializes the structure

CHOICES choices1[3] = {
   "open ", 'o', c_open,
   "close", 'c', c_close,
   "exit ", 'e', c_exit,
};
Thus, if the user chooses open, the application calls function c_open. (I used the c_ prefix to avoid possible function name conflicts.)

In this example application, there are three options (see Listing 4) . They are all tied together with

typedef struct pmenu {
   int num;
   int maxlength;
   CHOICES *ptr;
} PULLDOWN;
The structure PULLDOWN contains three pieces of information. The first field represents the number of options in the pulldown. The second indicates the maximum string length. The option close has five letters, and thus 5 is the maximum length. When creating the pulldown, the menu should have equal proportions. Thus, the shorter strings are padded with spaces to match that of the longest. The third field is the pointer to the structure that hold the choice information for this particular menu. The initialization of the entire PULLDOWN structure is

PULLDOWN pullmenu[3] = {
   3, 5, choices1,
   4, 6, choices2,
   3, 7, choices3,
};
To create a pulldown, the application calls the function do_pulldown. Choosing an option from the pulldown menu works like the menubar, except that do_pulldown uses UP_ARROW and DOWN_ARROW.

Unlike the menubar, the pulldown menu must be erased after an option has been chosen, and whatever was underneath must be restored. The command touchwin performs this task. For this example, the pulldown window blocks both the menubar and stdscr, so both must be restored.

Tying It All Together

The program in Listing 4 demonstrates the advantages of building these libraries. The actual application requires only a dozen or so lines of code. The program consists of two basic parts: building the screen and creating the menus.

The functions that the menu choices invoke are simply shells. (The programmer would substitute the appropriate functionality.) The only functions that perform any tasks are the version function, which displays the current program version, and the exit function, which terminates the application (Listing 6) .

To run the application, compile menu.c, uilibs.c, and funcs.c with the appropriate defines for the host platform. Then link them together with the curses library provided by the package. When the user starts the program, the menu screen will appear.

Conclusion

The curses interface has its limitations. A commercial package written in curses will most likely not show up on the store shelves. However, if you need a reasonably efficient and portable method of creating user interfaces, curses is an option.