Child Windows, applied to dragging an image
In this lecture I will develop an example application in which you can drag an image around the main window. The technique is good for dragging anything at all--we may as well drag the Mona Lisa bitmap.
The plan is this:
- The image to be dragged occupies a small window of its own.
- The small window is dragged by calling MoveWindow in OnMouseMove.
- Windows takes care of erasing the small window at its old location and drawing it at its new location.
- The drawing actually takes place in the OnDraw of the small window, not of the main window.
The small window will be a child window of the main window. The problems to be addressed in writing this program include:
- creating a child window
- displaying a bitmap or other image in a child window
- where to handle WM_MOUSEMOVE
- converting between client coordinates, screen coordinates, and coordinates in the child window
- understanding what coordinates MoveWindow needs
Creating a Window
In all our applications so far, the only windows have been those provided for us by MFC. However, many applications require additional windows.
When you want to create a child window in MFC, you begin by deriving a class from CWnd. Call it CCarrier, since we will use windows of this type to carry something that is being dragged. To create it, choose Project | Add Class. You’ll see:
Note that there are THREE “MFC Class” choices—be sure to get the right one. The status line says Adds an MFC class as shown in the picture. The next dialog lets you name your class and select a base class. The default base class is CWnd, which is what we want, so just name your class. Note that the names of your files are automatically generated and the initial C is dropped—you’ll need to remember the name of your header file so you can use the correct #include command.
It should go without saying (but does not, apparently) that whenever you are going to mention this class, the file that mentions it has to have an #include command near the top of the file (but after stdafx.h). If you omit that, you get a message that confuses some students—learn to recognize the cause of that message, when the name of your class is not recognized because the header file hasn’t been #included.
As usual with MFC, it is not enough to declare a variable of type CCarrier, although we begin by declaring m_Carrier of type CCarrier. We must also create an underlying Win32 object by calling m_Carrier.Create(...).
The Create function of the CWnd class is essentially the same as the CreateWindow function in the Win32 API. It has seven parameters, so you will have to look it up when you want to use it.
The first parameter is a string defining the Win32 "window class". There are predefined values for various controls. You could use one of those values to put a button on your view window, for instance. To create a simple child window, just pass NULLhere, and you'll get usable defaults. You can also create and "register" your own window classes, which you might do (for instance to specify a non-default background brush or cursor).
The second parameter is the window name. This will appear in the title bar if the window has one. Ours won't have a title bar, so it doesn't matter what we pass here.
The third parameter specifies the window style. This is done by combining predefined flags with the bitwise OR operator. The possible flags are on the next slide--wait a bit for details.
The next parameter of Create is a rectangle, specifing in the parent window's coordinates where we want the child window positioned and how big we want it to be. If it is a top-level window we use screen coordinates. Note that this parameter is a Win32 RECT object, not a CRect, even though Create is an MFC function.
The next parameter is a pointer to the parent window. If we calling Create in the view class, and we want to create a child of the view class, we can just pass this here.
The next parameter is an ID number. We can just make one up. In case the window class had been the button class, or some other control class, this ID number would be used when the control sends messages to its parent. Remember that controls in a dialog have ID numbers? This is where they are used, when the controls are created as children of the dialog window.
The last parameter is used only if you want to tinker with the creation of view or document windows. Don't ever do that. Just pass NULL here.
Window Styles
- WS_BORDER Creates a window that has a border.
- WS_CAPTION Creates a window that has a title bar (implies the WS_BORDER style). Cannot be used with the WS_DLGFRAME style.
- WS_CHILD Creates a child window. Cannot be used with the WS_POPUP style.
- WS_CLIPCHILDREN Excludes the area occupied by child windows when you draw within the parent window. Used when you create the parent window.
- WS_CLIPSIBLINGS Clips child windows relative to each other; that is, when a particular child window receives a paint message, the WS_CLIPSIBLINGS style clips all other overlapped child windows out of the region of the child window to be updated. (If WS_CLIPSIBLINGS is not given and child windows overlap, when you draw within the client area of a child window, it is possible to draw within the client area of a neighboring child window.) For use with the WS_CHILD style only.
- WS_DISABLED Creates a window that is initially disabled.
- WS_DLGFRAME Creates a window with a double border but no title.
- WS_GROUP Specifies the first control of a group of controls in which the user can move from one control to the next with the arrow keys. All controls defined with the WS_GROUP style FALSE after the first control belong to the same group. The next control with the WS_GROUP style starts the next group (that is, one group ends where the next begins).
- WS_HSCROLL Creates a window that has a horizontal scroll bar.
- WS_MAXIMIZE Creates a window of maximum size.
- WS_MAXIMIZEBOX Creates a window that has a Maximize button.
- WS_MINIMIZE Creates a window that is initially minimized. For use with the WS_OVERLAPPED style only.
- WS_MINIMIZEBOX Creates a window that has a Minimize button.
- WS_OVERLAPPED Creates an overlapped window. An overlapped window usually has a caption and a border.
- WS_OVERLAPPEDWINDOW Creates an overlapped window with the WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, and WS_MAXIMIZEBOX styles.
- WS_POPUP Creates a pop-up window. Cannot be used with the WS_CHILD style.
- WS_POPUPWINDOW Creates a pop-up window with the WS_BORDER, WS_POPUP, and WS_SYSMENU styles. The WS_CAPTION style must be combined with the WS_POPUPWINDOW style to make the Control menu visible.
- WS_SYSMENU Creates a window that has a Control-menu box in its title bar. Used only for windows with title bars.
- WS_TABSTOP Specifies one of any number of controls through which the user can move by using the TAB key. The TAB key moves the user to the next control specified by the WS_TABSTOP style.
- WS_THICKFRAME Creates a window with a thick frame that can be used to size the window.
- WS_VISIBLE Creates a window that is initially visible.
- WS_VSCROLL Creates a window that has a vertical scroll bar.
So what will our call to Create look like?
m_Carrier.Create( NULL, "Carrier",
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS,
// child window, initially visible, don't overwrite siblings
r, // initial size and location
this, //pointer to parent
5 // totally arbitrary ID number
);
Of course, we will have to initialize the RECT r first.
1. Create the project as an SDI application, accepting all other defaults.
- Add imported the monalisa.bmp file as a bitmap and name it IDB_MONALISA
- Create a new class called CCarrier derived from CWnd. Choose Project | Add Class,
Note, the picture shows "Carrier" instead of "CCarrier", but that results in files named "arrier.cpp" and "arrier.h".
- Add a member variable m_Carrier of type CCarrier to the view class. This will be the child window that will hold the bitmap and get dragged.
- In general child window classes should contain member variables to hold their data. In our case, add a member variable m_pPicture of type pointer to CBitmap.
5. We need to arrange that m_pPicture points back to m_picture in the view class. For this I added a member function SetPicture to the CCarrier class,
void Carrier::SetPicture(CBitmap *p)
{ m_ppicture = p;}
6. Where should we put our call to m_Carrier.Create? Not in the constructor of the view class, since at that time the view window itself is not yet constructed, so you can't construct a child of it. You must map the WM_CREATE message. Here's the code:
BOOL CDragDemoView::Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext)
{
RECT r;
BOOL a;
BITMAP bm;
m_picture.LoadBitmap(IDB_MONALISA);
m_picture.GetBitmap(&bm);
m_Carrier.SetPicture(&m_picture);
r.left = r.top = 50;
r.right = r.left + bm.bmWidth;
r.bottom = r.top + bm.bmHeight;
a = CWnd::Create(lpszClassName, lpszWindowName, dwStyle, rect,
pParentWnd, nID, pContext);
if(!a)
return FALSE;
return m_Carrier.Create(NULL,"Carrier",
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS,
// child window, initially visible, don't overwrite siblings
r, // initial size and;pcatopm
this, //pointer to parent
5 // totally arbitrary ID number
);
}
- Finally we map the WM_PAINT message in the CCarrier class:
void Carrier::OnPaint()
{
CPaintDC dc(this); // device context for painting
// display the bitmap pointed to by m_ppicture
DisplayBitmap2(m_ppicture,&dc);
}
DisplayBitmap2 was written in the first BitmapDemo program in the previous lecture. Just to remind you:
void DisplayBitmap2( CBitmap *p, CDC *pDC)
// same as DisplayBitmap but also sets the mapping mode properly
{ CDC dcMemory;
CSize ptSize, ptOrg;
BITMAP bm;
dcMemory.CreateCompatibleDC(pDC);
dcMemory.SelectObject(p);
dcMemory.SetMapMode(pDC->GetMapMode());
/* Get the height and width of the bitmap */
p->GetBitmap(&bm);
ptSize.cx = bm.bmWidth;
ptSize.cy = bm.bmHeight;
pDC->DPtoLP(&ptSize);
ptOrg.cx = 0;
ptOrg.cy = 0;
dcMemory.DPtoLP(&ptOrg);
pDC->BitBlt(0,0,ptSize.cx,ptSize.cy,&dcMemory,ptOrg.cx,ptOrg.cy,SRCCOPY);
}
In the other project it had 50,50 in the BitBlt call; here we need 0,0.
At this point you can build and run the program and see the Mona Lisa:
Now for making her draggable.
When the user clicks on the Mona Lisa, which window will get the mouse messages? Not the view class--it will be the carrier window that gets them!
- Following the techniques in our previous dragging experience, we set a variable m_dragging in OnLButtonDown, and call SetCapture, and unset that variable and call ReleaseCapture in OnLButtonUp.
- Then, in OnMouseMove, all we have to do is call MoveWindow. The only complication is to get the coordinates changed to coordinates in the view window. This is a common problem when working with child windows, and there are several functions to assist you. For example, ScreenToClient, ClientToScreen, which convert from coordinates in a window to and from screen coordinates. We could use ClientToScreen to get from coordinates in the child window to coordinates in the screen, and then ScreenToClient to get to coordinates in the parent (view) window. But the mother of all coordinate-changing functions is MapWindowPoints, which lets you change coordinates directly from one window to any other window.
CWnd::MapWindowPoints
voidMapWindowPoints(CWnd*pwndTo,LPRECTlpRect)const;
voidMapWindowPoints(CWnd*pwndTo,LPPOINTlpPoint,UINTnCount)const;
Parameters
pwndTo
Identifies the window to which points are converted. If this parameter is NULL, the
points are converted to screen coordinates.
lpRect
Specifies the rectangle whose points are to be converted. The first version of this function is available only for Windows 3.1 and later.
lpPoint
A pointer to an array of _POINT structures that contain the set of points to be converted.
nCount
Specifies the number of POINT structures in the array pointed to by lpPoint.
Remarks
Converts (maps) a set of points from the coordinate space of the CWnd to the coordinate space of another window.
void Carrier::OnLButtonDown(UINT nFlags, CPoint point)
{ m_dragging = TRUE;
m_cursor = point;
SetCapture();
}
void Carrier::OnLButtonUp(UINT nFlags, CPoint point)
{
m_dragging = FALSE;
ReleaseCapture();
}
void Carrier::OnMouseMove(UINT nFlags, CPoint point)
{
if(!m_dragging)
return;
CRect r;
GetClientRect(&r);
MapWindowPoints(GetParent(),(LPPOINT) &r,2); // one rectangle is two points
CSize change = point - m_cursor;
r += change;
MoveWindow(&r);
}
This code works like a charm! Too bad this isn't a live demo--you could see the Mona Lisa being dragged around, without the slightest flicker.
Moreover, by this method we could put any number of child windows on the screen at once, and drag around any of them we liked, and NOT have to keep track of where they are or which one is being clicked in. Windows will do all that for us. What will happen when we try to drag one on top of another? That's when the WS_CLIPSIBLINGS style comes into play. If I try to drag the Mona Lisa through Einstein, Einstein will not be overwritten. It will look like the Mona Lisa goes behind him. Without it, you will probably get a "WM_PAINT loop" crash, as each child window keeps overwriting the other and generating another WM_PAINT. I didn't try this, but I have seen similar things in the past.