MDI Applications – Part 3
If an array of data is to be serialized in a windows application, then one approach is to create a CArray of pointers (of the user defined class type) in the document class. The drawback with this technique is that you need to write the global SerializeElements function for the CArray object. Another approach is to use CTypedPtrList template class instead of CArray to create objects of user defined class in the document class. For example, to create an array of CStudent objects in the document class, you would declare the following in the document class header file:
CTypedPtrList<CObList,CStudent*> m_setofStudents;
The first parameter in the template declaration is
the base class for the collection, and the second parameter is the type for
parameters and return values. Because
all CTypedPtrList functions are inline, use of this template does not
significantly affect the size or speed of your code. Lists derived from CObList can be serialized, but those derived
from CPtrList cannot. When a CTypedPtrList object is deleted, or
when its elements are removed, only the pointers are removed, not the entities
they reference. Thus you should delete the user defined class objects referred
to by the CTypedPtrList in the DeleteContents member function of the
document class. Recall that DeleteContents is called by the framework
when the document is closed.
You need to
include <afxtempl.h> to use CTypedPtrList class. The class provides SetAt
and GetAt member functions to set or access the element at a particular
position. You can iterate through the array by using GetHead (to get to
the start element) and GetNext (to get to the next element) member
functions.
Example: Let us
create an MDI application that will allow us to draw as many circles as we want
and also be able to serialize them properly. Create an MDI application, name
the project mdidraw3.
Add a header file called CCircle to the project. Type the following
code in it.
//--------CCircle.h------------
#ifndef
CCIRCLE_H
#define
CCIRCLE_H
class
CCircle : public CObject {
DECLARE_SERIAL(CCircle);
int m_x1, m_y1, m_x2, m_y2; //
bounding rectangle;
public:
CCircle();
CCircle(int x1,int y1, int x2,
int y2);
void set(int x1, int y1, int x2,
int y2);
int get_x1() const;
int get_y1() const;
int get_x2() const;
int get_y2() const;
void Draw(CDC * pDC); // drawing
code
virtual void Serialize(CArchive
& ar); // override serialize
};
#endif
Add another cpp filed CCircle to the project, type the following code
in it.
//-----CCircle.cpp-----------
#include
"stdafx.h"
#include
"CCircle.h"
IMPLEMENT_SERIAL
(CCircle,CObject,1);
CCircle::CCircle()
{m_x1=0; m_y1=0; m_x2 = 100; m_y2=100; }
CCircle::CCircle(int
x1, int y1, int x2, int y2) {
m_x1 = x1; m_y1 = y1; m_x2 = x2;
m_y2 = y2;
}
void
CCircle::set(int x1, int y1, int x2, int y2) {
m_x1 = x1; m_y1 = y1; m_x2 = x2;
m_y2 = y2;
}
int
CCircle::get_x1() const { return m_x1;}
int
CCircle::get_y1() const { return m_y1;}
int
CCircle::get_x2() const { return m_x2;}
int
CCircle::get_y2() const { return m_y2;}
void
CCircle::Draw(CDC *pDC) {
CRect rb(m_x1,m_y1,m_x2,m_y2);
pDC->Ellipse(&rb);
}
void
CCircle::Serialize(CArchive & ar) {
if (ar.IsLoading())
ar >> m_x1
>> m_y1 >> m_x2 >> m_y2 ;
else
ar << m_x1
<< m_y1 << m_x2 << m_y2 ;
}
Add the CTypedPtrList declaration to the document header file.
…..
#pragma once
#endif // _MSC_VER >
1000
#include
"CCircle.h"
#include
<afxtempl.h>
class CMdidraw3Doc :
public CDocument
{
protected: // create from
serialization only
CMdidraw3Doc();
DECLARE_DYNCREATE(CMdidraw3Doc)
// Attributes
public:
CTypedPtrList<CObList,CCircle*>
m_setofCircles;
Add the Type code for the Serialize function in the document class cpp file.
void CMdidraw3Doc::Serialize(CArchive& ar)
{
if
(ar.IsStoring())
{
//
TODO: add storing code here
}
else
{
//
TODO: add loading code here
}
m_setofCircles.Serialize(ar);
}
Add the DeleteContents function to the document class by using the class wizard.
Type the following code in it.
void CMdidraw3Doc::DeleteContents()
{
//
TODO: Add your specialized code here and/or call the base class
while
(!m_setofCircles.IsEmpty())
{
delete
m_setofCircles.RemoveHead();
}
CDocument::DeleteContents();
}
Add the following declarations to the view class header file.
class CMdidraw3View : public CView
{
protected: // create from serialization only
CMdidraw3View();
DECLARE_DYNCREATE(CMdidraw3View)
// Attributes
public:
CMdidraw3Doc*
GetDocument();
int
m_x1, m_y1, m_x2, m_y2;
CCircle
m_Circle;
int
m_track;
Using the classwizard, add the WM_LBUTTONUP, WM_MOUSEMOVE, OnUpdate and OnInitialUpdate handlers to the view class. You also need to include the CCircle.h file in the view class cpp file.
void CMdidraw3View::OnLButtonUp(UINT nFlags, CPoint
point)
{
//
TODO: Add your message handler code here and/or call default
m_track = 0;
CMdidraw3Doc
* pDoc = GetDocument();
pDoc->SetModifiedFlag(TRUE);
pDoc->UpdateAllViews(this);
CView::OnLButtonUp(nFlags,
point);
}
void CMdidraw3View::OnLButtonUp(UINT nFlags, CPoint
point)
{
//
TODO: Add your message handler code here and/or call default
m_track =
0;
CMdidraw3Doc
* pDoc = GetDocument();
pDoc->SetModifiedFlag(TRUE);
pDoc->UpdateAllViews(this);
CView::OnLButtonUp(nFlags,
point);
}
void CMdidraw3View::OnMouseMove(UINT nFlags, CPoint
point)
{
//
TODO: Add your message handler code here and/or call default
if
(m_track == 1) {
CRect
Rectold(m_x1,m_y1, m_x2, m_y2);
m_x2
= point.x; m_y2 = point.y;
CRect
Rectnew(m_x1,m_y1, m_x2,m_y2);
CMdidraw3Doc
* pDoc = GetDocument();
CCircle
* pC = pDoc->m_setofCircles.GetTail();
pC->set(m_x1,m_y1,m_x2,
m_y2);
InvalidateRect(Rectold,TRUE);
InvalidateRect(Rectnew,TRUE);
}
CView::OnMouseMove(nFlags,
point);
}
void CMdidraw3View::OnUpdate(CView* pSender, LPARAM
lHint, CObject* pHint)
{
//
TODO: Add your specialized code here and/or call the base class
InvalidateRect(NULL);
}
void CMdidraw3View::OnInitialUpdate()
{
CView::OnInitialUpdate();
//
TODO: Add your specialized code here and/or call the base class
InvalidateRect(NULL);
}
Write the following code in the OnDraw function in the view class cpp file.
void CMdidraw3View::OnDraw(CDC* pDC)
{
CMdidraw3Doc*
pDoc = GetDocument();
ASSERT_VALID(pDoc);
//
TODO: add draw code for native data here
POSITION
pos = pDoc->m_setofCircles.GetHeadPosition();
CCircle
* pC;
while
(pos) {
pC=pDoc->m_setofCircles.GetNext(pos);
pC->Draw(pDC);
}
}
Build and test the application. You should be able to draw multiple circles, and be able to serialize the document successfully.
Selecting Shapes:
We will modify the above application so that the user can select a shape by clicking the left mouse button on a drawn shape. Since the left mouse click is being used to draw the circle right now, we will add two toolbar buttons (and corresponding menu items) so that the user could either be in the circle drawing mode or the shape selection mode.
Create two new toolbar buttons by modifying the IDR_MAINFRAME toolbar resource. The first toolbar button will have a shape of circle on it (give it an ID of ID_DRAW_CIRCLE), the second button will have a shape of arrow on it (give it an ID of ID_MODE_ARROW). Add two data members to the view class header file as shown below:
CString m_SelectedShapeOnToolbar;
int
m_Color; // selected shape will have a gray background
rather than white
In the constructor for the view class (in the view class cpp file), set the m_SelectedShapeOnToolbar to “CIRCLE”.
CMdidraw3View::CMdidraw3View()
{
//
TODO: add construction code here
m_SelectedShapeOnToolbar
= "CIRCLE";
m_Color
= WHITE_BRUSH;
}
Add two menu items under a new top level menu called “Drawing Mode”. The menu items are to be called “Draw Circle” (ID_DRAW_CIRCLE) and “Arrow Mode” (ID_MODE_ARROW). Write the following code in the event handlers for these menu items.
void CMdidraw3View::OnDrawCircle()
{
m_SelectedShapeOnToolbar
= "CIRCLE";
m_Color
= WHITE_BRUSH;
}
void CMdidraw3View::OnModeArrow()
{
//
TODO: Add your command handler code here
m_SelectedShapeOnToolbar
= "ARROW";
}
Modify the WM_LBUTTONDOWN and WM_LBUTTONUP handlers to as shown below.
void CMdidraw3View::OnLButtonDown(UINT nFlags,
CPoint point)
{
//
TODO: Add your message handler code here and/or call default
if
(m_SelectedShapeOnToolbar == "CIRCLE") {
m_x1
= point.x;
m_y1
= point.y;
m_x2
= point.x+1;
m_y2
= point.y+1;
CMdidraw3Doc
* pDoc = GetDocument();
CCircle
* pCircle = new CCircle(m_x1,m_y1,m_x2,m_y2);
pDoc->m_setofCircles.AddTail(pCircle);
m_track
= 1;
}
if
(m_SelectedShapeOnToolbar == "ARROW") // selection mode
{
m_Color
= WHITE_BRUSH;
CClientDC
dc(this);
OnDraw(&dc); // direct call, clear old selections
// InvalidateRect(NULL) will not work
// because of delay in message posting
//
cycle through all objects to see if mouse pressed
//
point is in one of the shapes that have been drawn
CMdidraw3Doc*
pDoc = GetDocument();
ASSERT_VALID(pDoc);
POSITION
pos = pDoc->m_setofCircles.GetHeadPosition();
CCircle
* pC;
int
x1, y1, x2, y2;
CRect
rect_shape;
while
(pos) {
pC=pDoc->m_setofCircles.GetNext(pos);
x1
= pC->get_x1();
y1
= pC->get_y1();
x2
= pC->get_x2();
y2
= pC->get_y2();
rect_shape.top
= y1;
rect_shape.left
= x1;
rect_shape.bottom
= y2;
rect_shape.right
= x2;
if
(rect_shape.PtInRect(point)) // mouse click is inside rectangle
{
m_Color
= GRAY_BRUSH;
InvalidateRect(rect_shape,TRUE);
break;
}
}
}
CView::OnLButtonDown(nFlags,
point);
}
void CMdidraw3View::OnLButtonUp(UINT nFlags, CPoint
point)
{
//
TODO: Add your message handler code here and/or call default
if
(m_SelectedShapeOnToolbar != "ARROW") {
m_track
= 0;
CMdidraw3Doc
* pDoc = GetDocument();
pDoc->SetModifiedFlag(TRUE);
pDoc->UpdateAllViews(this);
}
CView::OnLButtonUp(nFlags,
point);
}
Add the UP_COMMAND_UI event handlers for the two menu items ID_DRAW_CIRCLE and ID_MODE_ARROW, the code is shown below.
void CMdidraw3View::OnUpdateDrawCircle(CCmdUI*
pCmdUI)
{
//
TODO: Add your command update UI handler code here
if
(m_SelectedShapeOnToolbar == "CIRCLE")
pCmdUI->SetCheck(1);
else
pCmdUI->SetCheck(0);
}
void CMdidraw3View::OnUpdateModeArrow(CCmdUI*
pCmdUI)
{
//
TODO: Add your command update UI handler code here
if
(m_SelectedShapeOnToolbar == "ARROW")
pCmdUI->SetCheck(1);
else
pCmdUI->SetCheck(0);
}
The modified OnDraw code is shown below.
void CMdidraw3View::OnDraw(CDC* pDC)
{
CMdidraw3Doc*
pDoc = GetDocument();
ASSERT_VALID(pDoc);
//
TODO: add draw code for native data here
pDC->SelectStockObject(m_Color);
POSITION
pos = pDoc->m_setofCircles.GetHeadPosition();
CCircle
* pC;
while
(pos) {
pC=pDoc->m_setofCircles.GetNext(pos);
pC->Draw(pDC);
}
m_Color =
WHITE_BRUSH; // reset default color
}
Build and test the application. You should be able to select any object by clicking on it. The selected shape will be painted in gray background.
Moving Shapes:
In order to be able to select and then move a shape to a different position, we need to keep track of the exact coordinates at which the mouse was clicked on the shape so that as we move it, the relative position of the shape is kept proportionate to the click on the shape. For this purpose we will add the following data members to the view class header file.
class CMdidraw3View : public CView
{
protected: // create from serialization only
CMdidraw3View();
DECLARE_DYNCREATE(CMdidraw3View)
// Attributes
public:
CMdidraw3Doc*
GetDocument();
int
m_x1, m_y1, m_x2, m_y2;
CCircle
m_Circle;
int
m_track;
CString
m_SelectedShapeOnToolbar;
int
m_Color; // for setting color of the
selected object
//
-----following members added for moving shapes
CPoint
m_ptTopLeft ; // top left of the
selected shape, logical
CSize
m_szSizeOffset; // offset from top left to capture point, device
CSize
m_szbRect; // logical size of
bounding rect
BOOL
m_bCaptured; // mouse capture
CCircle
* m_pC;
The modified WM_LBUTTONDOWN, WM_MOUSEMOVE and WM_LBUTTONUP handlers are shown below.
void CMdidraw3View::OnLButtonDown(UINT nFlags,
CPoint point)
{
//
TODO: Add your message handler code here and/or call default
CClientDC
dc(this);
OnPrepareDC(&dc); //
important for CScrollView
CPoint
pos_mouse_logical(point);
dc.DPtoLP(&pos_mouse_logical);
// mouse pos in logical coord.
if
(m_SelectedShapeOnToolbar == "CIRCLE") {
m_x1
= pos_mouse_logical.x; // all shape
data is stored in
m_y1
= pos_mouse_logical.y; // in logical
coordinates
m_x2
= pos_mouse_logical.x+1;
m_y2
= pos_mouse_logical.y+1;
CMdidraw3Doc
* pDoc = GetDocument();
CCircle
* pCircle = new CCircle(m_x1,m_y1,m_x2,m_y2);
pDoc->m_setofCircles.AddTail(pCircle);
m_track
= 1;
}
if
(m_SelectedShapeOnToolbar == "ARROW") // selection mode
{
m_Color
= WHITE_BRUSH;
OnDraw(&dc); // direct call, clear old selections
// InvalidateRect(NULL) will not work
// because of delay in message posting
//
cycle through all objects to see if mouse pressed
//
point is in one of the shapes that have been drawn
CMdidraw3Doc*
pDoc = GetDocument();
ASSERT_VALID(pDoc);
POSITION
pos = pDoc->m_setofCircles.GetHeadPosition();
CCircle
* pC;
CRect
rect_shape;
while
(pos) {
pC=pDoc->m_setofCircles.GetNext(pos);
rect_shape.top
= pC->get_y1();
rect_shape.left
= pC->get_x1();
rect_shape.bottom
= pC->get_y2();
rect_shape.right
= pC->get_x2();
m_szbRect.cx
= rect_shape.Width();
m_szbRect.cy
= rect_shape.Height();
m_ptTopLeft.x
= pC->get_x1();
m_ptTopLeft.y
= pC->get_y1();
m_pC
= pC;
dc.LPtoDP(rect_shape);
// now in device coord.
CRgn
creg;
creg.CreateEllipticRgnIndirect(rect_shape);
if
(creg.PtInRegion(point)) // mouse click is inside rectangle
{
SetCapture();
// mouse messages to sent to this window
m_bCaptured
= TRUE;
CPoint
ptTopLeft(m_ptTopLeft); // logical units
dc.LPtoDP(&ptTopLeft);
m_szSizeOffset
= point - ptTopLeft; // device units
::SetCursor(::LoadCursor(NULL,IDC_CROSS));
m_Color
= GRAY_BRUSH;
InvalidateRect(rect_shape,TRUE);
break;
}
}
}
CView::OnLButtonDown(nFlags,
point);
}
void CMdidraw3View::OnLButtonUp(UINT nFlags, CPoint
point)
{
//
TODO: Add your message handler code here and/or call default
if
(m_bCaptured) {
::ReleaseCapture();
m_bCaptured
= FALSE;
}
if
(m_SelectedShapeOnToolbar != "ARROW") {
m_track
= 0;
CMdidraw3Doc
* pDoc = GetDocument();
pDoc->SetModifiedFlag(TRUE);
pDoc->UpdateAllViews(this);
InvalidateRect(NULL);
// draw this view
}
CView::OnLButtonUp(nFlags,
point);
}
void CMdidraw3View::OnMouseMove(UINT nFlags, CPoint
point)
{
//
TODO: Add your message handler code here and/or call default
CClientDC
dc(this);
OnPrepareDC(&dc);
CMdidraw3Doc
* pDoc = GetDocument();
if
(m_bCaptured) { //
true when moving a shape
CRect
rectold(m_ptTopLeft,m_szbRect);
dc.LPtoDP(rectold);
// before the move
InvalidateRect(rectold,TRUE); // erase old shape
// Invalidate rects are always in device
units
m_ptTopLeft
= point - m_szSizeOffset;
dc.DPtoLP(&m_ptTopLeft);
CRect
rectnew(m_ptTopLeft,m_szbRect);
m_pC->set(rectnew.left,rectnew.top,rectnew.right,rectnew.bottom);
dc.LPtoDP(rectnew);
InvalidateRect(rectnew,TRUE);
// draw new shape
pDoc->SetModifiedFlag(TRUE);
pDoc->UpdateAllViews(this);
}
if
(m_track == 1) // when drawing a new shape
{
CRect
Rectold(m_x1,m_y1, m_x2, m_y2);
CPoint
p2(point);
dc.DPtoLP(&p2); // now mouse pos. in logical coordinates
m_x2
= p2.x; m_y2 = p2.y;
CRect
Rectnew(m_x1,m_y1, m_x2,m_y2);
CCircle
* pC = pDoc->m_setofCircles.GetTail();
pC->set(m_x1,m_y1,m_x2,
m_y2);
dc.LPtoDP(Rectold);
dc.LPtoDP(Rectnew);
InvalidateRect(Rectold,TRUE);
InvalidateRect(Rectnew,TRUE);
}
CView::OnMouseMove(nFlags,
point);
}
The constructor for the view class will look as:
CMdidraw3View::CMdidraw3View()
{
//
TODO: add construction code here
m_SelectedShapeOnToolbar
= "CIRCLE";
m_Color
= WHITE_BRUSH;
m_bCaptured
= FALSE;
}
The OnDraw will be as shown below:
void CMdidraw3View::OnDraw(CDC* pDC)
{
CMdidraw3Doc*
pDoc = GetDocument();
ASSERT_VALID(pDoc);
//
TODO: add draw code for native data here
pDC->SelectStockObject(m_Color);
POSITION
pos = pDoc->m_setofCircles.GetHeadPosition();
CCircle
* pC;
while
(pos) {
pC=pDoc->m_setofCircles.GetNext(pos);
pC->Draw(pDC);
}
m_Color = WHITE_BRUSH; // reset default color
}
Build and test the application. You should be able to move shapes by pressing the mouse and dragging it.
Providing Zooming:
The important concept in zooming (as well as scrolling) is that there is a virtual drawing surface referred to as the “window”. We can view a portion of this surface through a “viewport”. The ratio of “window extent” to “viewport extent” provides the zooming factor if the mapping mode is set to MM_ISOTROPIC or MM_ANISOTROPIC. There are member functions in the device context class that allow us to set the viewport extent and the window extent e.g.,
PDC->SetMapMode(MM_ISOTROPIC);
pDC->SetWindowExtent(1000,1000);
pDC->SetViewportExtent(200,200); // now 1000 logical units correspond to 200
pixels
….
PDC->MoveTo(0,-100);
PDC->LineTo(100,-100); // this line will be 20 pixels long
PDC->SetViewportExtent(400,400);
PDC->MoveTo(0,-100);
PDC->LineTo(100,-100); // Now this line will be 40 pixels long
Note that window in general refers to logical units and viewport refers to device units. MM_ISOTROPIC performs a uniform scaling based on the ratio of the window extent to viewport extent whereas MM_ANISOTRPIC allows us to pick a different scaling for the X-axis and the Y-axis.
We will modify the mdidraw3 application to incorporate zooming in it. Add a data member to the view class header file:
float m_zoomFactor;
Add another line to the view class constructor to initialize the zoom factor as shown below:
m_zoomFactor = 1.0;
Modify the IDR_MDIDRATYPE menu resource to add a top level menu item called “Zoom”. Add three menu items under it call “Zoom in” (ID_ZOOM_IN), “Zoom out” (ID_ZOOM_OUT), and “No Zoom” (ID_ZOOM_ONE). The menu handlers for the COMMAND and UPDATE_COMMAND_UI events for these menus are shown below.
void CMdidraw3View::OnZoomIn()
{
//
TODO: Add your command handler code here
if
(m_zoomFactor < 3.0) {
m_zoomFactor
= m_zoomFactor * 1.25; // 25 % zooming
Invalidate();
}
}
void CMdidraw3View::OnZoomOut()
{
//
TODO: Add your command handler code here
if
(m_zoomFactor > 0.5) {
m_zoomFactor
= m_zoomFactor / 1.25; // 25 % reduction in zooming
Invalidate();
}
}
void CMdidraw3View::OnUpdateZoomIn(CCmdUI* pCmdUI)
{
//
TODO: Add your command update UI handler code here
if
(m_zoomFactor >= 3.0)
pCmdUI->Enable(FALSE);
else
pCmdUI->Enable(TRUE);
}
void CMdidraw3View::OnUpdateZoomOut(CCmdUI* pCmdUI)
{
//
TODO: Add your command update UI handler code here
if
(m_zoomFactor < 0.5)
pCmdUI->Enable(FALSE);
else
pCmdUI->Enable(TRUE);
}
void CMdidraw3View::OnZoomOne()
{
//
TODO: Add your command handler code here
m_zoomFactor
= 1.0;
Invalidate();
}
Through the class wizard, add the OnPrepareDC function to the view class. Type the following code in it.
void CMdidraw3View::OnPrepareDC(CDC* pDC,
CPrintInfo* pInfo)
{
//
TODO: Add your specialized code here and/or call the base class
pDC->SetMapMode(MM_ISOTROPIC);
float
zoomFactor = m_zoomFactor;
if
(pDC->IsPrinting())
zoomFactor
= 1; // print without zooming
CSize
logsize; // width,height of display in millimeters
logsize.cx=10*pDC->GetDeviceCaps(HORZSIZE);
logsize.cy
= 10*pDC->GetDeviceCaps(VERTSIZE);;
pDC->SetWindowExt(logsize);
CSize
devsize; // width, height of display in pixels
devsize.cx=zoomFactor*pDC->GetDeviceCaps(HORZRES);
devsize.cy
= zoomFactor*pDC->GetDeviceCaps(VERTRES);;
pDC->SetViewportExt(devsize);
CView::OnPrepareDC(pDC,
pInfo);
}
Build and test the application. You should be able to zoom in and out.
Providing Scrolling View:
MFC provides a CScrollView class that takes care of the primary scrolling tasks such as creating the virtual window and mapping it to a viewport, changing the viewport origin when trying to scroll etc..
Create a new MDI project called mdidraw4. When going through the application wizard steps, choose the base class for view to be CScrollView instead of the default CVew.
CScrollView does not support zooming, so we will not add any zooming code. Majority of the code here will be same as the mdidraw3 application (except for no zooming related handlers). The OnInitialUpdate for the view class will now look as:
void CMdidraw4View::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
CSize
sizeTotal;
//
TODO: calculate the total size of this view
m_SelectedShapeOnToolbar
= "ARROW";
CSize
sztotal(20000,30000); // total virtual size
CSize
szpage(sztotal.cx/4, sztotal.cy/4);
CSize
szline(sztotal.cx/200, sztotal.cy/200);
SetScrollSizes(MM_HIMETRIC,sztotal,szpage,szline);
// InvalidateRect(NULL);
}
Rest of the code in this application is same as mdidraw3.
Build and test the application.