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

一些方便的对话框技巧、提示和解决方法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (138投票s)

2002年1月9日

CPOL

9分钟阅读

viewsIcon

1269250

隐藏的模态对话框、窃取焦点、总在最前的对话框、全屏显示、展开和收缩对话框、移除任务栏图标、上下文相关帮助以及许多其他有用的提示和技巧。

引言

这些是我在使用对话框(无论是在基于对话框的应用程序还是在非基于对话框的应用程序中)时发现的一些技巧。其中一些可能看起来很简单,但却非常方便。我想与大家分享其中的一些技巧。

以隐藏状态启动模态对话框

你经常听到有人抱怨,尽管在他们的 OnInitDialog 函数中加入了 ShowWindow(SW_HIDE),但他们的模态对话框启动时仍然是可见的。问题在于,当 CDialog::OnInitDialog() 完成时,它会调用 ShowWindow(SW_SHOW)。因此,你的对话框又会变得可见。但是,正如所料,人们已经找到了解决方法。你需要这样做。

在你的对话框类中添加一个 BOOL 成员,并给它起个名字,比如 visible

现在,在你的对话框构造函数中,将 visible 设置为 false

visible = false;

接下来,你需要重写 WM_WINDOWPOSCHANGING。你可能需要更改消息筛选选项,才能让这个消息出现在类向导中。

void CTest_deleteDlg::OnWindowPosChanging(WINDOWPOS FAR* lpwndpos) 
{
    if(!visible)
        lpwndpos->flags &= ~SWP_SHOWWINDOW;

    CDialog::OnWindowPosChanging(lpwndpos);
}

就是这样。现在你的模态对话框实际上是以隐藏状态启动的。当你想要让它可见时,你需要这样做。

visible = true;
ShowWindow(SW_SHOW);

全屏对话框

有时你可能需要制作一个全屏对话框——即一个填满整个显示器的对话框。这其实很容易实现。你需要移除标题栏和边框,我们可以通过移除 WS_CAPTIONWS_BORDER 样式来做到。然后我们使用 HWND_TOPMOST 调用 SetWindowPos,并将对话框的大小调整到填满整个屏幕。只需将以下代码放入你的 OnInitDialog 函数中即可。

BOOL CFullScrDlgDlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    //...

    int cx, cy; 
    HDC dc = ::GetDC(NULL); 
    cx = GetDeviceCaps(dc,HORZRES) + 
        GetSystemMetrics(SM_CXBORDER); 
    cy = GetDeviceCaps(dc,VERTRES) +
        GetSystemMetrics(SM_CYBORDER); 
    ::ReleaseDC(0,dc); 

    // Remove caption and border
    SetWindowLong(m_hWnd, GWL_STYLE, 
        GetWindowLong(m_hWnd, GWL_STYLE) & 
    (~(WS_CAPTION | WS_BORDER))); 

    // Put window on top and expand it to fill screen
    ::SetWindowPos(m_hWnd, HWND_TOPMOST, 
        -(GetSystemMetrics(SM_CXBORDER)+1), 
        -(GetSystemMetrics(SM_CYBORDER)+1), 
        cx+1,cy+1, SWP_NOZORDER); 

    //...

    return TRUE; 
}

如何在 2K/XP 上窃取焦点

我敢打赌,你有时会怀念过去的日子,那时一个简单的 SetForegroundWindow 就能让你的对话框获得焦点。唉!现在在 2K/XP 系统中,情况有所改变,如果你尝试一个简单的 SetForegroundWindow,最终只会让任务栏图标闪烁几次(我没数过,但感觉是闪烁三次)。这可不是你想要的效果,对吧?幸运的是,有办法能让你的对话框显示到前台。

诀窍是使用 AttachThreadInput 将拥有当前前台窗口的线程附加到我们的线程,然后调用 SetForegroundWindow,再使用 AttachThreadInput 分离已附加的线程。很酷,是吧?

