摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P373
键盘加速键是可以生成 WM_COMMAND(或者有时是 WM_SYSCOMMAND)消息的组合键。通常,程序使用键盘加速键来复制公共菜单选项的动作,但它们也可以执行非菜单功能。例如,某些 Windows 程序有一个含有 Delete 或 Clear 选项的 Edit 菜单;这些程序通常指定 Del 键作为这个选项的键盘加速键。用户可以按 Alt 组合键从菜单中选择 Delete 选项,也可以简单地按 Del 键来使用键盘加速键。当窗口过程接收到 WM_COMMAND 消息时,它不需要检测该命令是来自于键盘加速键。
10.3.1 为什么你应该使用键盘加速键
你可能会问:为什么我应该使用键盘加速键?为什么我不能捕捉 WM_KEYDOWN 或 WM_CHAR 消息并自己复制菜单功能?它有什么优点?对一个单窗口程序,你当然可以捕捉键盘消息。但是使用键盘加速键的一个简单优点是,你不需要重写菜单和键盘加速键处理逻辑。
对有多个窗口和窗口过程的应用程序,键盘加速键变得十分重要。正如我们所见,对拥有当前输入焦点的窗口,Windows 会将键盘消息发送给它的窗口过程。然而,对键盘加速键而言,Windows 会将 WM_COMMAND 消息发送给一个句柄在 Windows 函数 TranslateAccelerator 中指定的窗口过程。通常,这会是你的主窗口,亦即拥有菜单的那个窗口,这意味着响应键盘加速键的逻辑不需要被复制到每一个窗口过程。
如果你使用非模态对话框(在第 11 章讨论)或在主窗口的客户区内使用子窗口,这个优点就变得极其重要。如果给多个窗口定义了同一个键盘加速键,只需在其中一个窗口包含这个逻辑即可。子窗口不会接收到来自键盘加速键的 WM_COMMAND 消息。
10.3.2 指定加速键的一些规则
理论上,你可以使用几乎任何虚拟键或字符键与 Shift 键、Ctrl 键或 Alt 键的组合来定义键盘加速键。然而,你应该试图和其他应用程序保持一致,以避免干扰 Windows 对键盘的使用。你
应该避免使用Tab、回车键、Esc 和空格键作为键盘加速键,因为它们通常保留给系统功能。
系统加速键最通常的用途是用于程序中 Edit 菜单的菜单项。对这些菜单项推荐使用的键盘加速键在 Windows 3.0 和 Windows 3.1 之间有变化,因此同时支持新旧两种加速键很常见,如下表所示:
功 能 | 旧加速键 | 新加速键 |
---|---|---|
Undo(撤销) | Alt+Backspace | Ctrl+Z |
Cut(剪切) | Shift+Del | Ctrl+X |
Copy(复制) | Ctrl+Ins | Ctrl+C |
Paste(粘贴) | Shift+Ins | Ctrl+V |
Delete 或 Clear(删除) | Del | Del |
10.3.3 加速键表
你可以在 Developer Studio 中定义加速键表。为了方便在你的程序中加载加速键表,可以使它与应用程序同名(这也适用于菜单和图标)。
每个加速键有一个 ID 和一个击键组合,这些你可以在 Accelerator Properties 对话框中定义。如果你已经定义了菜单,Aidan DI 会存在与组合框中,你不需要重新输入它们。
加速键可以是虚拟键代码或 ASCII 字符与 Shift、Ctrl 或 Alt 键的组合。你可以在字母前加^来指定 ASCII 字符与 Ctrl 键的组合。你也可以从组合框中选择虚拟键代码。
在为菜单项定义键盘加速键时,应当在菜单文本中包含相应的键组合。Tab 字符(\t)能将文本和加速键分开,这样加速键会排列在第二列。为了在菜单中标识加速键,可以使用文字 Ctrl、Shift 或 Alt,后面跟着一个加号和键名(例如 Shift+F6 或 Ctrl+F6)。
10.3.4 加载加速键表
在应用程序中,应使用 LoadAccelerators 函数来把加速键表加载到内存并获得它的句柄。LoadAccelerators 语句和 LoadIcon、LoadCursor 及 LoadMenu 语句很相似。首先用类型 HANDLE 定义一个加速键表的句柄:
HANDLE hAccel;
然后加载加速键表:
hAccel = LoadAccelerators (hInstance, TEXT("MyAccelerators"));
和图标、鼠标指针及菜单一样,你也可以用数字来命名加速键表,然后在 LoadAccelerators 中通过 MAKEINTRESOURCE 宏来使用该数字,或者将数字用引号括起来并加上前缀#字符。
10.3.5 翻译按键
我们现在要改动三行代码,这三行代码是本书迄今为止对所有 Windows 程序都通用的。这三行代码就是标准消息循环:
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg);
DispatchMessage (&msg);
}
下面显示我们如何改变它来使用键盘加速键表:
while (GetMessage (&msg, NULL, 0, 0))
{
if (!TranslateAccelerator (hwnd, hAccel, &msg))
{
<pre name="code" class="cpp"> TranslateMessage (&msg);
DispatchMessage (&msg);
}}
TranslateAccelerator 函数确定保存在 msg 消息结构中的消息是否是键盘消息。如果是,该函数在加速键表中寻找句柄为 hAccel 的匹配值。如果找到匹配值,它会调用句柄为 hwnd 的窗口过程。如果键盘加速键 ID 对应系统菜单的一个菜单项,则相应消息为 WM_SYSCOMMAND;否则,消息是 WM_COMMAND。
当 TranslateAccelerator 返回时,如果消息被翻译过(并且已被发送给窗口过程),则返回值为非零值,否则返回值为零。如果 TranslateAccelerator 返回非零值,你就不应该再调用 TranslateMessage 和 DispatchMessage,而是应该返回 GetMessage 循环。
TranslateAccelerator 中的 hwnd 参数看起来不太合适,因为消息循环中的三个函数都不需要它。并且,消息结构自身(结构变量 msg)有一个名字为 hwnd 的成员,它也是一个窗口句柄。
这个函数之所以有些不同的原因是:msg 结构的字段是由 GetMessage 调用来填充的。当 GetMessage 的第二个参数是 NULL 时,它检索属于该应用程序的所有窗口的消息。当 GetMessage 返回时,msg 结构的 hwnd 成员是将会得到该消息的窗口句柄。然而,当 TranslateAccelerator 将键盘消息翻译成 WM_COMMAND 或 WM_SYSCOMMAND 消息时,它将 msg.hwnd 窗口句柄替换成该函数的第一个参数所指定的窗口句柄。于是 Windows 会将所有键盘加速键消息发送给同一个窗口,即使程序中另外一个窗口当前拥有输入焦点。当模态对话框或消息框拥有输入焦点时,TranslateAccelerator 不翻译键盘消息,因为这些窗口的消息不通过程序的消息循环。
某些情况下,当应用程序中另一个窗口(比如非模态对话框)拥有输入焦点时,你并不想翻译键盘消息。如何处理这种情况将在第 11 章介绍。
10.3.6 接收加速键消息
当一个键盘加速键对应系统菜单的一个菜单项时,TranslateAccelerator 会向窗口过程发送一条 WM_SYSCOMMAND 消息;否则,TranslateAccelerator 向窗口过程发送一条 WM_COMMAND 消息。下表列出了对于键盘加速键、菜单命令和子窗口控件你会接收到的 WM_COMMAND 消息的类型:
加 速 键 | 菜 单 | 控 件 | |
---|---|---|---|
LOWORD(wParam) | 加速键 ID | 菜单 ID | 控件 ID |
HIWORD(wParam) | 1 | 0 | 通知码 |
lParam | 0 | 0 | 子窗口句柄 |
如果当前窗口被最小化,对于映射到启用的系统菜单项的键盘加速键,TranslateAccelerator 将向窗口过程发送 WM_SYSCOMMAND 消息而不是 WM_COMMAND 消息。对于没有映射到任何菜单项的加速键,TranslateAccelerator 也会向窗口过程发送 WM_COMMAND 消息。
10.3.7 带有菜单和加速键的 POPPAD 程序
在第 9 章,我们创建了 POPPAD1 程序,它使用一个子窗口编辑空间来实现了一个简单的记事本(notepad)。在本章,我们将加入 File 和 Edit 菜单,并相应将程序称为 POPPAD2。Edit 中菜单项都能功能;File 菜单功能我们会在第 11 章实现,Print 功能在第 13 章实现。POPPAD2 程序如图 10-11 所示。/*--------------------------------------------------------
POPPAD2.C -- Popup Editor Version 2 (includes menu)
(c) Charles Petzold, 1998
--------------------------------------------------------*/
#include <windows.h>
#include "resource.h"
#define ID_EDIT 1
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
TCHAR szAppName[] = TEXT("PopPad2");
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
HACCEL hAccel;
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(hInstance, szAppName);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = szAppName;
wndclass.lpszClassName = szAppName;
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(szAppName, szAppName,
WS_OVERLAPPEDWINDOW,
GetSystemMetrics(SM_CXSCREEN) / 4,
GetSystemMetrics(SM_CYSCREEN) / 4,
GetSystemMetrics(SM_CXSCREEN) / 2,
GetSystemMetrics(SM_CYSCREEN) / 2,
NULL, NULL, hInstance, NULL);
hAccel = LoadAccelerators(hInstance, szAppName);
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(hwnd, hAccel, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
int AskConfirmation (HWND hwnd)
{
return MessageBox(hwnd, TEXT("Really want to close PopPad2?"),
szAppName, MB_YESNO | MB_ICONQUESTION);
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HWND hwndEidt;
int iSelect, iEnable;
switch (message)
{
case WM_CREATE:
hwndEidt = CreateWindow(TEXT("edit"), NULL,
WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL |
WS_BORDER | ES_LEFT | ES_MULTILINE |
ES_AUTOHSCROLL | ES_AUTOVSCROLL,
0, 0, 0, 0, hwnd, (HMENU)ID_EDIT,
((LPCREATESTRUCT)lParam)->hInstance, NULL);
return 0;
case WM_SETFOCUS:
SetFocus(hwndEidt);
return 0;
case WM_SIZE:
MoveWindow(hwndEidt, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
return 0;
case WM_INITMENUPOPUP:
if (lParam == 1)
{
EnableMenuItem((HMENU)wParam, IDM_EDIT_UNDO,
SendMessage(hwndEidt, EM_CANUNDO, 0, 0) ? MF_ENABLED : MF_GRAYED);
EnableMenuItem((HMENU)wParam, IDM_EDIT_PASTE,
IsClipboardFormatAvailable(CF_TEXT) ? MF_ENABLED : MF_GRAYED);
iSelect = SendMessage(hwndEidt, EM_GETSEL, 0, 0);
if (HIWORD(iSelect) == LOWORD(iSelect))
iEnable = MF_GRAYED;
else
iEnable = MF_ENABLED;
EnableMenuItem((HMENU)wParam, IDM_EDIT_CUT, iEnable);
EnableMenuItem((HMENU)wParam, IDM_EDIT_COPY, iEnable);
EnableMenuItem((HMENU)wParam, IDM_EDIT_CLEAR, iEnable);
return 0;
}
break;
case WM_COMMAND:
if (lParam)
{
if (LOWORD(wParam) == ID_EDIT)
if (HIWORD(wParam) == EN_ERRSPACE ||
HIWORD(wParam) == EN_MAXTEXT)
MessageBox(hwnd, TEXT("Edit control out of space."),
szAppName, MB_OK | MB_ICONSTOP);
}
else switch (LOWORD (wParam))
{
case IDM_FILE_NEW:
case IDM_FILE_OPEN:
case IDM_FILE_SAVE:
case IDM_FILE_SAVE_AS:
case IDM_FILE_PRINT:
MessageBeep(0);
return 0;
case IDM_APP_EXIT:
SendMessage(hwnd, WM_CLOSE, 0, 0);
return 0;
case IDM_EDIT_UNDO:
SendMessage(hwndEidt, WM_UNDO, 0, 0);
return 0;
case IDM_EDIT_CUT:
SendMessage(hwndEidt, WM_CUT, 0, 0);
return 0;
case IDM_EDIT_COPY:
SendMessage(hwndEidt, WM_COPY, 0, 0);
return 0;
case IDM_EDIT_PASTE:
SendMessage(hwndEidt, WM_PASTE, 0, 0);
return 0;
case IDM_EDIT_CLEAR:
SendMessage(hwndEidt, WM_CLEAR, 0, 0);
return 0;
case IDM_EDIT_SELECT_ALL:
SendMessage(hwndEidt, EM_SETSEL, 0, -1);
return 0;
case IDM_APP_HELP:
MessageBox(hwnd, TEXT("Help not yet implemented!"),
szAppName, MB_OK | MB_ICONEXCLAMATION);
return 0;
case IDM_APP_ABOUT:
MessageBox(hwnd, TEXT("POPPAD2 (c) Charles Petzold, 1998"),
szAppName, MB_OK | MB_ICONINFORMATION);
return 0;
}
break;
case WM_CLOSE:
if (IDYES == AskConfirmation(hwnd))
DestroyWindow(hwnd);
return 0;
case WM_QUERYENDSESSION:
if (IDYES == AskConfirmation(hwnd))
return 1;
else
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
POPPAD2.RC (excerpts)
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
/
//
// Icon
//
POPPAD2 ICON "POPPAD2.ICO"
/
//
// Menu
//
POPPAD2 MENU
BEGIN
POPUP "&File"
BEGIN
MENUITEM "&New", IDM_FILE_NEW
MENUITEM "&Open...", IDM_FILE_OPEN
MENUITEM "&Save", IDM_FILE_SAVE
MENUITEM "Save &As...", IDM_FILE_SAVE_AS
MENUITEM SEPARATOR
MENUITEM "&Print", IDM_FILE_PRINT
MENUITEM SEPARATOR
MENUITEM "E&xit", IDM_APP_EXIT
END
POPUP "&Edit"
BEGIN
MENUITEM "&Undo\tCtrl+Z", IDM_EDIT_UNDO
MENUITEM SEPARATOR
MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT
MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY
MENUITEM "&Paste\tCtrl+V", IDM_EDIT_PASTE
MENUITEM "De&lete\tDel", IDM_EDIT_CLEAR
MENUITEM SEPARATOR
MENUITEM "&Select All", IDM_EDIT_SELECT_ALL
END
POPUP "&Help"
BEGIN
MENUITEM "&Help...", IDM_APP_HELP
MENUITEM "&About PopPad2...", IDM_APP_ABOUT
END
END
/
//
// Accelerator
//
POPPAD2 ACCELERATORS
BEGIN
VK_BACK, IDM_EDIT_UNDO, VIRTKEY, ALT, NOINVERT
VK_DELETE, IDM_EDIT_CLEAR, VIRTKEY, NOINVERT
VK_DELETE, IDM_EDIT_CUT, VIRTKEY, SHIFT, NOINVERT
VK_F1, IDM_APP_HELP, VIRTKEY, NOINVERT
VK_INSERT, IDM_EDIT_PASTE, VIRTKEY, CONTROL, NOINVERT
VK_INSERT, IDM_EDIT_PASTE, VIRTKEY, SHIFT, NOINVERT
"^C", IDM_EDIT_COPY, ASCII, NOINVERT
"^V", IDM_EDIT_PASTE, ASCII, NOINVERT
"^X", IDM_EDIT_CUT, ASCII, NOINVERT
"^Z", IDM_EDIT_UNDO, ASCII, NOINVERT
END
RESOURCE.H (excerpts)
// Microsoft Visual C++ 生成的包含文件。
// 供 PopPad2.rc 使用
//
#define IDR_ACCELERATOR1 102
#define IDI_ICON1 104
#define IDM_FILE_NEW 40001
#define IDM_FILE_OPEN 40002
#define IDM_FILE_SAVE 40003
#define IDM_FILE_SAVE_AS 40004
#define IDM_FILE_PRINT 40005
#define IDM_APP_EXIT 40006
#define IDM_EDIT_UNDO 40007
#define IDM_EDIT_CUT 40008
#define IDM_EDIT_COPY 40009
#define IDM_EDIT_PASTE 40010
#define IDM_EDIT_CLEAR 40011
#define IDM_EDIT_SELECT_ALL 40012
#define IDM_APP_HELP 40013
#define IDM_APP_ABOUT 40014
POPPAD2.ICO
POPPAD2.RC 资源脚本文件包含菜单和加速键表。你会注意到,加速键都在 Edit 弹出菜单的字符串中指定,并跟随\t字符之后。
10.3.8 启用菜单项
窗口过程的主要任务现在包括了启用和变灰 Edit 菜单中的菜单项,这是在处理 WM_INITMENUPOPUP 消息时完成的。首先,程序检查 Edit 弹出菜单是否将要被显示。由于 Edit 在菜单中的位置索引是 1(开始的 File 菜单项为 0),所以如果 Edit 弹出菜单将要被显示,lParam 将等于 1。
为了检查 Undo 菜单项是否可以被启用,POPPAD2 发送 EM_CANUNDO 消息给编辑控件。如果编辑控件可以执行 Undo 操作,SendMessage 会返回非零值,这时该选项被启用;否则,该选项变灰:
EnableMenuItem((HMENU)wParam, IDM_EDIT_UNDO,
SendMessage(hwndEidt, EM_CANUNDO, 0, 0) ? MF_ENABLED : MF_GRAYED);
只有当剪贴板当前包含文本时,Paste 菜单项才应该被启用。我们可以调用 IsClipboardFormatAvailable 并传递 CF_TEXT 标识符来检测:
EnableMenuItem((HMENU)wParam, IDM_EDIT_PASTE,
IsClipboardFormatAvailable(CF_TEXT) ? MF_ENABLED : MF_GRAYED);
只有在编辑控件中有文本被选中时,Cut、Copy 和 Delete 选项才应该被启用。向编辑控件发送 EM_GETSEL 消息会返回一个包含该信息的整数值:
iSelect = SendMessage(hwndEdit, EM_GETSEL, 0, 0);
iSelect 的低位字是第一个被选中字符的位置,高位字是紧随选中文本后面的第一个字符的位置。如果这两个字相等,则表明没有文本被选中:
if (HIWORD(iSelect) == LOWORD(iSelect))
iEnable = MF_GRAYED;
else
iEnable = MF_ENABLED;
iEnable 的值随后可以用于 Cut、Copy 和 Delete 菜单项:
EnableMenuItem((HMENU)wParam, IDM_EDIT_CUT, iEnable);
EnableMenuItem((HMENU)wParam, IDM_EDIT_COPY, iEnable);
EnableMenuItem((HMENU)wParam, IDM_EDIT_CLEAR, iEnable);
10.3.9 处理菜单项
当然,如果我们没有在 POPPAD2 中使用子窗口编辑控件,我们将面临一个问题,也就是说要具体实现 Edit 菜单中的 Undo、Cut、Copy、Paste、Clear 和 Select All 菜单。但是编辑控件简化了这个过程,因为我们只需要对应每一个菜单项向编辑控件发送一个消息:
case IDM_EDIT_UNDO:
SendMessage(hwndEidt, WM_UNDO, 0, 0);
return 0;
case IDM_EDIT_CUT:
SendMessage(hwndEidt, WM_CUT, 0, 0);
return 0;
case IDM_EDIT_COPY:
SendMessage(hwndEidt, WM_COPY, 0, 0);
return 0;
case IDM_EDIT_PASTE:
SendMessage(hwndEidt, WM_PASTE, 0, 0);
return 0;
case IDM_EDIT_CLEAR:
SendMessage(hwndEidt, WM_CLEAR, 0, 0);
return 0;
case IDM_EDIT_SELECT_ALL:
SendMessage(hwndEidt, EM_SETSEL, 0, -1);
return 0;
请注意,我们可以进一步简化这个过程,即让 IDM_EDIT_UNDO、IDM_EDIT_CUT 等的值等于相对应的窗口消息 WM_UNDO、WM_CUT 等的值,以此类推。
File 弹出菜单中的 About 菜单项引发一个简单的消息框:
case IDM_APP_ABOUT:
MessageBox(hwnd, TEXT("POPPAD2 (c) Charles Petzold, 1998"),
szAppName, MB_OK | MB_ICONINFORMATION);
return 0;
在第 11 章中,我们会把它变成一个对话框。当你从该菜单中选择 Help 菜单项或按键盘加速键 F1 时,也会引发一个消息框。
Exit 菜单项向窗口过程发送一条 WM_CLOSE 消息:
case IDM_APP_EXIT:
SendMessage(hwnd, WM_CLOSE, 0, 0);
return 0;
这正是 DefWindowProc 收到 wParam 为 SC_CLOSE 的 WM_SYSCOMMAND 消息时所作出的反应。
在以前的程序中,我们没有在窗口过程中处理 WM_CLOSE 消息,而是简单地将它传递给 DefWindowProc。DefWindowProc 对 WM_CLOSE 只简单地做了一件事:调用 DestroyWindow 函数。POPPAD2 则对 WM_CLOSE 消息进行了处理,而不是将它发送给 DefWindowProc。(这个事实现在不是很重要,但在第 11 章中当 POPPAD 能真正地编辑文件时,将变得非常重要。)
case WM_CLOSE:
if (IDYES == AskConfirmation(hwnd))
DestroyWindow(hwnd);
return 0;
AskConfirmation 是 POPPAD2 中的一个函数,它显示一个消息框,要求用户对关闭程序进行确认:
int AskConfirmation (HWND hwnd)
{
return MessageBox(hwnd, TEXT("Really want to close PopPad2?"),
szAppName, MB_YESNO | MB_ICONQUESTION);
}
当 Yes 按钮被选中时,消息框(以及 AskConfirmation 函数)返回 IDYES。只有这时 POPPAD2 才调用 DestroyWindow。否则,这个程序不会终止。
如果想再结束程序前得到确认,还必须处理 WM_QUERYENDSESSION 消息。当用户选择关闭 Windows 时,Windows 开始向每一个窗口过程发送 WM_QUERYENDSESSION 消息。如果有任何一个窗口过程对此消息返回 0,Windows 将不会被终止。下面的代码显示了如何处理 WM_QUERYENDSESSION 消息:
case WM_QUERYENDSESSION:
if (IDYES == AskConfirmation(hwnd))
return 1;
else
return 0;
如果想要在结束程序之前获得用户的认可,WM_CLOSE 和 WM_QUERYENDSESSION 消息是唯一两个必须要处理的消息。这就是为什么我们让 POPPAD2 中的 Exit 菜单项向窗口过程发送 WM_CLOSE 的原因。这样,我们避免了在第三处的确认。
如果要处理 WM_QUERYENDSESSION 消息,那么你可能也会对 WM_ENDSESSION 敢兴趣。Windows 向每一个曾接收过 WM_QUERYENDSESSION 消息的窗口发送此消息。如果由于其他程序从 WM_QUERYENDSESSION 消息返回 0 而导致终止操作失败,wParam 将被设为 0。WM_ENDSESSION 消息本质上回答了这样一个问题:我告诉 Windows 可以终止我,但是我真的被终止了吗?
虽然我在 POPPAD2 的 File 菜单中包含了常见的 New、Open、Save 和 Save As 等菜单项,但它们当前并不坐任何事情。要处理这些命令,我们需要使用对话框。现在你已经准备好了,开始学习第 11 章,研究对话框吧!