By Aymen++.
Introduction
This
program demonstrates how to draw
on the client area, it also demonstrates how to use the
mouse messages. I think the best way to learn programming
is programming, so,
let's begin our program now.
The drawing
flags
The user
chooses a drawing tool
to draw. For example
when the user wants to draw a
line, he chooses a line tool. So, we must
know which tool the user has selected, the bDrawFlag
for free drawing, bLineFlag
for the line, bRectangleFlag
for the rectangle, bEllipseFlag
for the
ellipse, and bFillFlag
if the user wants to
fill the shape with color.
When the
user clicks on a point, (Anchor
), and draws
the mouse pointer to a point (DrawTo
), and
we have the bLineFlag = TRUE
, we can easily
draw the line
when the user releases the mouse button.
Lets begin
now to build the program. Use AppWizard, in the File
menu, choose New, choose MFC
AppWizard (exe), and write the project name (Painter)
in the Project name text box. Then in the MFC
AppWizard-Step1 Dialog box, choose Single document
and click Finish.
In the
class view, double click on the CPainterView
class, the PainterView.h file will be opened. You
can see the CPainterView
class declaration.
Add these variables and MakeAllFlagsFalse()
method in the protected
section in the class declaration:
class CPainterView : public CView
{
.
.
protected:
CPoint Anchor;
CPoint DrawTo;
CPoint OldPoint;
BOOL bDrawFlag;
BOOL bLineFlag;
BOOL bRectangleFlag;
BOOL bEllipseFlag;
BOOL bFillFlag;
void MakeAllFlagsFalse();
.
.
};
These
variables when they initialized, there values are FALSE
.
But we need to make them all FALSE
if one
of them is changed by the MakeALlFlagsFalse()
method, we can simply write this method like this:
void CPainterView::MakeAllFlagsFalse()
{
bDrawFlag = FALSE;
bLineFlag = FALSE;
bRectangleFlag = FALSE;
bEllipseFlag = FALSE;
bFillFlag = FALSE;
}
Then we
must call this method in the CPainterView
class constructor:
CPainterView::CPainterView()
{
MakeAllFlagsFalse();
}
Building
the tools bar and the menu
In the
recourses view, double click the menu, and double click
the IDR_MAINFRAME
, add new menu, call it Tools,
give this menu five items, as illustrated bellow:

Then add 5
buttons in the tool bar. Every time you add a button in
the tool bar, the editor adds a blank button in the end
of the tool bar, as illustrated bellow:

Finally,
connect these buttons to the menu items. Double click on
the buttons on the tool bar, the Toolbar Button
Properties dialog box will be shown as illustrated
bellow. For example, in the ID assign ID_TOOLS_RECTANGLE
for the rectangle button.