//Attach foreground window thread
//to our thread
AttachThreadInput(
    GetWindowThreadProcessId(
        ::GetForegroundWindow(),NULL),
    GetCurrentThreadId(),TRUE);

//Do our stuff here ;-)
SetForegroundWindow();
SetFocus(); //Just playing safe

//Detach the attached thread
AttachThreadInput(
    GetWindowThreadProcessId(
        ::GetForegroundWindow(),NULL),
    GetCurrentThreadId(),FALSE);

让你的对话框保持在最前

你是否见过那些带有“总在最前”选项的程序?嗯,令人难以置信的是,你只需一行代码就能让你的对话框保持在最前。只需将下面这行代码放入你的对话框类的 OnInitDialog() 函数中。

SetWindowPos(&this->wndTopMost,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE);

我们基本上是使用 SetWindowPos 函数来改变我们对话框窗口的 Z 顺序。我们通过将对话框移动到 Z 顺序的顶部,使其保持在所有其他窗口之上。现在即使你激活了其他窗口,我们的窗口也会保持在最前。但我建议你在这样做之前,要确保你清楚地知道自己在做什么,因为如果人们在想把你的窗口移开时却做不到,这可能会惹恼他们。

展开和收缩你的对话框

我敢打赌你见过这样的程序,它们的对话框开始时是一个尺寸。它们会有一个叫做“展开视图”或者“高级”的按钮,当你点击那个按钮时,对话框会漂亮地展开,显示出一些之前隐藏的子控件。它们可能还有一个叫做“隐藏详情”的按钮,当你点击它时,对话框会收缩回原来较小的尺寸,隐藏那些展开时显示的额外控件。这可以很容易地通过使用 SetWindowPos 来实现,正如你所见,这是一个非常有用的函数。

假设你有两个按钮,“MakeSmall”和“Expand”。那么你需要把这些代码放到它们的点击处理函数中。当然,你需要用你自己的值替换 cxcy 参数。

void CTest_deleteDlg::OnMakeSmall() 
{
    SetWindowPos(NULL,0,0,200,200,SWP_NOZORDER|SWP_NOMOVE);	
}

void CTest_deleteDlg::OnExpand() 
{
    SetWindowPos(NULL,0,0,500,300,SWP_NOZORDER|SWP_NOMOVE);		
}

另外,请记住在你的对话框的 OnInitDialog() 函数中调用 OnMakeSmall(),这样你的对话框窗口就会以收缩后的尺寸启动。相反,你也可以在 OnInitDialog() 中调用 OnExpand() 使其以展开状态启动。有时你想使用同一个按钮,相应地改变按钮标题,并使用一个布尔标志来决定何时展开和何时收缩。

顺便说一下,这里有一个来自 Thomas Freudenberg 的额外提示。它涉及到这样一个事实:即使在收缩后,隐藏的控件仍然可以通过键盘访问,因此你可能需要使用 EnableWindow 来禁用这些控件。我要感谢 Thomas 提出这个建议。

关于“展开和收缩你的对话框”,你忽略了一些事情。看起来你更喜欢使用鼠标而不是键盘(你是一个所谓的 Mausschubser(德语,意为鼠标推手之类))。当对话框收缩后,你仍然可以使用 Tab 键切换到对话框外的控件。我建议对所有相应的控件调用 EnableWindow(fExtracted)。

让你的对话框回到桌面内

有时用户可能会在屏幕上移动对话框,直到它部分移出桌面。而你可能希望将对话框完全带回视野。也可能出现这样的情况:你在较高的分辨率下进行开发,在你的机器上对话框显示得很好很完整,但最终用户可能使用较低的屏幕分辨率,从而导致对话框的一部分超出了屏幕。同样,你真的希望确保对话框是完全可见的。嗯,信不信由你,这只需要一行代码就可以完成。

SendMessage(DM_REPOSITION);

