Introduction
The standard practice for moving a window is to drag it's title bar. This is
handled for us by the operating system itself. But there are some applications
that allow us to move the entire window by dragging anywhere within it's body.
Sometimes it is pretty annoying when that happens, but there might be occasions
where this is required. For dialog based applications there is a despicable
trick which we can use to achieve this. The secret is to handle WM_NCHITTEST
and
to trick the OS into thinking that the mouse click or movement was made in the title
bar. I explain the technique in more detail later down the article. But in SDI
applications, there is a slight issue. Because all the mouse clicks and moves
are handled by the view class! And if you attempt to use the same technique you
used in the case of the dialog based application, the results will be wretchedly
peculiar. Of course this just means we'll have to write that much more code. I
show how this is done later down the article. I haven't tried out this technique
on MDI applications, but my guess is that with a little bit of adjusting it
should work fine on MDI applications as well.
Of course there is always more than one way to skin a cat, and it's not much
different when it comes to programming. The same goes for this article too.
Roman Nurik, has explained a much easier way to accomplish the same as I have. I
have included this much easier method in terms of number of lines and effort at
the end of the article. I could have put them on the top of the article too, but
I wanted the flow of the article to be from good to better. In fact after
Roman's method, I have also provided a solution offered by Albert Ling, which in
my opinion is the most innovative of all the methods discussed here.
Draggable dialogs
The WM_NCHITTEST
message will sent to the dialog when the mouse
is moved through it or a mouse click is made on it. Of course since we are using
MFC, we have the OnNcHitTest
function which handles the
WM_NCHITTEST
message. The function returns one of several enumerated
values, each of which indicates where the mouse action took place. Now one of
these enumerated values is HTCAPTION
which indicates that the mouse
action took place on the title bar. So what we do is to verify if the mouse is
currently within the client area of the window, and if it is within the client
area of the window, we check if the mouse is down through a flag that is set and
unset from the OnLButtonDown
and OnLButtonUp
handlers.
If all our checks are passed, we return HTCAPTION,
thus fooling the
OS into thinking that the action is taking place on the title bar. It is very
important to verify that the mouse action is within the dialog's client area,
otherwise any buttons we have on the title bar, like the close and maximize
buttons will be rendered useless.
UINT CDragDialogDlg::OnNcHitTest(CPoint point)
{
CRect r;
GetClientRect(&r);
ClientToScreen(&r);
if(r.PtInRect(point))
{
if(m_mousedown)
{
return HTCAPTION;
}
}
return CDialog::OnNcHitTest(point);
}
m_mousedown
is a bool
member variable which is set and unset from the OnLButtonDown
and OnLButtonUp
handlers. We also set m_mousedown
to true
in the
OnInitDialog
handler
because otherwise the first drag attempt will fail, as m_mousedown
will still be
false
when the OnNcHitTest
handler is called.
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);
}
Draggable SDI windows
As I have mentioned earlier we cannot use the technique we used for dialog
based applications here. The whole issue here is that the CView
derived class is a wrapper for the view window and not for the main window of
the application. The main window of an SDI application is wrapped by the
CMainFrame
class which is derived from CFrameWnd
by the App
wizard. All mouse clicks and movements within the view are handled by the view
class and not by the frame window class. Well, this time we use another solution
to settle our issue. We do the most obvious thing to do in the situation, which
is that we move the window using code.
void CDragSDIView::OnLButtonDown(UINT nFlags,
CPoint point)
{
m_mousedown = true;
ClientToScreen(&point);
m_lastpoint = point;
CView::OnLButtonDown(nFlags, point);
}
Just as in the previous case, we override OnLButtonDown
. We set the
m_mousedown
flag to true
. This time as you'll notice we also have a
CPoint
member variable called m_lastpoint
in our CView
derived class. We set
m_lastpoint
to the CPoint
passed to us in OnLButtonDown
.
void CDragSDIView::OnLButtonUp(UINT nFlags,
CPoint point)
{
m_mousedown = false;
CView::OnLButtonUp(nFlags, point);
}
OnLButtonUp
is also overridden. But here we haven't done
anything different from what we did in the case of the dialog based application.
All our window moving work is done in the OnMouseMove
function
which we override as shown below.
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);
}
Well, we first check to see if the point is within the client area of the
view window. If it is, then we check the m_mousedownflag
is
true
. If m_mousedownflag
is true
, we
figure out the current window coordinates by calling GetWindowRect
on the main frame window. Now we call MoveWindow
on the main frame
window and pass it the new values which we calculate using the CPoint
passed to us by OnMouseMove
, the m_lastpoint
member
variable and the CRect
obtained by calling GetWindowRect
on the main frame window. And finally we set m_lastpoint
to the new
CPoint
.
A much easier way - Roman Nurik
As I have mentioned in the introduction, there are multiple ways to skin
cats, though why anyone would ever want to skin cats beat me. Cat skins are not
exactly useful in my opinion. Alright, let's get to Roman's method for making
draggable windows. First override WM_LBUTTONDOWN
and then
send a WM_NCLBUTTONDOWN
message to the main window of your
application. For dialog apps, this will be the dialog window itself and for SDI
apps, this will be your CMainFrame
window. The
WM_NCLBUTTONDOWN
message is sent to a window when a left mouse click is made on
the non client area of the window. The wParam
specifies the
hit-test enumeration value. We pass HTCAPTION
and the lParam
specifies the cursor position, which we pass as a 0 so that it's sure to be in
the title bar. Thus we end up with one of the following implementations.
ReleaseCapture();
POINT pt;
GetCursorPos(&pt);
POINTS pts = {pt.x, pt.y};
::SendMessage(m_hWnd,WM_NCLBUTTONDOWN,HTCAPTION,(LPARAM)&pts);
ReleaseCapture();
::SendMessage(m_hWnd,WM_NCLBUTTONDOWN,HTCAPTION,0);
m_hWnd
is the window handle of the main application window. With
MFC, you usually have a CWnd*
and not an HWND
. In such cases you can do an MFC
CWnd
version of the SendMessage
call.
pWnd->SendMesage(WM_NCLBUTTONDOWN,HTCAPTION,0);
Well, that was sure easier than the previously discussed techniques, wasn't
it and thanks goes to Roman Nurik for this really cool tip. But then I guess
each method would have it's pros and cons which may make themselves visible at
random.
Another way - Albert Ling
Well, we come to that matter of cats and skins again. Here is yet another
solution suggested by Albert Ling, that seems to me to be the best of all the
methods we have investigated. He overrides OnNcHitTest
and then
calls the base class implementation. He checks the value returned by the base
class implementation and if it is HTCLIENT
, he returns
HTCAPTION
. This one is for dialog based applications.
UINT CYourDlg::OnNcHitTest(CPoint point)
{
UINT hit = CDialog::OnNcHitTest(point);
if ( hit == HTCLIENT )
{
return HTCAPTION;
}
else
return hit;
}
If you thought Albert Ling's solution for dialog based apps was cool, you are
yet to see his solution for SDI apps. It was simply amazing! This is what he
did. He overrides OnNcHitTest
in the CView
derived class and calls the base
class implementation first. If the base class call returns HTCLIENT
, he returns
HTTRANSPARENT
. Now, HTTRANSPARENT
indicates that the mouse action was on a window
that's covered by another window, and thus the message gets sent to the
underlying window in the thread, which in our case would be the main frame
window. Thus we override OnNcHitTest
in CMainFrame
and call the base class
method. If the base class method returns HTCLIENT
, then we return
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;
}
Conclusion
Well, when I wrote this article I was under the thoroughly mistaken
impression that my method was the only way to go about doing it. That's when
Roman came and proved me wrong by offering his solution which was quite simpler
to implement. Just when I was trying to be complacent about all this by talking
about the cat-skins in the old adage, out comes Albert Ling with yet another
amazing solution. Now I live in constant fear of being bombarded with other
solutions and feel like a haunted man. Heheh. No, actually I don't. Was just
kidding. If any of you have other solutions, feel free to suggest it here via
the forum, or email me directly so that we can make this a single point source
for all methods used for creating draggable windows.