11.2 非模态对话框

摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P413

        在本章开始,我解释了对话框可以是“模态”或“非模态”的。到目前为止,我们一直在关注模态对话框,它是两种类型中较常见的。模态对话框(除了系统模态对话框外)允许用户在对话框和其他程序之间切换。但是,用户无法切换到同一程序的另一个窗口中,直到该模态对话框被销毁。非模态对话框则允许用户在对话框和窗口之间,以及在对话框和其他程序之间进行切换。非模态对话框因此更接近于你的程序产生的正常弹出窗口。

        如果在一段时间内,一直要显示某个对话框,用户更倾向于使用非模态对话框,因为它更方便。举例来说,字处理程序经常使用非模态对话框来实现文本查找修改对话框。如果查找对话框是一个模态对话框,用户将不得不从菜单中选择查找,输入想找的字符串,结束对话框以返回到文档,然后重复整个过程查找同一字符串的另一实例。允许在文档和对话框之间进行切换使用户的操作更方便。

        正如你已经看到的,使用 DialogBox 能产生模态对话框。只有对话框销毁后,函数才返回一个值。它返回的值是 EndDialog 的第二个参数所指定的,对话框过程使用该函数来终止对话框。调用 CreateDialog 可以创建非模态对话框。此函数的参数与 DialogBox 相同:

hDlgModeless = CreateDialog (hInstance, szTemplate, hwndParent, DialogProc);
不同的是CreateDialog 函数会立即返回对话框的窗口句柄。通常,你把这个窗口句柄存储在一个全局变量中

        虽然把 DialogBox 这个名字与模态对话框、把 CreateDialog 这个名字与非模态对话框联系起来看没有什么规律,但通过记住非模态对话框类似一个正常窗口,你就能记住哪个是哪个了。CreateDialog 应该使你想到 CreateWindow 函数,那个用来创建正常窗口的函数。

11.2.1  模态与非模态对话框的区别

        非模态对话框与模态对话框的原理是类似的,但也有一些重要的不同。

        首先,非模态对话框通常包括标题栏和系统菜单框。当你在 Developer Studio 中创建一个对话框时,这实际上时默认选项。非模态对话框模板的样式语句如下:

STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_VISIBLE
标题栏和系统菜单允许用户使用鼠标或键盘把非模态对话框移动至其他地方。你通常不会为模态对话框提供一个标题栏和系统菜单,因为用户对它下面的窗口根本无法做任何事情。

        第二个显著差异:请注意,WS_VISIBLE 样式被包含在我们的样式语句的例子中。在Developer Studio 中,请在 Dialog Properties 对话框的 More Styles 选项卡中选中此选项。如果你省略了 WS_VISIBLE,则必须在调用 CreateDialog 后调用 ShowWindow:

hDlgModeless = CreateDialog ( ... );
ShowWindow (hDlgModeless, WS_SHOW);
如果你既不包括 WS_VISIBLE 也不调用 ShowWindow,那么该非模态对话框将不会被显示。掌握了模态对话框的程序员往往忽视这一特殊性,因此经常在第一次试图创建一个非模态对话框时遇到困难。

        第三个差异:与模态对话框和消息框的消息不同的是,非模态对话框的消息要进入你程序的消息队列。而消息队列必须经过改动才能把这些消息传递给对话框窗口过程。具体做法如下:使用 CreateDialog 创建一个非模态对话框时,把该函数返回的对话框句柄保存到一个全局变量(例如 hDlgModeless)。如下更改消息循环:

while (GetMessage (&msg, NULL, 0, 0))
{
    if (hDlgModeless == 0 || !IsDialogMessage (hDlgModeless, &msg))
    {
        TranslateMessage (&msg);
        DispatchMessage (&msg);
    }
}
如果消息是针对非模态对话框的,IsDialogMessage 就会将其发送到对话框窗口过程并返回 TRUE(非零),否则返回 FALSE(0)。只有当 hDlgModeless 为 0 或者该消息不是给对话框的才应该调用 TranslateMessage 和 DispatchMessage 函数。如果对程序窗口还使用了键盘加速键,那么消息循环看起来会像下面这样:

while (GetMessage (&msg, NULL, 0, 0))
{
    if (hDlgModeless == 0 || !IsDialogMessage (hDlgModeless, &msg))
    {
        if (!TranslateAccelerator (hwnd, hAccel, &msg))
        {
            TranslateMessage (&msg);
            DispatchMessage (&msg);
        }
    }
}
因为全局变量初始化为 0,所以 hDlgModeless 将保持为 0 直到对话框被创建为止,从而确保了 IsDialgoMessage 不会在使用无效窗口句柄的情况下被调用。当你销毁非模态对话框时,你必须采取同样的预防措施,原因下面会有解释。

        hDlgModeless 变量也可以用在程序的其他地方,用来检测非模态对话框是否存在。例如,当 hDlgModeless 不等于 0 时,程序中的其他窗口可以将消息发送到该对话框。

        最后的大区别:请使用 DestroyWindow 而不是 EndDialog 来结束非模态对话框。当你调用 DestroyWindow 时,还要把 hDlgModeless 全局变量设置为 NULL。

        用户习惯于从系统菜单中选择 Close 来终止非模态对话框。虽然 Close 选项是可用的,Windows 内的对话框窗口过程并不处理 WM_CLOSE 消息。你必须在对话框过程中自己完成这个任务:

case WM_CLOSE:
    DestroyWindow (hDlg);
    hDlgModeless = NULL;
    break;
请注意这两个窗口句柄的区别:DestroyWindow 中的 hDlg 参数是传递给对话框过程的参数:hDlgModeless 则是从 CreateDialog 返回的、用来在消息循环中进行检测的全局变量。

        也可以允许用户使用按钮来关闭非模态对话框。对此应使用和 WM_CLOSE 消息相同的逻辑。任何对话框必须”返回“给创建它的窗口的信息,都可以存储在全局变量中,如果不希望使用全局变量,那么如前所述,可以使用 CreateDialogParam 来创建非模态对话框,并给它传递一个指向结构的指针。

11.2.2  新的 COLORS 程序

        第 9 章中所描述的 COLORS1 程序创建了 9 个子窗口来显示 3 个滚动条和 6 个文本项。在那个时候,该程序是我们开发的比较复杂的一个。而把 COLORS1 转换成使用非模态对话框使程序变得极其简单,尤其是其中的 WndProc 函数。修订后的 COLORS2 程序如图 11-7 所示。

/*--------------------------------------------------------
    COLORS2.C -- Version using Modeless Dialog Box
                (c) Charles Petzold, 1998
--------------------------------------------------------*/

#include 
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL    CALLBACK ColorScrDlg(HWND, UINT, WPARAM, LPARAM);

