如何从另一个进程显示模态对话框窗口?

3
我有一个32位的MFC应用程序,它使用一个自定义库,重新编译成x64将是一场噩梦。通常情况下,该应用程序并不需要作为64位运行,除了一个情况——就是渲染内容以在对话框窗口中显示时可以受益于更大的寻址空间。
因此,我的目标是“模仿”CDialog :: DoModal方法,但针对另一个进程中的对话框窗口。
我将该对话框窗口构建为独立的x64 MFC基于对话框的应用程序。它以文件路径作为输入参数,内部完成所有工作,并返回简单的用户选择:OK,Cancel。
因此,我从我的主要父进程执行以下操作:
//Error checks omitted for brevity
CString strCmd = L"D:\\C++\\MyDialogBasedApp.exe";

HWND hParWnd = this->GetSafeHwnd();

SHELLEXECUTEINFO sei = {0};
sei.cbSize = sizeof(sei);
sei.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_UNICODE | SEE_MASK_NOCLOSEPROCESS;
sei.nShow = SW_SHOW;
sei.lpVerb = _T("open");
sei.lpFile = strCmd.GetBuffer();
sei.hwnd = hParWnd;

BOOL bInitted = SUCCEEDED(::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE));

ShellExecuteEx(&sei);

DWORD dwProcID = ::GetProcessId(sei.hProcess);

//Try to get main Wnd handle for the child process
HWND hMainChildWnd = NULL;
for(;; ::Sleep(100))
{
    hMainChildWnd = getHwndFromProcID(dwProcID);
    if(hMainChildWnd)
        break;
}

HWND hPrevParWnd = ::SetParent(hMainChildWnd, hParWnd);
if(hPrevParWnd)
{
    //Wait for child process to close
    ::WaitForSingleObject(sei.hProcess, INFINITE);

    //Reset parent back
    ::SetParent(hMainChildWnd, hPrevParWnd);
}

::CloseHandle(sei.hProcess);

if(bInitted)
    ::CoUninitialize();

这里的getHwndFromProcID函数来自这个链接

这个方法基本上可以实现它的功能,但有以下问题:

(1)任务栏上会有两个图标:一个是主应用程序的,另一个是子应用程序的。有没有办法不显示子级图标?

(2)我可以从子窗口切换到父窗口,反之亦然。在实际模式对话框窗口中,当子窗口打开时,不能返回到父窗口。是否有办法解决这个问题?

(3)如果我开始与父窗口交互,它似乎会"挂起",操作系统甚至会在标题栏上显示它。

因此,我想知道是否有办法解决所有这些问题?

3个回答

3
  1. 你需要将自己窗口的指针传递给子进程。
  2. 在等待子进程退出时,您需要处理Windows消息。这里不能使用WaitForSingleObject,需要使用MsgWaitForMultipleObjectsEx
  3. 创建子进程时,子进程必须将您的窗口设置为自身所有者窗口-您不需要调用SetParent

通过这些步骤,一切都将完美地工作。在32位MFC应用程序中,您需要使用以下代码:

BOOL DoExternalModal(HWND hwnd, PCWSTR ApplicationName)
{
    STARTUPINFO si = { sizeof(si) };
    PROCESS_INFORMATION pi;
    WCHAR CommandLine[32];
    swprintf(CommandLine, L"*%p", hwnd);

    if (CreateProcessW(ApplicationName, CommandLine, 0, 0, 0, 0, 0, 0, &si, &pi))
    {
        CloseHandle(pi.hThread);

        MSG msg;

        for (;;)
        {
            switch (MsgWaitForMultipleObjectsEx(1, &pi.hProcess, INFINITE, QS_ALLINPUT, 0))
            {
            case WAIT_OBJECT_0:
                CloseHandle(pi.hProcess);
                return TRUE;
            case WAIT_OBJECT_0 + 1:
                while (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
                {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
                continue;
            default: __debugbreak();
            }
        }
    }

    return FALSE;
}

MyDialogBasedApp.exe 中,让我们使用 MessageBox 作为演示对话框。我们将使用您的 MFC 窗口作为第一个参数。
void ExeEntry()
{
    int ec = -1;

    if (PWSTR CommandLine = GetCommandLine())
    {
        if (CommandLine = wcschr(CommandLine, '*'))
        {
            HWND hwnd = (HWND)(ULONG_PTR)_wcstoi64(CommandLine + 1, &CommandLine, 16);

            if (hwnd && !*CommandLine && IsWindow(hwnd))
            {
                ec = MessageBoxW(hwnd, L"aaa", L"bbb", MB_OK);
            }
        }
    }

    ExitProcess(ec);
}

使用以下代码:

(1) 任务栏上只有一个主应用程序的图标。

(2) 您无法在子窗口和父窗口之间切换焦点。所有工作都像实际的模态对话框窗口一样。

(3) 父窗口不会“挂起”,因为它正在处理Windows消息 (MsgWaitForMultipleObjectsEx) - 您的代码“挂起”是因为您没有这样做,而是在WaitForSingleObject中等待。


哇,老兄。它完全有效!谢谢。不过你的代码有两处需要更正。(1) 为什么你没有使用MFC的theApp.PumpMessage();来代替PeekMessage循环?或者你严格遵循Win32?(2) 你的方法不能从任务栏中删除子进程的图标。要在MyDialogBasedApp.exe进程中实现这一点,你需要在WM_INITDIALOG处理程序中从自身窗口中删除WS_EX_APPWINDOW扩展样式。 - c00000fd
@c00000fd - 1) 是的 - 您可以使用 while (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) App->PumpMessage(); 替代 while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)){..}。 2) 在我的测试中,我使用了不会在任务栏上创建图标的 MessageBoxW 对话框。我没有使用 WS_EX_APPWINDOW。当然,我不知道您在 MyDialogBasedApp.exe 中使用了什么。 - RbMm
@c00000fd - 这里实现消息循环的方式并没有特别的区别,因为模态对话框有自己的消息循环。如果你直接从应用程序中调用 MessageBox,那么在其运行时,内部消息循环将在 MessageBox 中执行而不是在你的 MFC 循环中执行。主 UI 线程必须永久调用 PeekMessageGetMessage 来处理 Windows 消息,否则 UI 将挂起。 - RbMm
有跨进程的父/子或所有者/拥有窗口关系是合法的吗?这将在某个时候崩溃,而且你无法修复它,因为MFC不在你的控制范围内。你无法可靠地实现跨进程的所有者/拥有窗口关系。很抱歉,-1分甚至没有提到这一点。 - IInspectable
评论不是用于进行长时间讨论的;本次对话已被移至聊天室 - Bhargav Rao