You must
do the same for all the toolbar buttons.
The
interface between the flags and the drawing tools
Open the
class wizard, (CTRL+w), in the class name choose CPaintView
,
in the object ID's choose ID_TOOLS_DRAWFREEHAND
,
in the messages, choose command, and click add
function button, then click edit code. The
Class Wizard will add the OnToolsDrawfreehand()
function.
Add the
following code to OnToolsDrawfreehand()
:
void CPainterView::OnToolsDrawfreehand()
{
MakeAllFlagsFalse();
bDrawFlag = TRUE;
}
Do the
same for the following IDs:
-
ID_TOOLS_ELLIPSE
-
ID_TOOLS_FILLFIGURE
-
ID_TOOLS_LINE
-
ID_TOOLS_RECTANGLE
And add
the following code to there functions:
void CPainterView::OnToolsEllipse()
{
MakeAllFlagsFalse();
bEllipseFlag = TRUE;
}
void CPainterView::OnToolsFillfigure()
{
MakeAllFlagsFalse();
bFillFlag = TRUE;
}
void CPainterView::OnToolsLine()
{
MakeAllFlagsFalse();
bLineFlag = TRUE;
}
void CPainterView::OnToolsRectangle()
{
MakeAllFlagsFalse();
bRectangleFlag = TRUE;
}
Draw
check mark on the menu items
Open the
class wizard, choose the CPaintView
class
in the class name, in the ID's choose ID_TOOLS_DRAWFREEHAND
,
in the message choose UPDATE_COMMAND_UI
,
then press add function, and edit code, add this code to
the OnUpdateToolsDrawFreehand
function:
void CPainterView::OnUpdateToolsDrawfreehand(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(bDrawFlag);
}
Do the
same for the other IDs, and add the following code to
their functions:
00
void CPainterView::OnUpdateToolsEllipse(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(bEllipseFlag);
}
void CPainterView::OnUpdateToolsFillfigure(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(bFillFlag);
}
void CPainterView::OnUpdateToolsLine(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(bLineFlag);
}
void CPainterView::OnUpdateToolsRectangle(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(bRectangleFlag);
}
The mouse
events
When the
user clicks on the left button on the drawing area, WM_LBUTTON
message will be sent, we can add a message handler for
this message, in the class wizard. Be sure that the CPaintView
class has been chosen in the class name, in the object
ID's, choose CPaintView
. In the message,
scroll till you find the WM_LBUTTONDOWN
,
and press add function, and edit code. Add the following
code to the function handler for the WM_LBUTTONDOWN
message:
void CPainterView::OnLButtonDown(UINT nFlags, CPoint point)
{
Anchor.x = point.x;
Anchor.y = point.y;
.
.
}
The OnLButtonDown
takes the point parameter, this parameter is a CPoint
class, it saves the point that the user clicked.
Drawing
Lines
When the
user releases the mouse button, he creates a DrawTo
point, and we must register this point. When the user
releases the left mouse button, WM_LBUTTONUP
message will be sent. Open the class wizard to add a
function handler for the WM_LBUTTONUP
message as illustrated before, add the following code to
the message handler:
void CPainterView::OnLButtonUp(UINT nFlags, CPoint point)
{
DrawTo.x = point.x;
DrawTo.y = point.y;
.
.
}
It also
has the same parameter, CPoint
point.
Drawing
lines
How can we
get the device context? We can get it by the CClientDC
class which is inherited from the CDC
class. Add the following code to the OnLButtonUp
message handler:
void CPainterView::OnLButtonUp(UINT nFlags, CPoint point)
{
DrawTo.x = point.x;
DrawTo.y = point.y;
CClientDC* pDC = new CClientDC(this);
.
.
}
The this
points to the current object.
Then we
must check the bLineFlag
. If bLineFlag
is TRUE
, we are ready to draw
the line by moving
to the Anchor
point (the point that the
user clicked on), then we draw
the line to the DrawTo
point:
void CPainterView::OnLButtonUp(UINT nFlags, CPoint point)
{
.
.
CClientDC* pDC = new CClientDC(this);
if(bLineFlag){
pDC->MoveTo(Anchor.x, Anchor.y);
pDC->LineTo(DrawTo.x, DrawTo.y);
}
.
.
}
Drawing
rectangles
Drawing
rectangles is easy, by checking the bRectangleFlag
and adding this code:
void CPainterView::OnLButtonUp(UINT nFlags, CPoint point)
{
.
.
if(bLineFlag){
pDC->MoveTo(Anchor.x, Anchor.y);
pDC->LineTo(DrawTo.x, DrawTo.y);
}
If(bRectangleFlag){
pDC->SelectStockObject(NULL_BRUSH);
pDC->Rectangle(Anchor.x, Anchor.y, DrawTo.x, DrawTo.y);
}
.
.
}
That will
draw a transparent rectangle, because we selected the NULL_BRUSH
in the SelectStockObject
.
Add the
following code in the OnLButtonUp
handler
to draw ellipses:
.
.
if(bEllipseFlag){
pDC->SelectStockObject(NULL_BRUSH);
pDC->Ellipse(Anchor.x, Anchor.y, DrawTo.x, DrawTo.y);
}
.
.
Filling
the shapes with color
Till now
all the shapes are transparent, we can fill them by the FloodFill
method. I'll use BLACK_BRUSH
for the
filling brush. When we finish, the OnLButtonUp
handler must be like this:
Collapse
void CPainterView::OnLButtonUp(UINT nFlags, CPoint point)
{
DrawTo.x = point.x;
DrawTo.y = point.y;
CClientDC* pDC = new CClientDC(this);
if(bLineFlag){
pDC->MoveTo(Anchor.x, Anchor.y);
pDC->LineTo(DrawTo.x, DrawTo.y);
}
if(bRectangleFlag){
pDC->SelectStockObject(NULL_BRUSH);
pDC->Rectangle(Anchor.x, Anchor.y, DrawTo.x, DrawTo.y);
}
if(bEllipseFlag){
pDC->SelectStockObject(NULL_BRUSH);
pDC->Ellipse(Anchor.x, Anchor.y, DrawTo.x, DrawTo.y);
}
if(bFillFlag){
pDC->SelectStockObject(BLACK_BRUSH);
pDC->FloodFill(Anchor.x, Anchor.y, RGB(0, 0, 0));
}
delete pDC;
CView::OnLButtonUp(nFlags, point);
}
Free
drawing using the mouse
Add a
message handler for the WM_MOUSEMOVE
message:
void CPainterView::OnMouseMove(UINT nFlags, CPoint point)
{
CClientDC* pDC = new CClientDC(this);
.
.
delete pDC;
CView::OnMouseMove(nFlags, point);
}
Then we
must check the bDrawFlag
, if the mouse is
still moving and the left button still unreleased. We
can do that by the nFlags
and the MK_LBUTTON
:
void CPainterView::OnMouseMove(UINT nFlags, CPoint point)
{
CClientDC* pDC = new CClientDC(this);
if((nFlags && MK_LBUTTON) && bDrawFlag){
.
.
}
delete pDC;
CView::OnMouseMove(nFlags, point);
}
Suppose we
are drawing using
the free hand.
When we draw from the Anchor
point to the
current point, the current point will become the Anchor
point, like this:
void CPainterView::OnMouseMove(UINT nFlags, CPoint point)
{
CClientDC* pDC = new CClientDC(this);
if((nFlags && MK_LBUTTON) && bDrawFlag){
pDC->MoveTo(Anchor.x, Anchor.y);
pDC->LineTo(point.x, point.y);
Anchor.x = point.x;
Anchor.y = point.y;
}
delete pDC;
CView::OnMouseMove(nFlags, point);
}
Now our
program has been finished, but there are two problems.
When we draw we
can't see what we have been drawing,
and when we draw, and resize the window, every think
will disappear.
To solve
the first problem, we must draw from the Anchor
to the DrawTo
, then when the mouse moves
(the user is still clicking on the left button), we must
save the DrawTo
in OldPoint
and clear the line that we have been drawing and draw a
new line to the DrawTo
point.
We can do
that as follows:
void CPainterView::OnMouseMove(UINT nFlags, CPoint point)
{
int nOldMode;
.
.
if((nFlags && MK_LBUTTON) && bLineFlag){
nOldMode = pDC->GetROP2();
pDC->SetROP2(R2_NOT);
pDC->MoveTo(Anchor.x, Anchor.y);
pDC->LineTo(OldPoint.x, OldPoint.y);
.
.
}
.
.
}
Then draw
the new line, and copy the current point to the OldPoint
:
void CPainterView::OnMouseMove(UINT nFlags, CPoint point)
{
int nOldMode;
.
.
if((nFlags && MK_LBUTTON) && bLineFlag){
nOldMode = pDC->GetROP2();
pDC->SetROP2(R2_NOT);
pDC->MoveTo(Anchor.x, Anchor.y);
pDC->LineTo(OldPoint.x, OldPoint.y);
pDC->MoveTo(Anchor.x, Anchor.y);
pDC->LineTo(point.x, point.y);
OldPoint.x = point.x;
OldPoint.y = point.y;
pDC->SetROP2(nOldMode);
}
.
.
}
Note when
the user draws a line and passes over a black object,
the line will appear in a white color.
In the
same way, add the code for drawing the rectangles and
ellipses. The OnMouseMove
handler must be
like this:
Collapse
void CPainterView::OnMouseMove(UINT nFlags, CPoint point)
{
int nOldMode;
CClientDC* pDC = new CClientDC(this);
if((nFlags && MK_LBUTTON) && bDrawFlag){
pDC->MoveTo(Anchor.x, Anchor.y);
pDC->LineTo(point.x, point.y);
Anchor.x = point.x;
Anchor.y = point.y;
}
if((nFlags && MK_LBUTTON) && bLineFlag){
nOldMode = pDC->GetROP2();
pDC->SetROP2(R2_NOT);
pDC->MoveTo(Anchor.x, Anchor.y);
pDC->LineTo(OldPoint.x, OldPoint.y);
pDC->MoveTo(Anchor.x, Anchor.y);
pDC->LineTo(point.x, point.y);
OldPoint.x = point.x;
OldPoint.y = point.y;
pDC->SetROP2(nOldMode);
}
if((nFlags && MK_LBUTTON) && bRectangleFlag){
nOldMode = pDC->GetROP2();
pDC->SetROP2(R2_NOT);
pDC->SelectStockObject(NULL_BRUSH);
pDC->Rectangle(OldPoint.x, OldPoint.y, Anchor.x, Anchor.y);
pDC->Rectangle(Anchor.x, Anchor.y, point.x, point.y);
OldPoint.x = point.x;
OldPoint.y = point.y;
pDC->SetROP2(nOldMode);
}
if((nFlags && MK_LBUTTON) && bEllipseFlag){
nOldMode = pDC->GetROP2();
pDC->SetROP2(R2_NOT);
pDC->SelectStockObject(NULL_BRUSH);
pDC->Ellipse(OldPoint.x, OldPoint.y, Anchor.x, Anchor.y);
pDC->Ellipse(Anchor.x, Anchor.y, point.x, point.y);
OldPoint.x = point.x;
OldPoint.y = point.y;
pDC->SetROP2(nOldMode);
}
delete pDC;
CView::OnMouseMove(nFlags, point);
}
Now, the
first problem has been solved.
Refreshing
our drawing
The
metafile is a memory object that can support the device
context, so, when the window is resized (maximized,
minimized, etc), we can playback the metafile.
That will redraw all the shapes that we have drawn.
Add this
variable in the public section in the CPaintDoc
class declaration:
CMetaFileDC* pMetaFileDC;
Then in
the class constructor, add the following code:
CPainterDoc::CPainterDoc()
{
pMetaFileDC = new CMetaFileDC();
pMetaFileDC->Create();
}
Now, we
must reflect every thing we draw in the metafile. That
means when we call the device context, we must do the
same in the metafile:
Collapse
void CPainterView::OnLButtonUp(UINT nFlags, CPoint point)
{
CPainterDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
DrawTo.x = point.x;
DrawTo.y = point.y;
CClientDC* pDC = new CClientDC(this);
if(bLineFlag){
pDC->MoveTo(Anchor.x, Anchor.y);
pDC->LineTo(DrawTo.x, DrawTo.y);
pDoc->pMetaFileDC->MoveTo(Anchor.x, Anchor.y);
pDoc->pMetaFileDC->LineTo(DrawTo.x, DrawTo.y);
}
if(bRectangleFlag){
pDC->SelectStockObject(NULL_BRUSH);
pDC->Rectangle(Anchor.x, Anchor.y, DrawTo.x, DrawTo.y);
pDoc->pMetaFileDC->SelectStockObject(NULL_BRUSH);
pDoc->pMetaFileDC->Rectangle(Anchor.x,
Anchor.y, DrawTo.x, DrawTo.y);
}
if(bEllipseFlag){
pDC->SelectStockObject(NULL_BRUSH);
pDC->Ellipse(Anchor.x, Anchor.y, DrawTo.x, DrawTo.y);
pDoc->pMetaFileDC->SelectStockObject(NULL_BRUSH);
pDoc->pMetaFileDC->Rectangle(Anchor.x,
Anchor.y, DrawTo.x, DrawTo.y);
}
if(bFillFlag){
pDC->SelectStockObject(BLACK_BRUSH);
pDC->FloodFill(Anchor.x, Anchor.y, RGB(0, 0, 0));
pDoc->pMetaFileDC->SelectStockObject(NULL_BRUSH);
pDoc->pMetaFileDC->Rectangle(Anchor.x,
Anchor.y, DrawTo.x, DrawTo.y);
}
delete pDC;
CView::OnLButtonUp(nFlags, point);
}
When the
window is required to redraw itself, it calls the OnDraw()
function. What we have to do now is show the metafile.
Add the following code to OnDraw
function:
void CPainterView::OnDraw(CDC* pDC)
{
CPainterDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
HMETAFILE MetaFileHandle = pDoc->pMetaFileDC->Close();
pDC->PlayMetaFile(MetaFileHandle);
CMetaFileDC* ReplacementMetaFile = new CMetaFileDC();
ReplacementMetaFile->Create();
ReplacementMetaFile->PlayMetaFile(MetaFileHandle);
DeleteMetaFile(MetaFileHandle);
delete pDoc->pMetaFileDC;
pDoc->pMetaFileDC = ReplacementMetaFile;
}
That will
solve the second problem. Now we are ready to start our
program. Enjoy.