DWinLib - 创建模态对话框





5.00/5 (2投票s)
需要创建一个简单的,或者不太简单的对话框?即使没有 DWinLib,以下方法也可能正是您需要的!
本文从纯 Windows 应用程序编程接口 (API) 的角度以及半等效的 DWinLib 包装器角度,讨论了在 DWinLib 中创建模态对话框。
在我创建具有双耳节拍功能的 MIDI 程序时,我需要简单的方法来调整 MIDI 事件属性。自然而然地会想到的一种方法是双击一个项目以弹出属性窗口。
当我创建 DWinLib 时,我对 MFC 或 Windows API 并没有太多经验。我所有的工作都集中在 Borland Builder 及其 Visual Class Library 上,所以我试图让我的框架像它一样简单。
我对对话框的经验基本上仅限于 Windows 的 MessageBox
。如果您玩过这个函数,您就知道它会阻塞您的代码流程,直到用户按下“确定”或“取消”。换句话说,给定
int result = MessageBox(hwndC, _T("Press Yes"), _T("Do It"), MB_YESNO);
if (result == IDYES) doSomething();
else if (result == IDNO) giveUserHardTime();
在用户按下 MessageBox
按钮(该按钮会关闭对话框并从该点继续程序)之前,程序不会执行 if (result == IDYES) doSomething();
这行代码。
当我在程序中需要类似功能时,我不知道该怎么办。我以前从未玩过 CreateDialog
或 DialogBox
,而且我看到的示例似乎都是面向 MFC 的。所以我从我了解的东西入手。那就是创建 Windows:我想以同样的方式创建对话框。经过大量的研究,我弄明白了。
下面的项目包含了我的努力成果。它包含 DWinLib 6.04(2021 年 1 月)的完整副本,所以如果您解压了 zip 文件,如果您已经从另一篇文章中获得了这些子目录,您可以跳过它们。 .sln 文件是用 Visual Studio 2019 编译的,所以旧版本可能需要手动重新创建项目。
你好世界!
在深入研究模态对话框之前,请允许我介绍一个简单的“Hello World”程序,因为它引起了我的注意,我还没有这样做。旅途中将分享一些深奥的 DWinLib 知识!
假设您解压缩了 zip 文件并在 Visual Studio 中成功打开并构建了 .sln 文件,请关闭可执行文件。导航到 MainAppWin.h 文件,并在 MainAppWin 类的末尾添加以下行
//For play:
private:
dwl::Button * helloButtonC;
public:
LRESULT hello(Button b, WPARAM flags, Point p);
};
并在 .cpp 文件中将以下粗体文本添加到 MainAppWin::instantiate()
bool MainAppWin::instantiate() {
ui::MainMenu * theMenu = new ui::MainMenu(this);
//In the following, MainMenu has a virtual destructor, so everything will be deleted
//properly. I also noted in the past that gMainWin must be set before the menu
//instantiation. That is done in MainWin's constructor, which is called beforehand.
mainMenuC.reset(theMenu);
fullMenuC = s_cast<ui::MainMenu*>(mainMenuC.get());
MainWin::instantiate(mainMenuC.get()); //The MDI client is created in this call
helloButtonC = new dwl::Button(mdiClientC.get(), [&](dwl::Object*) { hello(Button::Left, 0, {0, 0}); } );
helloButtonC->instantiate(50, 50, 120, 25, _T("Press Me!"));
helloButtonC->onButtonDown([&](Button b, WPARAM flags, Point p) {
return hello(b, flags, p);});
Rect winRect;
//...
并将 hello()
函数添加到文件中
LRESULT MainAppWin::hello(Button b, WPARAM flags, Point p) {
dwl::msgBox(_T("Hello there!"));
return 0;
}
编译并执行项目,您将看到一个按钮,单击该按钮后,将显示经典的“Hello”消息
现在是深奥的知识。
这个按钮很特别。它被父化到应用程序的主多文档界面 (MDI) 客户端窗口,如果您查看我 "DWinLib - The Guts" 文章中的设计图,您会看到 MdiClient
继承自 Control
。阅读那篇文章,您还会发现 ControlWin
和 BaseWin
类具有 winProc
(或者您更喜欢 WindowProc
这个术语)。这意味着该按钮没有窗口过程可以链接!MDI 客户端无法捕获 WM_COMMAND
消息并将其路由到应用程序的委托处理程序,以便将其转发到您自己的函数。
这就是为什么上面的代码会设置按钮的 onChange
处理程序。由于 Button
继承自 WinControl
,它们有一个内置的 winProc
处理程序,在该处理程序中有一个 std::function
,当按下鼠标左键时会触发它。
这也意味着我们的按钮与其他按钮相比非常有限。无法通过 Tab 键进入,也不能通过按“Enter”键来触发按钮。只有按下鼠标左键才会显示“Hello”消息。
另一个限制是,如果您通过按“新建文档”按钮或“Ctrl + N”打开新文档,然后立即将其“还原”为非最大化状态。出于某种原因,按钮会“渗透”到文档上,直到文档被强制重绘,方法是调整窗口大小。
解决这些问题的最简单方法是不要将控件父化到主 MDI 客户端窗口。但这对于学习来说是一个很好的例子。
(我怀疑如果 MdiClient
有一个在适当的地方调用 DefMDIChildProc
的窗口过程,您就可以克服这些限制。但这值得麻烦吗?我相信这种情况是 YAGNI - 您永远不需要它,这就是为什么我从未走过这条路。)
继续前进!
如果您愿意,可以在继续之前删除之前的添加内容。然后再次运行程序,通过“Ctrl + N”或按“新建文档”按钮向其中添加一个新文档(或选择主菜单的“文件”项),然后按顶部的按钮。将出现一个 DWinLib 模态对话框。在功能上,它几乎等同于真正的 Windows 对话框。但并非完全如此,如果您仔细检查对话框的特性。
正如我所说,在遇到这个问题时,我还没有从 API 的角度玩过真正的模态窗口。所以我搜来搜去。最终我找到了解决方案的关键:EnableWindow
。
通过玩弄它,我发现我可以伪造模态窗口行为。经过更多的努力,我让一切都奏效了。
解决方案需要两部分。第一部分是创建一个看起来像模态的窗口。这在创建过程中完成。使用具有以下属性的 CREATESTRUCT
调用 CreateWindow
//...
cs.dwExStyle = WS_EX_TOOLWINDOW;
cs.style = WS_POPUP | WS_CAPTION | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
//...
最后,第二部分:在模态窗口的创建和销毁过程中使用 EnableWindow
//In 'ModalBaseForm::instantiate':
if (modalC) {
EnableWindow(parentC->hwnd(), FALSE);
//...
}
//In both the destructor and <code>wClose</code>:
EnableWindow(parentC->hwnd(), TRUE);
有了这些,如果窗口被指定为真正的模态,主窗口将变得禁用,并且模态窗口将获得控制权。如果它不是完全模态的,主窗口可以在被单击时重新获得焦点,但无模式窗口不会消失。
当然,如果您想处理加速器,还需要更多内容。负责的代码是 instantiate
方法和析构函数
ModalBaseForm::~ModalBaseForm() {
EnableWindow(parentC->hwnd(), TRUE);
if (modalC) gDwlGlobals->dwlApp->accel().pop();
gDwlGlobals->dwlApp->removeWindow(this);
}
void ModalBaseForm::instantiate(int x, int y, int width, int height) {
static bool alreadyRegistered = false;
wpWidthC = width;
wpHeightC = height;
if (!alreadyRegistered) {
WNDCLASSEX wc;
ZeroMemory(&wc, sizeof(wc));
wc.cbSize = sizeof (WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = dwl::Application::winProc;
wc.hInstance = gDwlGlobals->dwlApp->instance();
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
//The ones that were left to the user to set if needed were:
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hIcon = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = &winClassName[0];
wc.hIconSm = NULL;
if (!RegisterClassEx (&wc))
throw dwl::Exception(_T("Unable to register ModalBaseForm"));
alreadyRegistered = true;
}
CREATESTRUCT cs;
cs.dwExStyle = WS_EX_TOOLWINDOW;
cs.lpszClass = &winClassName[0];
cs.lpszName = winCaption;
cs.style = WS_POPUP | WS_CAPTION | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
cs.x = x;
cs.y = y;
cs.cx = width;
cs.cy = height;
cs.hwndParent = gDwlGlobals->dwlMainWin->hwnd();
cs.hMenu = NULL;
cs.hInstance = gDwlGlobals->dwlApp->instance();
cs.lpCreateParams = NULL;
hwndC = gDwlGlobals->dwlApp->createWindow(this, cs);
if (!hwndC) throw dwl::Exception(_T("Unable to create window"));
if (modalC) {
EnableWindow(parentC->hwnd(), FALSE);
gDwlGlobals->dwlApp->accel().push();
gDwlGlobals->dwlApp->accel().addAccelerator(FVIRTKEY, VK_ESCAPE, (short)cancelCallbackC->id());
gDwlGlobals->dwlApp->changeAccelTable();
}
}
解决方案的第二部分是“伪造”来自对话框的返回值。这是在响应对话框按钮按下时完成的。
因为 DWinLib 的“对话框”是对普通窗口的薄包装,所以可以将指向父窗口或控制类的指针轻松地传递到构造函数中。(您也可以用稍微困难一些的方式将相同的操作应用到 WinAPI 对话框上。您只需将一个“LPARAM
变量”强制转换为实际类型。)
同样,对话框中的按钮和其他控件都是完整的 DWinLib 控件,可以像这样使用。这与资源创建的对话框不同,后者只能使用类似 Windows API 的功能。该功能限制您只能使用“define
”作为按钮标识符,这意味着您必须以某种方式将这些“define
”映射到相应的选项或控件流。
在 DWinLIb 中,每个 ControlWin
派生控件都包含一个 user
变量,它可以保存“任何东西”。这只是 Christopher Diggin 的“any
” 工具对象。对于这个例子,只需将一个 enum
值放入该 user
中,然后基于该值采取直接行动。将其放入代码中,
ModalReturnTest::ModalReturnTest(AppWindow * win, bool modal) :
dwl::ModalBaseForm(modal),
appWindowC(win) { //This line sets up an <code>AppWindow</code> pointer
//for later use
DWORD style = ((WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_OVERLAPPEDWINDOW) &
~WS_SIZEBOX & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
SetWindowLong(hwndC, GWL_STYLE, style);
SetWindowPos(hwndC, 0, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOZORDER | SWP_NOSIZE |
SWP_NOACTIVATE | SWP_FRAMECHANGED);
SetWindowText(hwndC, _T("Modal Return Test"));
//Set up the buttons to call back to the ModalReturnTest::buttonCallback:
buttonAC.reset(new dwl::Button(this,
CreateDwlDelegate(ModalReturnTest, buttonCallback, this, this)));
//And set the 'user' to the appropriate option:
buttonAC->user = MRT::OptionA;
buttonBC.reset(new dwl::Button(this,
CreateDwlDelegate(ModalReturnTest, buttonCallback, this, this)));
buttonBC->user = MRT::OptionB;
buttonCC.reset(new dwl::Button(this,
CreateDwlDelegate(ModalReturnTest, buttonCallback, this, this)));
buttonCC->user = MRT::OptionC;
cancelButtonC.reset(new dwl::Button(this,
CreateDwlDelegate(dwl::ModalBaseForm, cancel, this, this)));
}
//The 'buttonCallback is' defined as:
void ModalReturnTest::buttonCallback(dwl::Object * obj) {
dwl::Button * button = d_cast<dwl::Button*>(obj);
if (!button) return;
MRT option = button->user.cast<MRT>();
appWindowC->optionCallback(this, option);
wClose();
}
//And, finally, the AppWindow::optionCallback, where the final logic occurs, is:
void AppWindow::optionCallback(ModalReturnTest * win, MRT option) {
//Take action here, based upon the option selected in the modal dialog box.
if (option == MRT::OptionA)
MessageBox(win->hwnd(), _T("Option A selected"), _T("Result"), MB_OK);
else if (option == MRT::OptionB)
MessageBox(win->hwnd(), _T("Option B selected"), _T("Result"), MB_OK);
else if (option == MRT::OptionC)
MessageBox(win->hwnd(), _T("Option C selected"), _T("Result"), MB_OK);
}
非常简单直接的 C++ 代码。请注意,ModalReturnTest::buttonCallback
是将 user
强制转换回其选项的地方。
在我撰写此文的 2021 年 1 月版本时,我想最终理解 Windows API 创建对话框的方式。我稍微了解了如何通过 Visual Studio 的资源编辑器创建和使用资源,并想知道 DWinLib 程序与这样创建的资源进行交互有多容易。我发现这并不难。表单上的第三个和第四个按钮以模态和无模式方式创建资源对话框。
第一件事是创建对话框。在 Visual Studio 中,您必须在解决方案资源管理器中右键单击一个过滤器,并向其“添加”一个对话框资源。然后,您可以从工具箱(“视图 -> 工具箱”菜单项)中向该资源添加控件。右键单击添加的按钮,选择“属性”,然后您可以将按钮的 ID 修改为逻辑助记符。从那里,您必须为对话框向程序添加一个回调函数。在该回调中,您必须处理这些 ID 的 WM_COMMAND
消息,并在该点采取适当的操作。
为了显示对话框,您必须调用 CreateDialog
或 DialogBox
函数。对于前者,您还必须调用 ShowWindow
。将其放入代码中
//This is a plain windows callback function, unassociated with any class.
//The AppWindow * must be sent to it in the form of a LPARAM variable. This means the
//CreateDialogParam version of CreateDialog must be used. Also note that this means
//only 1 non-modal window could use this at a time as-is, because of the 'static' variable:
INT_PTR CALLBACK DialogProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
static AppWindow * win = nullptr;
switch (msg) {
case WM_INITDIALOG:
win = r_cast<AppWindow*>(lParam);
return TRUE;
break;
case WM_COMMAND:
switch(LOWORD(wParam)) {
case ID_OPTIONA:
if (win) win->winDialogCallback(ID_OPTIONA);
EndDialog(hwnd, ID_OPTIONA);
break;
case ID_OPTIONB:
if (win) win->winDialogCallback(ID_OPTIONB);
EndDialog(hwnd, ID_OPTIONB);
break;
case ID_OPTIONC:
if (win) win->winDialogCallback(ID_OPTIONC);
EndDialog(hwnd, ID_OPTIONC);
break;
case IDCANCEL:
EndDialog(hwnd, IDCANCEL);
break;
}
break;
default:
return FALSE;
}
return TRUE;
}
void AppWindow::modalResourceButtonPress(dwl::Object * obj) {
int option = DialogBox(gDwlGlobals->dwlApp->instance(), MAKEINTRESOURCE(IDD_DIALOG1),
hwndC, (DLGPROC)DialogProc);
if (option == ID_OPTIONA) dwl::msgBox(_T("Option A Selected"));
if (option == ID_OPTIONB) dwl::msgBox(_T("Option B Selected"));
if (option == ID_OPTIONC) dwl::msgBox(_T("Option C Selected"));
}
void AppWindow::nonModalResourceButtonPress(dwl::Object * obj) {
HWND hwnd = CreateDialogParam(gDwlGlobals->dwlApp->instance(), MAKEINTRESOURCE(IDD_DIALOG1),
hwndC, (DLGPROC)DialogProc, (LRESULT)this);
if (hwnd) ShowWindow(hwnd, SW_SHOW);
int a = 0;
}
void AppWindow::winDialogCallback(int option) {
if (option == ID_OPTIONA) dwl::msgBox(_T("Option A Selected"));
if (option == ID_OPTIONB) dwl::msgBox(_T("Option B Selected"));
if (option == ID_OPTIONC) dwl::msgBox(_T("Option C Selected"));
}
当然,那些“选项”(ID_OPTIONA 等)定义在一个头文件中(默认名为“resource.h”),该头文件会在您通过资源编辑器更改 ID 助记符时自动生成和更新。
就是这样。我想不出更多的话来占用空间,所以我会在这里结束写作,希望您发现它对您有所帮助,即使只是出于对 MFC 等框架幕后 Windows API 工作原理的好奇。
编程愉快!
更新历史
- 2021/1/16 - 更新文章以反映 DWinLib 6.04。这包括添加资源创建的对话框,以了解 DWinLib 和 Windows API 如何协同工作。
- 2013/2/13 - 更新文章以反映 DWinLib 3.01。
- 2006/2/27 - 修复了一些小错误。