HWND hDlgModeless;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    PSTR szCmdLine, int iCmdShow)
{
    static TCHAR szAppName[] = TEXT("Colors2");
    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(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = CreateSolidBrush(0L);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szAppName;

    if (!RegisterClass(&wndclass))
    {
        MessageBox(NULL, TEXT("This program requires Windows NT!"),
            szAppName, MB_ICONERROR);
        return 0;
    }
    
    hwnd = CreateWindow(szAppName, TEXT("Color Scroll"),
        WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
        CW_USEDEFAULT, CW_USEDEFAULT,
        CW_USEDEFAULT, CW_USEDEFAULT,
        NULL, NULL, hInstance, NULL);

    ShowWindow(hwnd, iCmdShow);
    UpdateWindow(hwnd);

    hDlgModeless = CreateDialog(hInstance, TEXT("ColorScrDlg"), hwnd, ColorScrDlg);

    while (GetMessage(&msg, NULL, 0, 0))
    {
        if (hDlgModeless == 0 || !IsDialogMessage(hDlgModeless, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_DESTROY:
        DeleteObject((HGDIOBJ)SetClassLong(hwnd, GCL_HBRBACKGROUND, (LONG)GetStockObject(WHITE_BRUSH)));
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, message, wParam, lParam);
}

BOOL CALLBACK ColorScrDlg(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    static int    iColor[3];
    HWND        hwndParent, hCtrl;
    int            iCtrlID, iIndex;

    switch (message)
    {
    case WM_INITDIALOG:
        for (iCtrlID = 10; iCtrlID < 13; iCtrlID++)
        {
            hCtrl = GetDlgItem(hDlg, iCtrlID);
            SetScrollRange(hCtrl, SB_CTL, 0, 255, FALSE);
            SetScrollPos(hCtrl, SB_CTL, 0, FALSE);
        }
        return TRUE;

    case WM_VSCROLL:
        hCtrl = (HWND)lParam;
        iCtrlID = GetWindowLong(hCtrl, GWL_ID);
        iIndex = iCtrlID - 10;
        hwndParent = GetParent(hDlg);

        switch (LOWORD(wParam))
        {
        case SB_PAGEDOWN:
            iColor[iIndex] += 15;        // fall through
        case SB_LINEDOWN:
            iColor[iIndex] = min(255, iColor[iIndex] + 1);
            break;
        case SB_PAGEUP:
            iColor[iIndex] -= 15;        // fall through
        case SB_LINEUP:
            iColor[iIndex] = max(0, iColor[iIndex] - 1);
            break;
        case SB_TOP:
            iColor[iIndex] = 0;
            break;
        case SB_BOTTOM:
            iColor[iIndex] = 255;
            break;
        case SB_THUMBPOSITION:
        case SB_THUMBTRACK:
            iColor[iIndex] = HIWORD(wParam);
            break;
        default:
            return FALSE;
        }
        SetScrollPos(hCtrl, SB_CTL, iColor[iIndex], TRUE);
        SetDlgItemInt(hDlg, iCtrlID + 3, iColor[iIndex], FALSE);

        DeleteObject((HGDIOBJ)SetClassLong(hwndParent, GCL_HBRBACKGROUND, 
                            (LONG)CreateSolidBrush( RGB(iColor[0], iColor[1], iColor[2]))));
        InvalidateRect(hwndParent, NULL, TRUE);
        return TRUE;
    }
    return FALSE;
}
COLORS2.RC (节选)

// Microsoft Visual C++ generated resource script.
//
#include "resource.h"

/
//
// Dialog
//

COLORSCRDLG DIALOGEX 16, 16, 120, 141
STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION
CAPTION "Color Scroll Scrollbars"
FONT 8, "MS Sans Serif", 0, 0, 0x1
BEGIN
    CTEXT           "&Red",IDC_STATIC,8,8,24,8,NOT WS_GROUP
    SCROLLBAR        10, 8, 20, 24, 100, SBS_VERT | WS_TABSTOP
    CTEXT            "0", 13, 8, 124, 24, 8, NOT WS_GROUP
    CTEXT           "&Green", IDC_STATIC, 48, 8, 24, 8, NOT WS_GROUP
    SCROLLBAR        11, 48, 20, 24, 100, SBS_VERT | WS_TABSTOP
    CTEXT            "0", 14, 48, 124, 24, 8, NOT WS_GROUP
    CTEXT           "&Blue", IDC_STATIC, 89, 8, 24, 8, NOT WS_GROUP
    SCROLLBAR        12, 89, 20, 24, 100, SBS_VERT | WS_TABSTOP
    CTEXT            "0", 15, 89, 124, 24, 8, NOT WS_GROUP
END
RESOURCE.H (节选)

// Microsoft Visual C++ 生成的包含文件。
// 供 Colors2.rc 使用
//
#define IDC_STATIC                     -1

        尽管最初的 COLORS1 程序会基于窗口的大小来显示滚动条,但新版本能使它们在非模态对话框中保持大小不变,如图 11-8 所示。

11.2 非模态对话框_第1张图片

图 11-8  COLORS2 的显示

        创建对话框模板时,请为三个滚动条使用 ID 值 10、11 和 12,13、14 和 15 则用于显示滚动条当前值的三个静态文本字段。给每个滚动条设定 Tab 停靠位样式,并从 6 个静态文本字段中删除选项组样式。

        非静态对话框是在 COLORS2 的 WinMain 函数中,在用来显示程序主窗口的 ShowWindow 被调用之后创建的。请注意,主窗口的窗口样式包括 WS_CLIPCHILDREN,这可以使程序在不擦除对话框的情况下重绘主窗口

        如前所述,CreateDialog 返回的对话框窗口句柄被存储在全局变量 hDlgModeless 中并在消息循环中被检测。其实在这个程序中,没有必要把句柄存储在全局变量里或在调用 IsDialogMessage 前测试它的值。该消息循环可以写成这样:

while (GetMessage(&msg, NULL, 0, 0))
{
	if (hDlgModeless == 0 || !IsDialogMessage(hDlgModeless, &msg))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
}
由于在对话框创建之后程序才进入消息循环,而且直到程序结束对话框才被销毁,所以 hDlgModeless 的值永远是有效的。如果你想添加一些代码到对话框窗口过程来销毁该对话框,则基本的逻辑如下所示:

case WM_CLOSE:
    DestroyWindow (hDlg);
    hDlgModelesss = NULL;
    break;

        在原来的 COLORS1 程序中,在调用 wsprintf 把整数转换为文本后,再调用 SetWindowText 来设置三个数字标签的值。代码如下所示:

wsprintf (szBuffer, TEXT("%i"), color[i]);
SetWindowText (hwndValue[i], szBuffer); 
i 的值是目前正在处理的滚动条的 ID 值,hwndValue 是一个数组,包含三个用来显示颜色值的静态文本子窗口的窗口句柄。

        新版本则使用 SetDlgItemInt 来为子窗口设定每个文本字段的数值:

SetDlgItemInt (hDlg, iCtrlID + 3, color[iCtrlID], FALSE);
虽然 SetDlgItemInt 和 GetDlgItemInt 最常与编辑控件一起使用,但它们也可以用来设置其他控件的文本字段,如静态文本控件。其中的 iCtrlID 变量是滚动条的 ID 号;这个值加 3 则转换为相应的数字标签的 ID 号。第三个参数是颜色值。第四个参数指示第三个参数的值是否被视为 有符号数( 如果第四个参数为 TRUE)或 无符号数(如 果第四个参数为 FALSE)。对此程序来说,值范围是从 0~255,所以第四个参数没有任何作用。

        在转换 COLORS1 到 COLORS2 的过程中,我们把越来越多的工作交由 Windows 来完成。旧版调用 CreateWindow 一共 10 次;新版本调用 CreateWindow 一次及 CreateDialog 一次。但如果你认为我们已经对 CreateWindow 的调用减少到了最低限度,那么还是来看看下一个程序吧。

11.2.3  HEXCALC:窗口还是对话框?

        也许懒惰编程的最好诠释是 HEXCALC 程序。这个程序根本没有调用 CreateWindow,也从未处理 WM_PAINT 消息,从来没有获取设备环境,从来没有处理鼠标消息。然而,在不到 150 行的源代码里,它实现了一个有 10 种功能的十六进制计算器,外加完整的键盘和鼠标接口。该计算器如图 11-10 所示。

/*--------------------------------------------------------
	HEXCALC.C -- Hexadecimal Calculator
		    (c) Charles Petzold, 1998
--------------------------------------------------------*/

#include 

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	PSTR szCmdLine, int iCmdShow)
{
	static TCHAR szAppName[] = TEXT("HexCalc");
	HWND         hwnd;
	MSG          msg;
	WNDCLASS     wndclass;

	wndclass.style = CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc = WndProc;
	wndclass.cbClsExtra = 0;
	wndclass.cbWndExtra = DLGWINDOWEXTRA;		// Note!
	wndclass.hInstance = hInstance;
	wndclass.hIcon = LoadIcon(hInstance, szAppName);
	wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
	wndclass.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
	wndclass.lpszMenuName = NULL;
	wndclass.lpszClassName = szAppName;

	if (!RegisterClass(&wndclass))
	{
		MessageBox(NULL, TEXT("This program requires Windows NT!"),
			szAppName, MB_ICONERROR);
		return 0;
	}

	hwnd = CreateDialog(hInstance, szAppName, 0, NULL);

	ShowWindow(hwnd, iCmdShow);
	UpdateWindow(hwnd);

	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return msg.wParam;
}

void ShowNumber(HWND hwnd, UINT iNumber)
{
	TCHAR szBuffer[20];

	wsprintf(szBuffer, TEXT("%X"), iNumber);
	SetDlgItemText(hwnd, VK_ESCAPE, szBuffer);
}

DWORD CalcIt(UINT iFirstNum, int iOperation, UINT iNum)
{
	switch (iOperation)
	{
	case '=': return iNum;
	case '+': return iFirstNum + iNum;
	case '-': return iFirstNum - iNum;
	case '*': return iFirstNum * iNum;
	case '&': return iFirstNum & iNum;
	case '|': return iFirstNum | iNum;
	case '^': return iFirstNum ^ iNum;
	case '<': return iFirstNum << iNum;
	case '>': return iFirstNum >> iNum;
	case '/': return iNum ? iFirstNum / iNum : MAXDWORD;
	case '%': return iNum ? iFirstNum % iNum : MAXDWORD;
	default : return 0;
	}
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static BOOL		bNewNumber = TRUE;
	static int		iOperation = '=';
	static UINT		iNumber, iFirstNum;
	HWND			hButton;

	switch (message)
	{
	case WM_KEYDOWN:
		if (wParam != VK_LEFT)			// left arrow --> backspace
			break;
		wParam = VK_BACK;
										// fall through
	case WM_CHAR:
		if ((wParam = (WPARAM)CharUpper((TCHAR *)wParam)) == VK_RETURN)
			wParam = '=';

		if (hButton = GetDlgItem(hwnd, wParam))
		{
			SendMessage(hButton, BM_SETSTATE, 1, 0);
			Sleep(100);
			SendMessage(hButton, BM_SETSTATE, 0, 0);
		}
		else
		{
			MessageBeep(0);
			break;
		}
												// fall through
		
	case WM_COMMAND:
		SetFocus(hwnd);

		if (LOWORD(wParam) == VK_BACK)			// backspace
			ShowNumber(hwnd, iNumber /= 16);

		else if (LOWORD(wParam) == VK_ESCAPE)	// escape
			ShowNumber(hwnd, iNumber = 0);

		else if (isxdigit(LOWORD(wParam)))		// hex digit
		{
			if (bNewNumber)
			{
				iFirstNum = iNumber;
				iNumber = 0;
			}
			bNewNumber = FALSE;

			if (iNumber <= MAXDWORD >> 4)
				ShowNumber(hwnd, iNumber = 16 * iNumber + wParam - (isdigit(wParam) ? '0' : 'A' - 10));
			else
				MessageBeep(0);
		}
		else										// operation
		{
			if (!bNewNumber)
				ShowNumber(hwnd, iNumber = CalcIt(iFirstNum, iOperation, iNumber));
			bNewNumber = TRUE;
			iOperation = LOWORD(wParam);
		}
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}
HEXCALC.RC (节选)

// Microsoft Visual C++ generated resource script.
//
#include "resource.h"

/
//
// Icon
//

HEXCALC                 ICON                    "HexCalc.ico"

/
#include "hexcalc.dlg"
HEXCALC.DLG

/*------------------------------
    HEXCALC.DLG dialog script
-------------------------------*/

HexCalc DIALOG -1, -1, 102, 122
STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX
CLASS "HexCalc"
CAPTION "Hex Calculator"
{
    PUSHBUTTON "D",        68, 8,  24, 14, 14
    PUSHBUTTON "A",        65, 8,  40, 14, 14
    PUSHBUTTON "7",        55, 8,  56, 14, 14
    PUSHBUTTON "4",        52, 8,  72, 14, 14
    PUSHBUTTON "1",        49, 8,  88, 14, 14
    PUSHBUTTON "0",        48, 8, 104, 14, 14

    PUSHBUTTON "0",        27, 26,   4, 50, 14
    PUSHBUTTON "E",        69, 26,  24, 14, 14
    PUSHBUTTON "B",        66, 26,  40, 14, 14
    PUSHBUTTON "8",        56, 26,  56, 14, 14
    PUSHBUTTON "5",        53, 26,  72, 14, 14
    PUSHBUTTON "2",        50, 26,  88, 14, 14
    PUSHBUTTON "Back",     8, 26, 104, 32, 14

    PUSHBUTTON "C",        67, 44,  40, 14, 14
    PUSHBUTTON "F",        70, 44,  24, 14, 14
    PUSHBUTTON "9",        57, 44,  56, 14, 14
    PUSHBUTTON "6",        54, 44,  72, 14, 14
    PUSHBUTTON "3",        51, 44,  88, 14, 14

    PUSHBUTTON "+",         43, 62,  24, 14, 14
    PUSHBUTTON "-",         45, 62,  40, 14, 14
    PUSHBUTTON "*",         42, 62,  56, 14, 14
    PUSHBUTTON "/",         47, 62,  72, 14, 14
    PUSHBUTTON "%",         37, 62,  88, 14, 14
    PUSHBUTTON "Equals", 61, 62, 104, 32, 14
    
    PUSHBUTTON "&&",      38, 80,  24, 14, 14
    PUSHBUTTON " | ",     124, 80,  40, 14, 14
    PUSHBUTTON "^",          94, 80,  56, 14, 14
    PUSHBUTTON "<",          60, 80,  72, 14, 14
    PUSHBUTTON ">",          62, 80,  88, 14, 14
}
HEXCALC.ICO

11.2 非模态对话框_第2张图片

11.2 非模态对话框_第3张图片

图 11-10  HEXCALC 的显示

        HEXCALC 程序使用中缀表达式,并使用 C 语言中的运算符。它支持无符号的 32 位整数,并有加、减、乘、除和求余数运算,可进行按位与(AND)、按位或(OR)及按位异或操作;并能实现左位移和右位移。被 0 除去会导致 FFFFFFFF 的结果。

        在使用 HEXCALC 时,可以用鼠标或键盘。开始时,通过“单击”或输入方式来输入第一个数值(最多 8 位十六进制数),接着输入运算符,然后第二个数值。然后,可以单击 Equals 按钮或按下等号键或左箭头键即可。单击“结果显示”框或按 ESC 键可清除当前输入。

        HEXCALC 的特殊之处在于,屏幕上显示的窗口看起来像是一种普通的层叠窗口和非模态对话框的混合体。一方面,所有送到 HEXCALC 的消息都在一个名为 WndProc 的函数中被处理,而这个 WndProc 看上去似乎是一个正常的窗口过程,它返回一个长整数,处理 WM_DESTROY 消息,并调用 DefWindowProc,就像一个正常的窗口过程一样。另一方面,该程序在 WinMain 中通过调用 CreateDialog 函数来创建窗口,该函数使用的是一个在 HEXCALC.DLG 中定义的对话框模板。那么,HEXCALC 到底是一个普通的层叠窗口还是一个非模态对话框呢?

        答案很简单,对话框框本身就是一个窗口。通常情况下,Windows 会使用其自身的内部窗口过程来处理给对话框窗口的消息。然后 Windows 会将这些消息传递给创建该对话框的程序内部的对话框过程。在 HEXCALC 中,我们迫使 Windows 使用对话框模板来创建一个窗口,但我们却自己处理该窗口的消息。

        不幸的是有些对话框模板需要的东西是不能在 Developer Studio 的对话框编辑器中添加的。出于这个原因,HEXCALC.DLG 文件中的对话框模板,正如你可能(正确)猜到的,是手动输入的。你可以添加一个文本文件到任何一个项目中,为此请从 File 菜单中选择 New,然后选择 File 选项卡。并在文件类型列表中选择 Text File。这种包含额外资源定义的文件需要被包括在资源脚本中。从 View 菜单上,选择 Resource Includes。这会显示一个对话框。在 Compile-time Directives 文本框中输入以下代码:

#include "hexcalc.dlg"
这样此行代码就会被插入 HEXCALC.RC 资源脚本。

        请仔细观察 HEXCALC.DLG 文件中的对话框模板,它将揭示 HEXCALC 如何针对对话框使用它自己的窗口过程。该对话框模板的开始部分如下:

HexCalc DIALOG -1, -1, 102, 122
STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX
CLASS "HexCalc"
CAPTION "Hex Calculator"

        请注意这些标识符,如 WS_OVERLAPPED 和 WS_MINIMIZEBOX,我们可以使用它们来调用 CreateWindow 从而创建一个正常的窗口。这个对话框和其他我们迄今创建的对话框的关键区别是CLASS 语句(这是 Developer Studio 中的对话框编辑器不允许我们指定的)。当我们在前面的对话框模板中省略这一语句时,Windows 会为对话框注册窗口类,并使用它自己的窗口过程来处理对话框消息。CLASS 语句在这里告诉 Windows 把消息发送到别处,具体而言,是发送到 HexCalc 窗口类中指定的窗口过程

        HexCalc 窗口类在 WinMain 函数中注册,就像一个正常窗口的窗口类。然而,请注意这个非常重要的差别:在 WNDCLASS 结构中 cbWndExtra 字段被设置为 DLGWINDOWEXTRA。这是你注册自己的对话框过程所必不可少的。

        注册窗口类后,WinMain 调用 CreateDialog:

hwnd = CreateDialog(hInstance, szAppName, 0, NULL);
第二个参数(字符串 “HexCalc”)是对话框模板的名称。第三个参数通常应是父窗口句柄,这里因为没有父窗口而被设置为 0。最后一个参数通常是对话框过程的地址,再次并不需要,因为 Windows 将不会处理任何消息,从而也不能把它们送给对话框过程。

        上面的 CreateDialog 调用,与对话框模板一起,被 Windows 有效地翻译成 CreateWindow 调用,其作用相当于

hwnd = CreateWindow (TEXT("HexCalc"), TEXT("Hex Calculator"),
          WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, 
          CW_USEDFAULT, CW_USEDFAULT,
          102 * 4 / cxChar, 122 * 8 / cyChar,
          NULL, NULL, hInstance, NULL);
其中的 cxChar 和 cyChar 变量是对话框的字体字符的宽度和高度。

        我们从让 Windows 调用 CreateWindow 中收获了巨大的好处:Windows 不止会创建一个弹出窗口,它还会为所有对话框模板中定义的 29 个子窗口按钮控件调用 CreateWindow。所有这些控件发送 WM_COMMAND 消息到父窗口的窗口过程,这和 WndProc 没有什么不同。这是一种极好的创建含有一组子窗口的窗口的方法。

        这里还有另一种使 HEXCALC 的代码保持到最少的方法:你会发现,HEXCALC 不包含任何头文件,而定义对话框模板中的子窗口控件的标识符通常需要头文件。我们可以免除此文件的原因是这些 ID 值被设成了每一个按钮控件中显示的文本的 ASCII 码。这意味着,WndProc 可以用大致相同的方式处理 WM_COMMAND 消息和 WM_CHAR 消息。在每种情况下,wParam 的低位字都是按钮的 ASCII 码

        当然,对键盘消息做一些处理还是必要的。WndProc 捕获 WM_KEYDOWN 消息,以把左箭头键翻译成 Backspace 键。在处理 WM_CHAR 消息时,WndProc 把字符码转换为大写形式,把回车键转换为等号键的 ASCII 码。

        调用 GetDlgItem 检查 WM_CHAR 消息的有效性。如果 GetDlgItem 函数返回 0,则表示键盘字符不是对话框模板中定义的一个标识符。如果字符是标识符之一,那么相应的按钮会收到两个 BM_SETSTATE 消息而闪动:

if (hButton = GetDlgItem(hwnd, wParam))
{
	SendMessage(hButton, BM_SETSTATE, 1, 0);
	Sleep(100);
	SendMessage(hButton, BM_SETSTATE, 0, 0);
}
这样以最小的努力,我们为 HEXCALC 的键盘界面增加了一种良好的触感。Sleep 函数会使程序中止 100 毫秒。这样就避免了由于按钮被快速“单击”而无法察觉。

        在 WndProc 处理 WM_COMMAND 消息时,它总是将输入焦点设为父窗口:

case WM_COMMAND:
    SetFocus(hwnd);
否则,当鼠标单击某个按钮时,输入焦点将转移到该按钮。

你可能感兴趣的:(《Windows,程序设计》学习之旅)