很流畅吧?请记住,这个消息只对顶级对话框有效,对子对话框无效。

为你的对话框添加最小化/最大化按钮

如果你想在设计时这样做,你只需要相应地设置属性。但如果出于某种原因你需要在运行时这样做,那么你需要这样做。重写 OnCreate() 并在其中添加这段代码。请注意,将此代码放在 OnInitDialog() 中会有一个奇怪的副作用。按钮确实会显示出来,包括最大化和最小化按钮;但它们是假的,意味着它们不能正常工作。我的猜测是,在 OnInitDialog() 中更改对话框的样式已经太晚了。

int CTest_deleteDlg::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
    if (CDialog::OnCreate(lpCreateStruct) == -1)
        return -1;	
    // TODO: Add your specialized creation code here
    SetWindowLong(this->m_hWnd,GWL_STYLE,
        GetWindowLong(this->m_hWnd,GWL_STYLE) | 
            WS_MINIMIZEBOX | WS_MAXIMIZEBOX);	
    return 0;
}

另外请注意,lpCreateStruct 只是传入的原始 CREATESTRUCT 的一个副本。因此,更改那里的样式位没有效果。并且出于某些令人费解(至少对我来说)的原因,PreCreateWindow 对于模态对话框永远不会被调用,因此我们也不能像对视图窗口或框架窗口那样在那里更改样式位。

更改鼠标光标 - 来自 Andrew Peace

嗯,我要感谢 Andrew Peace 的这个建议。有时你可能需要在对话框中更改默认的鼠标光标。你需要做的是重写 OnSetCursor,设置一个新的光标,然后返回,而不调用基类函数,如下所示。我曾尝试过这样做但失败了,因为我一直在调用基类。再次感谢 Andrew Peace 为我指明了正确的方向。

BOOL CTest_deleteDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) 
{
    // TODO: Add your message handler code here and/or call default
    SetCursor(AfxGetApp()->LoadStandardCursor(IDC_UPARROW));
    // Now we return instead of calling the base class
    return 0;	
    // return CDialog::OnSetCursor(pWnd, nHitTest, message);
}

更改对话框的背景和控件文本颜色

有一个经常被忽视的 CWinApp 成员函数,SetDialogBkColor,它允许我们做到这一点。该函数接受两个 COLORREF 作为其参数。第一个 COLORREF 是对话框的背景色。第二个 COLORREF 将是静态文本、复选框和单选按钮控件的颜色。它不会影响编辑框和按钮控件。应用程序弹出的所有对话框和消息框都将全局使用这些颜色。将此代码放入你派生自 CWinApp 的类的 InitInstance() 中。记住要在实例化你的 CDialog 派生对象之前调用该函数。

//Red background with Green colored controls
SetDialogBkColor(RGB(255,0,0),RGB(0,255,0)); 

这个函数现在已经过时了!它甚至可能无法工作!我将在下次更新中用正确的方法更新这个技巧!

移除基于对话框的应用的任务栏图标

有时你可能出于各种原因想要创建隐形的对话框应用程序。嗯,它们在严格意义上并非隐形,因为对话框是可见的。但让我们假设你出于某种原因不希望它们有任务栏图标。要实现这一点,你需要这样做。我们首先创建一个不可见的顶层框架窗口。以下是需要放入你派生自 CWinApp 的类中的代码。

CFrameWnd *abc=new CFrameWnd();
abc->Create(0,0,WS_OVERLAPPEDWINDOW);
CNoTaskBarIconDlg dlg(abc);
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
}
else if (nResponse == IDCANCEL)
{
}
delete abc;

现在我们需要修改对话框窗口的样式。所以把这段代码放到你派生自 CDialog 的类的 OnInitDialog 中。我们需要移除 WS_EX_APPWINDOW 样式。

