创建可拖动的窗口——SDI和对话框






4.97/5 (16投票s)
演示如何创建可以拖动的窗口,不仅可以通过标题栏拖动,还可以在窗口体内的任何位置拖动。
引言
移动窗口的标准做法是拖动其标题栏。这由操作系统本身为我们处理。但是,有些应用程序允许我们通过拖动其主体内的任何位置来移动整个窗口。有时这很烦人,但有时可能需要这样做。对于基于对话框的应用程序,我们可以使用一个卑鄙的技巧来实现这一点。诀窍是处理 WM_NCHITTEST
并欺骗操作系统,让它认为鼠标点击或移动发生在标题栏上。我将在文章后面更详细地解释这种技术。但在 SDI 应用程序中,存在一个小问题。因为所有鼠标点击和移动都由视图类处理!如果您尝试使用与基于对话框的应用程序中相同的技术,结果将非常奇怪。当然,这意味着我们必须编写更多的代码。我将在文章后面展示如何做到这一点。我还没有在 MDI 应用程序上尝试过这项技术,但我的猜测是,只需稍作调整,它在 MDI 应用程序上应该也能正常工作。
当然,解决问题的方法不止一种,编程也是如此。本文也是如此。Roman Nurik 已经解释了一种更简单的方法来完成我所做的事情。我在文章的最后包含了这种在代码行数和工作量方面都更容易的方法。我也可以把它们放在文章的开头,但我希望文章的流程是从好到更好。事实上,在 Roman 的方法之后,我还提供了一个 Albert Ling 提供的解决方案,在我看来,这是所有讨论方法中最具创新性的。
可拖动的对话框
当鼠标移动到对话框上或在对话框上单击鼠标时,会向对话框发送 WM_NCHITTEST
消息。当然,由于我们使用的是 MFC,我们有处理 WM_NCHITTEST
消息的 OnNcHitTest
函数。该函数返回几个枚举值之一,每个值都表示鼠标动作发生的位置。其中一个枚举值是 HTCAPTION
,它表示鼠标动作发生在标题栏上。所以我们要做的就是验证鼠标当前是否在窗口的客户区内,如果它在窗口的客户区内,我们就通过一个标志来检查鼠标是否被按下,该标志从 OnLButtonDown
和 OnLButtonUp
处理程序设置和取消设置。如果所有检查都通过,我们就返回 HTCAPTION
,从而欺骗操作系统,让它认为该操作发生在标题栏上。验证鼠标动作是否在对话框的客户区内非常重要,否则我们标题栏上的任何按钮,如关闭和最大化按钮,都将失效。
UINT CDragDialogDlg::OnNcHitTest(CPoint point) { CRect r; GetClientRect(&r); ClientToScreen(&r); //Chk to see if the mouse is within //the dialog client area if(r.PtInRect(point)) { if(m_mousedown) { return HTCAPTION; } } return CDialog::OnNcHitTest(point); }
m_mousedown
是一个 bool
成员变量,它由 OnLButtonDown
和 OnLButtonUp
处理程序设置和取消设置。我们还将在 OnInitDialog
处理程序中将 m_mousedown
设置为 true
,因为否则第一次拖动尝试将失败,因为当调用 OnNcHitTest
处理程序时 m_mousedown
仍然是 false
。
void CDragDialogDlg::OnLButtonDown(UINT nFlags, CPoint point) { m_mousedown = true; CDialog::OnLButtonDown(nFlags, point); } void CDragDialogDlg::OnLButtonUp(UINT nFlags, CPoint point) { m_mousedown = false; CDialog::OnLButtonUp(nFlags, point); }
可拖动的 SDI 窗口
正如我在前面提到的,我们不能在这里使用用于基于对话框的应用程序的技术。这里的主要问题是 CView
派生类是视图窗口的包装器,而不是应用程序主窗口的包装器。SDI 应用程序的主窗口由 App Wizard 派生的 CMainFrame
类包装。视图内的所有鼠标点击和移动都由视图类处理,而不是由框架窗口类处理。好吧,这次我们使用另一种解决方案来解决这个问题。我们做出了在这种情况下最显而易见的事情,那就是用代码移动窗口。
void CDragSDIView::OnLButtonDown(UINT nFlags, CPoint point) { m_mousedown = true; ClientToScreen(&point); m_lastpoint = point; CView::OnLButtonDown(nFlags, point); }
与上一种情况一样,我们重写 OnLButtonDown
。我们将 m_mousedown
标志设置为 true
。这次,您会注意到我们在 CView
派生类中还有一个名为 m_lastpoint
的 CPoint
成员变量。我们将 m_lastpoint
设置为传递给 OnLButtonDown
的 CPoint
。
void CDragSDIView::OnLButtonUp(UINT nFlags, CPoint point) { m_mousedown = false; CView::OnLButtonUp(nFlags, point); }
OnLButtonUp
也被重写了。但是,在这里我们所做的与在基于对话框的应用程序中所做的没有什么不同。我们所有的窗口移动工作都在我们下面显示的 OnMouseMove
函数中完成。
void CDragSDIView::OnMouseMove(UINT nFlags, CPoint point) { CRect r; GetClientRect(&r); ClientToScreen(&r); ClientToScreen(&point); if(r.PtInRect(point)) { if(m_mousedown) { AfxGetMainWnd()->GetWindowRect(&r); AfxGetMainWnd()->MoveWindow( r.left - (m_lastpoint.x - point.x), r.top - (m_lastpoint.y - point.y), r.Width(),r.Height()); m_lastpoint = point; } } CView::OnMouseMove(nFlags, point); }
首先,我们检查该点是否在视图窗口的客户区内。如果是,我们就检查 m_mousedownflag
是否为 true
。如果 m_mousedownflag
为 true
,我们就通过调用主框架窗口的 GetWindowRect
来获取当前的窗口坐标。现在,我们调用主框架窗口的 MoveWindow
,并将通过调用主框架窗口的 GetWindowRect
获得的 CRect
、传递给 OnMouseMove
的 CPoint
以及 m_lastpoint
成员变量计算出的新值传递给它。最后,我们将 m_lastpoint
设置为新的 CPoint
。
一种更简单的方法 - Roman Nurik
正如我在引言中所提到的,有很多种方法可以解决问题,不过为什么有人会想去剥猫皮我倒是不知道。在我看来,猫皮并不是很有用。好了,让我们来看看 Roman 的可拖动窗口制作方法。首先重写 WM_LBUTTONDOWN
,然后向应用程序的主窗口发送一个 WM_NCLBUTTONDOWN
消息。对于对话框应用程序,这将是对话框窗口本身;对于 SDI 应用程序,这将是您的 CMainFrame
窗口。当在窗口的非客户区单击鼠标左键时,会向窗口发送 WM_NCLBUTTONDOWN
消息。wParam
指定命中测试枚举值。我们传递 HTCAPTION
,而 lParam
指定光标位置,我们将其传递为 0,以确保它在标题栏内。这样,我们就得到了以下实现之一。
ReleaseCapture(); //This is not compulsory
POINT pt;
GetCursorPos(&pt);
POINTS pts = {pt.x, pt.y};
::SendMessage(m_hWnd,WM_NCLBUTTONDOWN,HTCAPTION,(LPARAM)&pts);
ReleaseCapture(); //This is not compulsory ::SendMessage(m_hWnd,WM_NCLBUTTONDOWN,HTCAPTION,0);
m_hWnd
是主应用程序窗口的窗口句柄。在 MFC 中,您通常拥有一个 CWnd*
而不是 HWND
。在这种情况下,您可以执行 MFC CWnd
版本的 SendMessage
调用。
pWnd->SendMesage(WM_NCLBUTTONDOWN,HTCAPTION,0);
好吧,这肯定比之前讨论的技术更容易,不是吗?感谢 Roman Nurik 提供的这个非常棒的技巧。不过,我想每种方法都有其优点和缺点,这些优点和缺点可能会随机显现。
另一种方法 - Albert Ling
好吧,我们又回到了猫和皮毛的问题。这是 Albert Ling 提出的又一个解决方案,在我看来,这是我们研究过的所有方法中最好的。他重写了 OnNcHitTest
,然后调用基类的实现。他检查基类实现返回的值,如果它是 HTCLIENT
,他就返回 HTCAPTION
。这是为基于对话框的应用程序设计的。
UINT CYourDlg::OnNcHitTest(CPoint point) { UINT hit = CDialog::OnNcHitTest(point); if ( hit == HTCLIENT ) { return HTCAPTION; } else return hit; }
如果您认为 Albert Ling 的对话框应用程序解决方案很棒,那么您还没有看到他的 SDI 应用程序解决方案。简直太神奇了!他就是这样做的。他在 CView
派生类中重写了 OnNcHitTest
,并首先调用基类的实现。如果基类调用返回 HTCLIENT
,他就返回 HTTRANSPARENT
。现在,HTTRANSPARENT
表示鼠标动作发生在一个被另一个窗口覆盖的窗口上,因此消息会被发送到线程中的底层窗口,在我们的例子中是主框架窗口。因此,我们在 CMainFrame
中重写 OnNcHitTest
并调用基类方法。如果基类方法返回 HTCLIENT
,我们就返回 HTCAPTION
。
UINT CYourView::OnNcHitTest(CPoint point) { UINT hit = CView::OnNcHitTest(point); if (hit == HTCLIENT ) return HTTRANSPARENT; else { return hit; } } UINT CMainFrame::OnNcHitTest(CPoint point) { UINT hit = CFrameWnd::OnNcHitTest(point); if ( hit == HTCLIENT ) { return HTCAPTION; } else return hit; }
结论
好吧,当我写这篇文章时,我错误地认为我的方法是唯一的解决方案。这时 Roman 出现了,并提供了一个更简单的实现方法,证明我是错的。就在我试图通过谈论老话中的猫皮来安于现状时,Albert Ling 又提出了一个令人惊叹的解决方案。现在我生活在不断担心被其他解决方案轰炸的恐惧中,感觉自己像个被鬼缠身的人。呵呵。不,其实我不是。我只是在开玩笑。如果您有其他解决方案,请随时通过论坛在此处提出,或直接给我发邮件,以便我们能够将其打造成创建可拖动窗口的所有方法的单一信息源。