0

模态对话框有两个使其“模态”的特点:

  • 对话框的“所有者”设置为父窗口。
  • 禁用父窗口。

我稍微尝试了一下,虽然你可以手动完成这些操作,但最简单的方法是将父窗口句柄传递给DialogBox函数(或MFC中的CDialog构造函数)。

在父进程的ShellExecuteEx之后,您的子进程可以使用FindWindow(或类似机制)获取父窗口句柄,并使用该句柄显示对话框,而不是在父进程中完成所有工作。


有跨进程的父/子或所有者/拥有窗口关系是合法的吗? - IInspectable

0

你试图做的事情是不安全的。博客文章是否可以拥有跨进程的父/子或所有者/拥有窗口关系?解释了,安装跨进程所有者/拥有窗口关系会导致系统调用AttachThreadInput,而且众所周知AttachThreadInput就像是把两个线程的钱汇入一个联合银行账户,需要双方都在场才能取出任何钱。这会产生非常真实的死锁风险。只有当你控制参与的两个线程时,才能安全地防止这种情况发生。由于至少有一个线程使用第三方应用程序框架(在这种情况下是MFC),因此这是被禁止的。

既然我们已经确认,您提出的解决方案无法安全实施,您需要寻找替代方案。一种解决方案可能是将工作委托给64位进程进行计算,并将结果传回32位进程以供显示。


1
感谢您的贡献。在我们进一步探讨之前,让我问一下。Chen的文章涉及到一个子进程是一个“陌生人”或其他我们无法控制的进程。而在我的情况中,我可以控制这两个进程并根据需要进行调整。此外,MFC框架也不是一个陌生人。我们有它的源代码,我们知道它背后发生了什么。那里没有秘密。我会将其静态链接,这样它就不会因为Windows更新而改变。最重要的是,我不会编码这些窗口相互发送消息。那么,您能否使用这个特定的例子说一下为什么它行不通呢? - c00000fd
我在从XP到Win10的各种操作系统上进行了测试,唯一似乎存在问题的是XP。由于某种原因,那里的两个窗口的行为类似于不同的进程(而不是模态子父级关系)。其余的Vista及以上版本都可以正常工作,没有问题、挂起、内存损坏等。再次强调,我并不是在批评您的评论,我只是想知道具体的原因为什么不行... - c00000fd
故障模式并不是因为你不知道MFC在做什么。问题在于,MFC没有准备好在线程之间共享输入队列,并且没有采取任何特殊措施来满足该用例。(此外,MFC不是操作系统的一部分,也不会收到任何更新。如果您想过上幸福的生活,您应该选择动态链接。)另外,你确实在窗口之间发送消息。你只是不知道,因为那段代码被嵌入到对话框管理器中(你正在使用它)。 - IInspectable
只是好奇,您认为此答案中描述的方法会“在线程之间共享输入队列”或调用AttachThreadInput的原因是什么? - c00000fd
@c00000fd:MessageBoxW 中传递了一个 HWND,它属于另一个线程。从我链接的博客文章中我们已经了解到,*"创建跨线程的所有者/拥有窗口关系会隐式地连接这些窗口所属的线程的输入队列"*。我相信如果你请那个提出答案的作者进入代码,他们会得出结论,即在创建跨线程窗口层次结构的过程中调用了 AttachThreadInput - IInspectable
我问这个问题的原因是因为我使用了WinDbg检查了父进程和子进程,但它们似乎都没有调用AttachThreadInput或者bp USER32!NtUserAttachThreadInput - c00000fd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接