BOOL CNoTaskBarIconDlg::OnInitDialog()
{
    CDialog::OnInitDialog();
    ModifyStyleEx(WS_EX_APPWINDOW,0);

    SetIcon(m_hIcon, TRUE);  // Set big icon
    SetIcon(m_hIcon, FALSE); // Set small icon
	
    // TODO: Add extra initialization here
	
    return TRUE;  // return TRUE  unless you set the focus to a control
}

上下文相关帮助 - P J Arends

这个来自 Pete Arends 的技巧向你展示了如何在对话框中实现上下文相关帮助。首先你需要做的是确保对话框的标题栏上有一个问号。为此,你需要在 OnInitDialog 中这样做,如下所示。

BOOL HelpDialog::OnInitDialog() 
{
    //blah blah blah
    //blah blah blah
    ModifyStyleEx(0, WS_EX_CONTEXTHELP);
    return CDialog::OnInitDialog();
}

OnHelpInfo(...) 可能会在两种不同情况下被调用。第一种情况是用户按下 F1 键。这通常应该会弹出对话框的主帮助窗口。另一种情况是用户点击问号,然后点击对话框中的某个控件。或者,用户也可以右键单击一个控件,并从弹出菜单中选择“这是什么?”选项。当然,你必须自己处理弹出菜单的逻辑。因此,我们需要处理这两种情况,如下所示。

BOOL HelpDialog::OnHelpInfo(HELPINFO* pHelpInfo) 
{
    short state = GetKeyState (VK_F1);
    if (state < 0)   // F1 key is down, get help for the dialog
        return CDialog::OnHelpInfo(pHelpInfo);
    else
    {    // F1 key not down, get help for specific control
        if (pHelpInfo->dwContextId)
            WinHelp (pHelpInfo->dwContextId, 
                HELP_CONTEXTPOPUP);
        return TRUE;
    }
}

请记住,控件必须有关联的帮助 ID。这可以通过 [属性 --- 常规选项卡 --- 勾选帮助 ID] 来完成。当然,你还需要为你的程序编写帮助文件。谢谢。

主对话框关闭后显示 MessageBox

有时,需要在基于对话框的应用程序中主对话框被关闭后显示某种消息框。但你会发现你的消息框从未显示。有趣的是,如果你在那里设置一个断点,程序在调试时确实会中断。问题在于,在基于对话框的应用程序中,CWinThread::m_pMainWnd 就是对话框窗口本身,当对话框被关闭时,主窗口被销毁,程序退出。解决方法是注释掉将 m_pMainWnd 设置为对话框窗口的那一行代码。

BOOL CTestApp::InitInstance()
{

    // ....

    CTestDlg dlg;

    /* Comment out the following line */
    //m_pMainWnd = &dlg;

    int nResponse = dlg.DoModal();
    if (nResponse == IDOK)
    {
        // TODO: Place code here to handle 
        // when the dialog is
        //  dismissed with OK
    }
    else if (nResponse == IDCANCEL)
    {
        // TODO: Place code here to handle 
        // when the dialog is
        //  dismissed with Cancel
    }

    
    MessageBox(NULL,"Some message","Title",0);
        
    return FALSE;
}

当然,你绝不能在你的应用程序的任何地方调用 AfxGetMainWnd。因为 AfxGetMainWnd 会盲目地返回你的 CWinApp 派生类的 m_pMainWnd 成员。否则,你可以做的是将你的 CDialog* 保存到另一个成员变量中,比如说 m_pMainWnd2,然后编写一个函数 AfxGetMainWnd2,它只返回 m_pMainWnd2,如果你真的非常想使用 AfxGetMainWnd 的话。

最后更新

由于我非常频繁地更新这篇文章,我没有保留完整的历史记录,但我会在这里提及最后一次更新的信息。以下一个或多个技巧是在2002年9月18日的最后一次更新中添加的。

  • 全屏对话框
  • 如何在 2K/XP 上窃取焦点
© . All rights reserved.