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:
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:
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:
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.
I'm iraqi citizen living in Libya, I have a B.Sc degree in computer engineering, my hobbies is only programing, now i'm working in a computer services centre as a computer engineer.