CodeBlock下的人机交互界面设计
人机交互界面指的是计算机系统与用户之间的接口。通过该接口,一方面,计算机系统向用户输出系统的运行状态、运行控制和运行结果等方面信息;另一方面,用户根据输出信息向系统输入相应的指令和数据等信息。
3.4.1 控制台窗口和屏幕缓冲区
控制台窗口是个二维平面空间,其坐标系统的原点(0, 0)设在窗口左上角,即窗口第一行第一列字符单元的位置。横轴(X轴)的正向沿原点向右,与窗口的第一行重合,每刻度为一个字符宽度;纵轴(Y轴)的正向沿原点向下,与窗口的第一列重合,每刻度为一个字符高度。窗口中每个字符单元对应一个二维坐标。比如,第5行第32列字符单元的坐标为(31, 4)。如图3.8所示。
图3.8 控制台窗口和屏幕缓冲区关系示意图
屏幕缓冲区是个二维数组,逻辑上可看作一个二维平面空间。数组第一个元素的下标[0][0]对应此平面空间坐标系统的原点(0, 0),数组第1维的下标对应坐标系统的纵坐标(Y坐标),第2维下标对应坐标系统的横坐标(X坐标)。屏幕缓冲区存放着M行N列字符单元的信息,M和N的大小由系统设置,并可以进行修改。
每个字符单元信息用一个CHAR_INFO结构类型的数据来表示,结构成员Char存放字符的码值(Unicode码或ASCII码,取决于系统所采用的字符集),结构成员Attributes存放字符的属性(字符显示所用的前景色和背景色)。
操作系统以一定的频率从屏幕缓冲区读取字符单元信息,并显示在控制台窗口中。应用程序的输出信息实际上输出到了屏幕缓冲区,由此改变了控制台窗口所显示的内容。初始状态下,屏幕缓冲区坐标系统与控制台窗口坐标系统重合,窗口中第m行第n列字符的码值和颜色值存放在屏幕缓冲区二维数组中下标为[m-1][n-1]的元素中。利用控制台函数可以改变这两个坐标系统的对应关系,实现特殊的显示效果。图3.8表示了控制台窗口和屏幕缓冲区的相互关系。
一个控制台可拥有多个屏幕缓冲区,但只有处于激活状态的屏幕缓冲区内容显示在控制台窗口中。操作系统在为进程创建控制台的同时会创建一个屏幕缓冲区。
进程可调用函数CreateConsoleScreenBuffer为其控制台创建另外的屏幕缓冲区。
调用函数SetConsoleActiveScreenBuffer可以将某个已有的屏幕缓冲区置为激活状态,使其内容显示在屏幕窗口中。
不管是否处于激活状态,屏幕缓冲区都可以通过句柄来进行读写操作,只不过激活状态下屏幕缓冲区的内容可以看到,非激活状态下看不到而已。
屏幕缓冲区相关的多个属性可以独立进行设置。激活的屏幕缓冲区属性值的变化能在控制台窗口中产生奇妙的外观效果。屏幕缓冲区相关的属性包括:
l 屏幕缓冲区大小,以字符行和列为单位;
l 文本属性(文本信息显示的前景色和背景色);
l 窗口大小和定位(控制台屏幕缓冲区在控制台窗口中显示时所处的矩形区域);
l 光标位置、外观和是否可见;
l 输出模式(控制字符的输出处理和行末换行处理)。
屏幕缓冲区在创建时,它所包含的字符内容初始化为空格,光标设为可见并定位在缓冲区原点(0, 0),而窗口的原点(左上角)与缓冲区原点置为重合。控制台屏幕缓冲区的大小、窗口的大小、文本属性和光标的外观取决于用户或系统的缺省设置。
为获取控制台屏幕缓冲区各种相关属性的当前值,可分别调用函数:
GetConsoleScreenBufferInfo;
GetConsoleCursorInfo;
GetConsoleMode。
屏幕缓冲区光标信息用CONSOLE_CURSOR_INFO结构类型的数据表示,成员bVisible表示光标是否可见,成员dwSize表示光标外观大小,取值范围为1~100。光标可见时,dwSize的取值从100变为1,光标外观大小从充满整个字符单元变为出现在单元底部的一条水平线。调用函数GetConsoleCursorInfo和SetConsoleCursorInfo分别可以获得和设置光标属性值。
由高级控制台I/O函数(如getchar,putchar,printf,scanf等)输出的字符将输出在光标当前位置,同时光标移动到下一个字符输出位置。
调用函数:
GetConsoleScreenBufferInfo和SetConsoleCursorPosition,
分别可以获得和设置光标在屏幕缓冲区坐标系统中的当前位置,由此可以控制高级I/O函数输出或回显字符的位置。
字符属性分为两类:颜色属性和DBCS(Double-Byte Character Set,双字节字符集)属性。表3.14中的符号常量在wincon.h头文件中进行定义。
表3.14 字符属性符号常量表
属性
含义
FOREGROUND_BLUE
文本颜色包含蓝色
FOREGROUND_GREEN
文本颜色包含绿色
FOREGROUND_RED
文本颜色包含红色
FOREGROUND_INTENSITY
文本颜色加亮
BACKGROUND_BLUE
背景含蓝色
BACKGROUND_GREEN
背景含绿色
BACKGROUND_RED
背景含红色
BACKGROUND_INTENSITY
背景加亮
COMMON_LVB_LEADING_BYTE
首字节
COMMON_LVB_TRAILING_BYTE
末字节
COMMON_LVB_GRID_HORIZONTAL
首行
COMMON_LVB_GRID_LVERTICAL
左列
COMMON_LVB_GRID_RVERTICAL
右列
COMMON_LVB_REVERSE_VIDEO
翻转前景及背景属性
COMMON_LVB_UNDERSCORE
下划线
前缀为FOREGROUND的常量值指定文本颜色(文本的前景色)。前缀为BACKGROUND的常量值指定用于填充字符单元背景的颜色。其他常量值用于DBCS属性。
应用程序可以将前景色和背景色常量值组合起来,获得不同颜色。例如,下面颜色组合的效果为蓝色背景上的亮青色文本。
FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY | BACKGROUND_BLUE
配色问题可以由实验的输出试验确定!
如果不指定背景颜色值,那么背景为黑色,而不指定前景颜色值,文本为黑色。例如,下面颜色组合将产生白色背景上的黑色文本效果。
BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED
每个屏幕缓冲区字符单元储存了在“画”该单元的文本(前景)和背景时所使用的颜色属性值。应用程序可以分别设置每个字符单元的颜色值,并将颜色值存储在每个单元CHAR_INFO结构类型数据的Attributes成员中。
3.4.2 在屏幕上指定位置输出信息
有多种方法在屏幕指定位置输出带属性的字符串信息,这里介绍其中四种基本方法。
(1) 用标准输出函数(putchar, printf, puts等)输出字符串信息;
//设置光标位置
SetConsoleCursorPosition(output_handle, new_pos);
//输出字符串string
printf(“%s”, string);
//在字符串输出位置填充指定的文本属性
FillConsoleOutputAttribute(output_handle, new_attributes, strlen(string), new_pos, NULL);
其中output_handle是屏幕缓冲区句柄,new_pos为COORD类型的变量,存放指定的光标位置坐标,new_attributes为WORD类型的变量,存放指定的文本属性值,string是字符数组,存放被输出的字符串。
(2) 用函数WriteConsole输出字符串信息;
//设置光标位置
SetConsoleCursorPosition (output_handle, new_pos);
//设置文本属性
SetConsoleTextAttribute(output_handle, new_attributes);
//输出字符串
WriteConsole(output_handle, string, strlen(string), NULL, NULL);
变量的含义同上。
(3) 用函数WriteConsoleOutputCharacter输出字符串信息;
//在指定位置填充与所输出字符串等长的文本属性值
FillConsoleOutputAttribute(output_handle, new_attributes, strlen(string), new_pos, NULL);
//在该指定位置输出字符串
WriteConsoleOutputCharacter(output_handle, string, strlen(string), new_pos, NULL);
(4) 用函数WriteConsoleOutput输出字符串信息;
CHAR_INFO * lpBuffer;
COORD pos = {0, 0};
COORD size = { strlen(string), 1};
SMALL_RECT area = {new_pos.X, new_pos.Y, new_pos.X+strlen(string)-1, new_pos.Y};
lpBuffer = (CHAR_INFO *)malloc(size.X * size.Y * sizeof(CHAR_INFO));
for(i=0; i<strlen(string); i++) {
lpBuffer->Char.AsciiChar = string[i];
lpBuffer->Attributes = new_attributes;
}
WriteConsoleOutput(output_handle, lpBuffer, size, pos, &area);
free(&area);
演示并解释例3.1 menu_ex3_1
这种方法使用起来相对复杂一些。基本思想是将输出信息当作一个矩形字符信息块,设置矩形块的大小size,矩形块在窗口中的输出位置area,将矩形块内字符信息存放在一个动态存储缓冲区lpBuffer内,字符信息包含了字符的码值和颜色属性,最后调用函数WriteConsoleOutput将lpBuffer中字符块信息写到控制台屏幕缓冲区指定位置。
3.4.3 弹出窗口的设计
屏幕窗口是个有限的信息显示区域。在文本字符界面下,控制台窗口的大小通常设为80个字符的宽度和25行字符的高度,即每屏可以显示2000个字符。
弹出窗口设计的基本思路是:
1、确定输出信息的屏幕位置和大小;
2、将新窗口弹出后所要覆盖的屏幕区域字符信息读入到一块内存缓冲区;
3、在新窗口内输出信息,模拟“弹出”效果。
4、弹出窗口内的操作完成后,把保存在内存缓冲区的字符信息写到其原来所在的屏幕位置,弹出窗口消失,屏幕恢复为窗口弹出之前的外观。
演示并解释例3.1 menu_ex3_2
多层弹出窗口自学
按照这一思路,可以实现多层弹出窗口。窗口的多层弹出和逐层关闭,给屏幕信息的维护带来了复杂性。为了便于处理,我们用链表来模拟堆栈,实现弹出窗口的栈式管理。
弹出窗口栈式管理用到以下结构类型。
typedef struct layer_node {
char LayerNo; //弹出窗口层数
SMALL_RECT rcArea; //弹出窗口区域坐标
CHAR_INFO *pContent; //弹出窗口区域字符单元原信息存储缓冲区
char *pScrAtt; //弹出窗口区域字符单元原属性值存储缓冲区
struct layer_node *next; //下一结点的地址
} LAYER_NODE;
利用这种结构类型的数据可以模拟出如图3.10所示的堆栈,对弹出窗口信息进行管理。
图3.10 弹出窗口信息堆栈
LAYER_NODE结构的5个成员分别表示了弹出窗口相关信息。LayerNo表示当前弹出窗口的层数;rcArea表示当前弹出窗口矩形区域的位置和大小;pContent指向的动态存储区存放被弹出窗口所覆盖区域的原字符单元信息,用于当前弹出窗口关闭后恢复原屏幕窗口信息;pScrAtt指向的动态存储区存放内容的用途与输入处理相关,将在下面的输入处理中进行介绍;next存放下层弹出窗口相关信息的地址。
堆栈栈顶由LAYER_NODE结构指针TopLayer来指示,初值为NULL。系统界面初始化完成之后,屏幕窗口看作第一层弹出窗口,窗口相关信息用LAYER_NODE结构类型的动态存储区存放后入栈,结构指针TopLayer指向栈顶;以后每弹出一层窗口,就执行一次入栈操作。关闭弹出窗口时,用TopLayer指向的LAYER_NODE结构类型数据恢复被覆盖的屏幕区域,将TopLayer指向下一层结点,释放用过的动态存储区,完成出栈操作。
3.4.4 键盘和鼠标输入信息的获取
ReadConsoleInput函数原型为:
BOOL WINAPI ReadConsoleInput(HANDLE hConsoleInput, PINPUT_RECORD
lpBuffer, DWORD nLength, LPDWORD lpNumberOfEventsRead);
功能:用来从控制台输入缓冲区读取输入数据,并将读出数据从输入缓冲区删除掉。
函数ReadConsoleInput被调用时,如果缓冲区中没有输入数据,函数将等待下去,直到读到至少一条记录后返回。读到的记录存放在参数lpBuffer所指向的内存单元。
记录用INPUT_RECORD结构类型的数据来表示;
成员EventType表示事件的类型;
成员Event是联合类型,存放事件的具体内容。
编程时,需要对EventType的值为KEY_EVENT(键盘输入)或MOUSE_EVENT(鼠标输入)的两类事件进行处理。
当EventType的值为KEY_EVENT时,Event的联合成员KeyEvent存放了按键相关信息。KeyEvent是KEY_EVENT_RECORD结构类型,其成员bKeyDown表明键是被按下(TRUE)还是被释放(FALSE),成员wRepeatCount表明按键重复的次数,成员wVirtualKeyCode存放按键的虚拟键码,成员wVirtualScanCode存放按键的虚拟扫描码,成员uChar存放按键的ASCII码或Unicode码(取决于系统所采用的字符集),成员dwControlKeyState表示有哪些控制键被同时按下。我们每按一次键会产生两个事件记录,一个记录表示键被按下,另一条记录表示键被释放。常用键的各种码值参见附录。
当EventType的值为MOUSE_EVENT时,Event的联合成员MouseEvent存放了鼠标操作相关信息。MouseEvent是MOUSE_EVENT_RECORD结构类型,其成员dwMousePosition存放了鼠标操作时的坐标位置,表明鼠标处于窗口中的某行和某列的字符单元位置上,成员dwButtonState表明鼠标哪些按钮被按下,取值可为下面符号常量之一或多个符号常量的组合值:
FROM_LEFT_1ST_BUTTON_PRESSED值为1,表示按下了鼠标最左边按钮;
RIGHTMOST_BUTTON_PRESSED值为2,表示按下了鼠标最右边按钮;
FROM_LEFT_2ND_BUTTON_PRESSED值为4,表示按下了鼠标左起第二个按钮;
FROM_LEFT_3RD_BUTTON_PRESSED值为8,表示按下了鼠标左起第三个按钮;
FROM_LEFT_4TH_BUTTON_PRESSED值为16,表示按下了鼠标左起第四个按钮。
成员dwControlKeyState表示在鼠标事件发生时有哪些控制键被同时按下,成员dwEventFlags表示鼠标事件的具体类型,取值为以下符号常量:
MOUSE_MOVED值为1,表示鼠标移动事件;
DOUBLE_CLICK值为2,表示鼠标双击事件;
MOUSE_WHEELED值为4,表示鼠标滚轮滚动事件。
3.4.5 输入处理(略)
(1) 键盘输入处理
按照表3.15进行处理,对其他按键不予响应。
表3.15 主界面下的键盘输入处理设计
按键
系统响应
F1
执行帮助菜单下的帮助主题子菜单对应功能模块
Alt+X
执行文件菜单下的退出系统子菜单对应功能模块
Alt+F
清除当前选中菜单项标记,标记文件菜单项并弹出文件菜单的子菜单
Alt+M
清除当前选中菜单项标记,标记数据维护菜单项并弹出数据维护菜单的子菜单
Alt+Q
清除当前选中菜单项标记,标记数据查询菜单项并弹出数据查询菜单的子菜单
Alt+S
清除当前选中菜单项标记,标记数据统计菜单项并弹出数据统计菜单的子菜单
Alt+H
清除当前选中菜单项标记,标记帮助菜单项并弹出帮助菜单的子菜单
向左←
清除当前选中菜单项标记,标记左侧菜单项
向右→
清除当前选中菜单项标记,标记右侧菜单项
向下↓
弹出当前菜单项的子菜单
f或F
清除当前选中菜单项标记,标记文件菜单项并弹出文件菜单的子菜单
m或M
清除当前选中菜单项标记,标记数据维护菜单项并弹出数据维护菜单的子菜单
q或Q
清除当前选中菜单项标记,标记数据查询菜单项并弹出数据查询菜单的子菜单
s或S
清除当前选中菜单项标记,标记数据统计菜单项并弹出数据统计菜单的子菜单
h或H
清除当前选中菜单项标记,标记帮助菜单项并弹出帮助菜单的子菜单
回车
弹出当前菜单项的子菜单
键盘输入处理时,要考虑输入处理的优先级,应先响应快捷键,再响应组合键,最后响应单键。以Alt组合键为例,判断组合键的方法为:
ReadConsoleInput(hIn, &inRec, 1, &res); //从输入缓冲区读取一条输入记录
if (inRec.EventType == KEY_EVENT && inRec.Event.KeyEvent.bKeyDown) {
//输入事件类别为KEY_EVENT,且事件由键被按下所触发
vkc = inRec.Event.KeyEvent.wVirtualKeyCode; //提取虚拟键码
asc = inRec.Event.KeyEvent.uChar.AsciiChar; //提取ASCII码
if (inRec.Event.KeyEvent.dwControlKeyState //如果左或右Alt键被按下
& (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED)) {
…… //进一步判断与Alt组合的另一个键,并做响应处理
}
…… //非Alt组合键的响应处理
}
常用控制键所对应的符号常量在wincon.h头文件中进行了定义,如表3.16所示。
表3.16 控制键对应符号常量表
键名
符号常量
值
键名
符号常量
值
右Alt
RIGHT_ALT_PRESSED
1
左Alt
LEFT_ALT_PRESSED
2
右Ctrl
RIGHT_CTRL_PRESSED
4
左Ctrl
LEFT_CTRL_PRESSED
8
左右Shift
SHIFT_PRESSED
16
数字锁定
NUMLOCK_ON
32
屏幕锁定
SCROLLLOCK_ON
64
大写锁定
CAPSLOCK_ON
128
(2) 鼠标输入处理
热区本是网页设计中用到的一个概念,是指网页上建有链接的区域。我们将热区这个概念借用到程序设计中人机交互界面的设计上来,用来指界面上需要对鼠标事件(包括鼠标移动、滚轮转动、双击和任意一到多个键的按下)产生反应的区域。
基于这一思想,可用一个字符(8个二进制位)来表示窗口中某个字符单元的属性。如图3.12所示,这8个二进制位分为三段:0~1比特为A1,2~5比特为A2,6~7比特为A3。A1的取值范围为0~3,用来表示字符单元的“高度”,即该字符单元上弹出窗口的层数,0表示字符单元处于系统主界面层,没有弹出窗口覆盖该字符单元,这样弹出窗口的层数最多可到3层;A2取值范围为0~15,用来表示字符单元的热区编号,0表示字符单元不属于热区,这样同一层上的热区最多可为15个;A3取值范围为0~3,在A2取值不为0时用来表示字符单元的热区类型,0代表按钮类型,1代表输入框类型,2代表下拉选框类型,3保留备用。不同类型的热区被鼠标击中时,系统可以分别进行处理。本课程设计中,字符单元的属性用8个二进制位来存放,刚好够用。在其他应用程序开发中,如果弹出菜单超过3层,或某层窗口中热区超过15个,或热区类型超过4类,可以考虑用16个二进制位来存放字符单元属性。
7
6
5
4
3
2
1
0
A3
A2
A1
图3.12 字符单元属性的表示
前面在介绍弹出窗口设计时,弹出窗口的栈式管理用到以下结构类型:
typedef struct layer_node {
char LayerNo; //弹出窗口层数
SMALL_RECT rcArea; //弹出窗口区域坐标
CHAR_INFO *pContent; //弹出窗口区域字符单元原信息存储缓冲区
char *pScrAtt; //弹出窗口区域字符单元原属性值存储缓冲区
struct layer_node *next; //下一结点的地址
} LAYER_NODE;
其中,结构成员pScrAtt用来指向一个动态存储区,该存储区存放被弹出窗口所覆盖字符单元的原先属性值,在弹出窗口关闭时用于恢复窗口弹出前屏幕字符单元的属性。
系统界面初始化完成之后,屏幕显示系统的主界面(如图3.9所示)。用一个字符数组ScrAtt存放屏幕上所有字符单元的属性值,字符单元的坐标X和Y与存放其属性值的数组元素下标n的关系为:
n = Y × 屏幕缓冲区的宽度 + X
主界面中只有5个主菜单项显示区域为热区,依次编号1~5,热区类型为按钮型。此后,如果有窗口弹出(弹出菜单也是弹出窗口),则将窗口所覆盖区域字符单元的信息和属性分别保存起来,执行弹出窗口信息入栈操作,然后在弹出窗口区域输出提示信息,将弹出窗口字符单元的属性值写入数组ScrAtt对应元素以设置热区。鼠标输入处理时,取鼠标所在字符单元的属性值,根据字符单元的层数、热区编号和热区类型,结合鼠标事件类型做出相应处理。当弹出窗口关闭时,用所保存的弹出窗口区域字符单元原先的属性值修改数组ScrAtt对应元素值,恢复窗口弹出前屏幕字符单元的属性,最后执行弹出窗口信息出栈操作。
3.4.6 菜单操作与系统功能函数的调用
按照概要设计,系统功能分为五个模块,各模块所包含的子模块共有22个,分别用22个函数实现相应功能。其中,数据加载函数和界面初始化函数只在系统启动时执行一次,以后不再执行。其余20个函数可以通过菜单操作或快捷键执行相应功能。
例3.3 在图3.9所示的主界面下,实现菜单操作与系统功能函数的调用。本例中给出了函数SysRun和函数ExeFunction的定义,分别用于菜单操作和系统功能函数的调用。例子中调用了例3.1和例3.2中的函数,而其余函数的定义没有给出。
#include "dorm.h"
void SysRun( )
{
INPUT_RECORD inRec;
DWORD res;
COORD pos = {0, 0};
BOOL bRet = TRUE;
int i, loc, num;
int cNo, cAtt; //cNo:字符单元层号, cAtt:字符单元属性
char vkc, asc; //vkc:虚拟键代码, asc:字符的ASCII码值
while (bRet) { // 循环
ReadConsoleInput(hIn, &inRec, 1, &res);
if (inRec.EventType == MOUSE_EVENT) {
pos = inRec.Event.MouseEvent.dwMousePosition; /* pos 是坐标 */
cNo = ScrAtt[pos.Y * ScrCol + pos.X] & 3;
cAtt = ScrAtt[pos.Y * ScrCol + pos.X] >> 2;
if (cNo == 0) {
if (cAtt > 0 && cAtt != SelMenu && TopLayer->LayerNo > 0) {
PopOff();
SelSMenu = 0;
PopMenu(cAtt);
}
}
else if (cAtt > 0) {
TagSMenu(cAtt);
}
if (inRec.Event.MouseEvent.dwButtonState
== FROM_LEFT_1ST_BUTTON_PRESSED) {
if (cNo == 0) {
if (cAtt > 0) {
PopMenu(cAtt);
}
else if (TopLayer->LayerNo > 0) {
PopOff();
SelSMenu = 0;
}
}
else {
if (cAtt > 0) {
PopOff();
SelSMenu = 0;
bRet = ExeFunction(SelMenu, cAtt);
}
}
}
else if (inRec.Event.MouseEvent.dwButtonState
== RIGHTMOST_BUTTON_PRESSED) {
if (cNo == 0) {
PopOff();
SelSMenu = 0;
}
}
}
else if (inRec.EventType == KEY_EVENT
&& inRec.Event.KeyEvent.bKeyDown) {
vkc = inRec.Event.KeyEvent.wVirtualKeyCode;
asc = inRec.Event.KeyEvent.uChar.AsciiChar;
//系统快捷键的处理
if (vkc == 112) { //F1键
if (TopLayer->LayerNo != 0) {
PopOff();
SelSMenu = 0;
}
bRet = ExeFunction(5, 1); //F1帮助主题
}
else if (inRec.Event.KeyEvent.dwControlKeyState
& (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED)) {
switch (vkc) { //组合键Alt+字母
case 88: if (TopLayer->LayerNo != 0) {
PopOff();
SelSMenu = 0;
}
bRet = ExeFunction(1,4); break; //Alt+X 退出
case 70: PopMenu(1); break; //Alt+F
case 77: PopMenu(2); break; //Alt+M
case 81: PopMenu(3); break; //Alt+Q
case 83: PopMenu(4); break; //Alt+S
case 72: PopMenu(5); break; //Alt+H
}
}
else if (asc == 0) { //方向键的处理
if (TopLayer->LayerNo == 0) { //未弹出子菜单时
switch (vkc) { //方向键(左、右、下)的处理
case 37: SelMenu--;
if (SelMenu == 0) {
SelMenu = 5;
}
TagMenu(SelMenu); break;
case 39: SelMenu++;
if (SelMenu == 6) {
SelMenu = 1;
}
TagMenu(SelMenu); break;
case 40: PopMenu(SelMenu);
TagSMenu(1); break;
}
}
else { //已弹出子菜单时
for (loc=0,i=1; i<SelMenu; i++) {
loc += cSMenu[i-1];
} //找到子菜单第一项在子菜单数组中的位置(下标)
switch (vkc) { //方向键(左、右、上、下)的处理
case 37: SelMenu--;
if (SelMenu < 1) {
SelMenu = 5;
}
TagMenu(SelMenu); PopOff();
PopMenu(SelMenu); TagSMenu(1);
break;
case 38: num = SelSMenu - 1;
if (num < 1) {
num = cSMenu[SelMenu-1];
}
if (strlen(SMenu[loc+num-1]) == 0) {
num--;
}
TagSMenu(num); break;
case 39: SelMenu++;
if (SelMenu > 5) {
SelMenu = 1;
}
TagMenu(SelMenu); PopOff();
PopMenu(SelMenu); TagSMenu(1);
break;
case 40: num = SelSMenu + 1;
if (num > cSMenu[SelMenu-1]) {
num = 1;
}
if (strlen(SMenu[loc+num-1]) == 0) {
num++;
}
TagSMenu(num); break;
}
}
}
else if ((asc-vkc == 0) || (asc-vkc == 32)){ //按下普通键
if (TopLayer->LayerNo == 0) { //未弹出子菜单时
switch (vkc) {
case 70: PopMenu(1); break; //f或F
case 77: PopMenu(2); break; //m或M
case 81: PopMenu(3); break; //q或Q
case 83: PopMenu(4); break; //s或S
case 72: PopMenu(5); break; //h或H
case 13: PopMenu(SelMenu); //回车
TagSMenu(1); break;
}
}
else { //已弹出子菜单时的键盘输入处理
if (vkc == 27) { //按下ESC键时, 关闭子菜单
PopOff();
SelSMenu = 0;
}
else if(vkc == 13) { //|| vkc == 32
num = SelSMenu;
PopOff();
SelSMenu = 0;
bRet = ExeFunction(SelMenu, num);
}
else {
for (loc=0,i=1; i<SelMenu; i++) {
loc += cSMenu[i-1];
}
for (i=loc; i<loc+cSMenu[SelMenu-1]; i++) {
if (strlen(SMenu[i])>0 && vkc==SMenu[i][1]) {
PopOff();
SelSMenu = 0;
bRet = ExeFunction(SelMenu, i-loc+1);
}
}
}
}
}
}
}
}
BOOL ExeFunction(int m, int s)
{
BOOL bRet = TRUE;
BOOL (*pFunction[cSMenu[0]+cSMenu[1]+cSMenu[2]+cSMenu[3]+cSMenu[4]])(void);
int i, loc;
//pFunction
pFunction[0] = DataSave;
pFunction[1] = DataBackup;
pFunction[2] = DataRestore;
pFunction[3] = SySexit;
pFunction[4] = MaintainSex;
pFunction[5] = MaintainType;
pFunction[6] = NULL;
pFunction[7] = MaintainDorm;
pFunction[8] = MaintainStu;
pFunction[9] = MaintainCharge;
pFunction[10] = QuerySex;
pFunction[11] = QueryType;
pFunction[12] = NULL;
pFunction[13] = QueryDorm;
pFunction[14] = QueryStu;
pFunction[15] = QueryCharge;
pFunction[16] = StatIn;
pFunction[17] = StatType;
pFunction[18] = StatCharge;
pFunction[19] = StatUncharge;
pFunction[20] = HelpTopic;
pFunction[21] = NULL;
pFunction[22] = AboutDorm;
for (i=1,loc=0; i<m; i++) {
loc += cSMenu[i-1];
}
loc += s - 1;
if (pFunction[loc] != NULL) {
bRet = (*pFunction[loc])();
}
return bRet;
}
演示并解释例3.1 menu_ex3_2