65.9K
CodeProject 正在变化。 阅读更多。
Home

DWinLib - 创建模态对话框

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2013年2月14日

CPOL

8分钟阅读

viewsIcon

17003

downloadIcon

390

需要创建一个简单的,或者不太简单的对话框?即使没有 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(); 这行代码。

当我在程序中需要类似功能时,我不知道该怎么办。我以前从未玩过 CreateDialogDialogBox,而且我看到的示例似乎都是面向 MFC 的。所以我从我了解的东西入手。那就是创建 Windows:我想以同样的方式创建对话框。经过大量的研究,我弄明白了。

下面的项目包含了我的努力成果。它包含 DWinLib 6.04(2021 年 1 月)的完整副本,所以如果您解压了 zip 文件,如果您已经从另一篇文章中获得了这些子目录,您可以跳过它们。 .sln 文件是用 Visual Studio 2019 编译的,所以旧版本可能需要手动重新创建项目。

 

Image 1

你好世界!

在深入研究模态对话框之前,请允许我介绍一个简单的“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”消息

Image 1

现在是深奥的知识。

这个按钮很特别。它被父化到应用程序的主多文档界面 (MDI) 客户端窗口,如果您查看我 "DWinLib - The Guts" 文章中的设计图,您会看到 MdiClient 继承自 Control。阅读那篇文章,您还会发现 ControlWinBaseWin 类具有 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 消息,并在该点采取适当的操作。

为了显示对话框,您必须调用 CreateDialogDialogBox 函数。对于前者,您还必须调用 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 - 修复了一些小错误。
© . All rights reserved.