为什么拥有 window 显示在拥有 window 之上?

Why is the owning window showing up above the owned window?

我有一个非常奇怪的问题。我正在尝试复制 window 层次结构。因此,在创建一级对话框时,我将启动二级对话框的实例。

我已经用很多不同的方式做到了这一点,但它总是显示为第 2 级低于第 1 级,然后通常会发生 zorder 反转(它们翻转位置)。偶尔不会发生反转,但是如果我点击所有者,所有者立即跳到zorder的顶部。

下面是一个小例子的主要部分来说明这种情况:

const unsigned short WMA_DIALOGACTION        = WM_APP+1;
// Button event handler for the 0th level
void CdialogcallingdialogsDlg::OnBnClickedDlgLvl1()
{
    CDlgLvl1 x(this);
    x.DoModal();
}
BEGIN_MESSAGE_MAP(CDlgLvl1, CDialogEx)
    ON_WM_WINDOWPOSCHANGED()
    ON_MESSAGE(WMA_DIALOGACTION, OnDialogAction)
END_MESSAGE_MAP()

void CDlgLvl1::OnWindowPosChanged(WINDOWPOS* lpwndpos)
{
    if (!m_shownDlg) {
        m_shownDlg = true;
        PostMessage(WMA_DIALOGACTION);
    }
}

// Level 1 dialog opening up level 2 dialog
LRESULT CDlgLvl1::OnDialogAction(WPARAM wParam, LPARAM lParam)
{
    ShowWindow(SW_SHOW);
    CDlgLvl2 x(this);
    x.DoModal();
    return LRESULT();
}
BEGIN_MESSAGE_MAP(CDlgLvl2, CDialogEx)
    ON_WM_WINDOWPOSCHANGING()
END_MESSAGE_MAP()

// Level 2 dialog offseting its position
void CDlgLvl2::OnWindowPosChanging(WINDOWPOS* lpwndpos)
{
    ASSERT(lpwndpos->hwnd == m_hWnd);
    // Offset dialog to see the problem of dlg2 showing up below dlg1
    if (!(lpwndpos->flags & SWP_NOMOVE)) {
        lpwndpos->x += 10;
        lpwndpos->y += 10;
    }
}

在示例中,您单击主对话框中的按钮。然后启动 CDlgLvl1,然后启动 CDlgLvl2。这些对话框是默认对话框,除了此处显示的消息处理和主应用程序对话框上的按钮。仔细看就可以看出反转了

我做错了什么?也许有更好的方法来做到这一点?

以防万一,问题在 Windows 10 下更为明显,在 Windows 8.1 上似乎不可见。

可以从我的 git 存储库中获取解决方案的副本:

https://github.com/Ma-XX-oN/dialog-calling-dialogs.git

我刚刚在对话框中添加了一些位图来真正显示问题,但我还没有在我的 8.1 机器上测试过。

我记录了它是如何弹出的,这是该记录的第 0、2 和 3 帧:

第0帧

第2帧

第3帧

如您所见,在第 2 帧中 LVL1 出现在 LVL2 之上,然后在第 3 帧中翻转位置。

可以找到完整视频here

使用这个示例项目,我无法复制 LVL1 并保持在 LVL2 之上,但我相信没有发生 zorder 反转的行为是某种竞争条件。

我在 Visual Studio 2019 年尝试了您的项目:

我运行它处于DEBUG模式并且工作正常。第三个对话显示为第二个对话的子对话(即具有正确的 ZORDER)。 RELEASE构建也是如此。

参见:https://www.dropbox.com/s/8f5z5ltq3vfc10r/Test.mp4?dl=0

更新

如果我的 类 之一有一个计时器并且我这样做了:

void CChristianLifeMinistryEditorDlg::OnTimer(UINT_PTR nIDEvent)
{
    READYSTATE eState = READYSTATE_UNINITIALIZED;

    if (nIDEvent == PRINT_PREVIEW_TIMER)
    {
        eState = m_pPrintHtmlPreview->GetReadyState();
        if (eState == READYSTATE_COMPLETE)
        {
            KillTimer(m_uPreviewTimer);
            PostMessage(WM_COMMAND,
                MAKELONG(IDC_BUTTON_PRINT_PREVIEW2, BN_CLICKED));
        }
    }

    CResizingDialog::OnTimer(nIDEvent);
}

你可以调整原理,然后模拟按下按钮显示第二个下一个对话框。可能有用。

可能是因为一级对话框在它有机会显示自己之前创建了二级对话框引起的。是的,这可能因系统而异。没有真正的解决办法,但我会建议使用计时器的解决方法。下面是一些代码。

CDlgLvl1 的头文件:

class CDlgLvl1 : public CDialogEx
{
.
.
.
protected:
    UINT_PTR nIDTimer = 0; // Add this
};

CDlgLvl1 的源文件:

BEGIN_MESSAGE_MAP(CDlgLvl1, CDialogEx)
    .
    .
    ON_MESSAGE(WMA_DIALOGACTION, OnDialogAction)
    ON_WM_TIMER()
END_MESSAGE_MAP()


BOOL CDlgLvl1::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    nIDTimer = SetTimer(1, 250, NULL);
    return TRUE;
}

void CDlgLvl1::OnTimer(UINT_PTR nIDEvent)
{
    if (nIDTimer && nIDEvent == nIDTimer)
    {
        KillTimer(nIDTimer);
        nIDTimer = 0;
        PostMessage(WMA_DIALOGACTION);
        return;
    }

    CDialogEx::OnTimer(nIDEvent);
}

LRESULT CDlgLvl1::OnDialogAction(WPARAM wParam, LPARAM lParam)
{
    CDlgLvl2 x(this);
    x.DoModal();
    return 0;
}

您提供的防止第二次 window 被多次显示的机制(m_shownDlg 变量)已被 nIDTimer 检查所取代。

请试验计时器的经过值。我建议的时间(250 - 1/4 秒)对大多数系统来说都可以,而且用户察觉不到。

我是在 SO 编辑器中写的,没有在 VS 中进行实际测试(因此它可能包含一些语法错误 - 如果是,请修复它们)。

注意:如果您只想设置第二个对话框的位置,则无需覆盖OnWindowPosChanging()。它是相对于其父级的,因此您可以简单地设置对话框资源的 X PosY Pos 属性。

问题是在启用windows "transition animation"时引起的。 WM_WINDOWPOSCHANGED 动画结束前正在发送。

要解决此问题,您只需禁用对话框的转换即可:

BOOL CDlgLvl2::OnInitDialog()
{
    BOOL res = CDialogEx::OnInitDialog();
    BOOL attrib = TRUE;
    DwmSetWindowAttribute(m_hWnd, DWMWA_TRANSITIONS_FORCEDISABLED, &attrib, sizeof(attrib));
    return res;
}


如果您不想禁用转换,则必须等到此转换完成。我不知道如何检测它或如何确定过渡时间。好像是250毫秒。 SystemParametersInfo(SPI_SETMENUSHOWDELAY...) 给出的值为 400 毫秒,这似乎有点太长了。

假设我们知道时间,使用SetTimer到运行转换结束后的函数:

BOOL CDlgLvl2::OnInitDialog()
{
    BOOL res = CDialogEx::OnInitDialog();
    ANIMATIONINFO info = { sizeof info };
    SystemParametersInfo(SPI_GETANIMATION, sizeof(ANIMATIONINFO), &info, 0);
    if (info.iMinAnimate)
        SetTimer(1, 250, nullptr);
    else
        SetTimer(1, 1, nullptr);
    return res;
}

void CDlgLvl2::OnTimer(UINT_PTR nIDEvent)
{
    CDialogEx::OnTimer(nIDEvent);
    if(nIDEvent == 1)
    {
        KillTimer(nIDEvent);
        CDlgLvl2(this).DoModal();//note, PostMessage is not needed in SetTimer
    }
}