Windows编程与MFC # 4 单文档应用程序(4)

如何画一条直线

基于MFC进行开发,是一个小型的基于图形的绘图应用系统。
绘图系统的功能是绘制各种图形形状,主要分为两类:
(1)非封闭图元:直线、多段线、自由曲线、圆弧;
(2)封闭图元:矩形、椭圆(圆)、扇形、多边形、圆角矩形。
Windows编程与MFC # 4 单文档应用程序(4)_第1张图片
Windows编程与MFC # 4 单文档应用程序(4)_第2张图片
对于一个绘图系统,图形元素的绘制是最为基本的功能,也是程序实现过程中首先要完成的部分。
绘图系统能够绘制的图元形状不一,但其绘制过程都是相似的。
以直线图元的绘制为例具体介绍其绘制功能的实现,之后再实现其他图元的绘制。

在编写具体程序之前,必须先设计绘图程序与用户的交互方式,画的图形不同,交互的方式也会有所区别。
当用户点击菜单“绘图|直线”之后,就进入到“绘制直线”命令状态,可以绘制直线了。绘图的时候,鼠标被当作“绘图工具”,绘制直线的的步骤是这样的:
用户在客户区第一次单击鼠标就是在输入直线的端点坐标,第二次单击鼠标后,一条直线就被“画”出来,即显示在客户区中。接着鼠标再次单击就是另一条直线的起点。

【例1】建立一个单文档程序,项目名为MyDrawSystem。设计一个菜单“绘图”,“绘图|直线”菜单命令的功能。

程序运行后,用户点击“绘图|直线”菜单项后,进入“绘制直线”命令状态,当用户在客户区单击鼠标左键开始绘制直线,光标即改用标准的十字光标。直线的端点由两次鼠标单击给定。

(1)首先为菜单项“绘图”|”直线”添加相应的消息响应函数
void CMyDrawSystemView::OnLineMenu()
由于要在视图区显示“绘制”的直线,因此将该命令项映射到视图类。

Windows编程与MFC # 4 单文档应用程序(4)_第3张图片
(2)为了记录鼠标单击的次数和鼠标单击的位置坐标,为视图类添加新的成员变量。另外为了改变鼠标光标,添加鼠标光标句柄。代码如下。

class CMyDrawSystemView : public CView
{  protected: // create from serialization onlyint  m_nMouseStep;       //记录鼠标单击次数
       CPoint m_StartPos, m_EndPos;  
       //记录鼠标单击位置,表示线段的起点和终点
       HCURSOR m_hCursor;          //光标句柄
    …
}

在CMyDrawSystemView类的构造函数中添加代码,将m_nMouseStep初始化为0,表示尚未单击鼠标。代码如下:

CMyDrawSystemView::CMyDrawSystemView()
{
    // TODO: add construction code here
    m_nMouseStep = 0;    //鼠标单击次数初始化为0
}

(3)当点击“绘图|直线”命令后,首先获得十字光标句柄,为菜单项消息响应函数添加代码如下:

void CMyDrawSystemView::OnLineMenu() 
{
    // TODO: Add your command handler code here
   //点击菜单命令后获得十字光标句柄
   m_hCursor=AfxGetApp()
           ->LoadStandardCursor(IDC_CROSS);     
}

(4)使用ClassWizard为视图类添加按下鼠标左键WM_LBUTTONDOWN的消息响应函数。
在本例中,当鼠标左键被按下,首先要捕获鼠标,将鼠标光标设置为十字光标。并且开始画直线。
在绘制的过程中,我们需要判断鼠标点击的次数,以便确定这次单击的意义,即此次单击给出的是直线的起点还是终点。
当鼠标第一次单击,确定这就是直线的起点,紧接着鼠标第二次的单击就是直线的终点。绘制一条直线后,释放鼠标,并且释放设备环境。代码如下。

void CMyDrawSystemView::OnLButtonDown(UINT nFlags, CPoint point) 
{
  // TODO: Add your message handler code here  and/or call default
     SetCapture();     //捕获鼠标
     ::SetCursor(m_hCursor);          //设置十字光标
     CDC* pDC = GetDC(); 
     switch(m_nMouseStep)
	{	case 0:          //第一次单击鼠标左键
			m_StartPos = m_EndPos = point;
			m_nMouseStep++;
			break;
                case 1:     //第二次单击鼠标左键
			m_EndPos = point;
			m_nMouseStep = 0;
			pDC->MoveTo(m_StartPos);
                                pDC->LineTo(m_EndPos);
			ReleaseCapture();  
                                  //释放鼠标,还原鼠标形状
	}
          ReleaseDC(pDC);     //释放设备环境
	CView::OnLButtonDown(nFlags, point);
}

在绘制直线时用到了两个CDC类的成员函数MoveTo()和LineTo()。
函数MoveTo()的原型如下:
CPiont MoveTo(int x, int y)
//移动当前位置到x和y指定的点
CPiont MoveTo(POINT piont)
//移动当前位置到point指定的点
该函数返回前一个位置坐标的x和y值。该位置值被作为一个CPoint对象。
函数LineTo()的原型如下:
BOOL LineTo(int x, int y)
//从当前点向x和y指定的点画线
BOOL LineTo(POINT piont)
//从当前点向point指定的点画线
实现画线功能就是通过这两个函数来完成的。两个函数配合使用,可以完成任何绘制直线和折线的操作
(5)编译连接运行程序,使用“绘图|直线”命令即可绘制直线。

补充*-直线的拖拽显示
在各种绘图软件中更为通常的绘制方式是,进入绘制直线命令状态,在客户区按下鼠标左键,拖动鼠标移动,有一条橡皮线随之移动,在客户区松开鼠标左键,此时便在屏幕上画出了一条直线。

按下鼠标左键,即获取直线段起点坐标,移动鼠标将拖出一条橡皮线,松开鼠标左键,取得直线段的终点坐标,直线随即画出。
当再次按下鼠标左键,应该是新的一条直线的起点。
(1)由于在本例中一条直线的“绘制”需在三个鼠标响应函数中共同完成,必须使用一个标志变量来记录当前操作中是否完成了直线的绘制。
在视图类中添加一个新的成员变量m_bFinish 类型为BOOL,初始化为TRUE。
(2)将函数OnLButtonDown()原来的代码删除,新代码如下:

void CMyDrawSystemView::OnLButtonDown(UINT nFlags, CPoint point) 
{
  SetCapture();                           //捕获鼠标
  ::SetCursor(m_hCursor);         //设置十字光标
  m_StartPos = m_EndPos = point;       //按下鼠标左键即取得直线段起点坐标
 m_bFinish = FALSE ;              //标志尚未生成了一条直线
  CView::OnLButtonDown(nFlags, point);
}

(3)增加鼠标左键释放消息响应函数OnLButtonUp(),当鼠标左键被释放,取得线段的终点坐标,这时释放鼠标,绘制直线:

void CMyDrawSystemView::OnLButtonUp(UINT nFlags, CPoint point) 
{
  CDC* pDC = GetDC();
  m_EndPos = point;  //松开鼠标左键即取得直线段终点坐标
  pDC->MoveTo(m_StartPos); 
  pDC->LineTo(m_EndPos);
  m_bFinish = TRUE ; //标志生成了一条直线
  ReleaseCapture();     //释放鼠标,还原鼠标形状
  ReleaseDC(pDC);
  CView::OnLButtonUp(nFlags, point);
}

(4)所谓橡皮线,就是在画线的过程中,随着鼠标的移动,系统动态地显示一个形状随着鼠标位置变化而变化的临时图元。即鼠标移动到哪里,线就画到哪里,这些线是临时存在的。

void CMyDrawSystemView::OnMouseMove(UINT nFlags, CPoint point) 
{
  CDC* pDC = GetDC();
  if(!m_bFinish)  //鼠标左击一次且没有生成直线
 { 
        m_EndPos = point;
        pDC->MoveTo(m_StartPos);
        pDC->LineTo(m_EndPos);
 }	
 ReleaseDC(pDC);
CView::OnMouseMove(nFlags, point);
}

(5)编译、运行程序。所有画出来的线并不“消失”。
Windows编程与MFC # 4 单文档应用程序(4)_第4张图片
(6)解决的方法就是将上一条刚刚画出来的直线“擦除”,随着鼠标的移动再 “画”出新的一条。要实现这种做法需要使用特殊的绘图模式。设置新的绘图模式需要调用CDC的成员函数SetROP2()。
在视图类中添加一个成员变量,用来保存绘图模式。
int m_nDrawMode; //绘图模式

(7)修改OnMouseMove()函数

void CMyDrawSystemView::OnMouseMove(UINT nFlags, CPoint point) 
{
  CDC* pDC = GetDC();
  m_nDrawMode = pDC->SetROP2(R2_NOT); 
  if(!m_bFinish) { 
        CPoint curPoint= point;              //取得鼠标当前位置坐标
        //绘制橡皮线
        pDC->MoveTo(m_StartPos);       //从鼠标左键按下的位置开始画线   
        pDC->LineTo(m_EndPos);          //将旧线画一遍,就擦除了旧线
        pDC->MoveTo(m_StartPos);  
        pDC->LineTo(curPoint);	            //画一条新线  
        m_EndPos = point;                     //将当前点保存
}	
 ReleaseDC(pDC);

绘图模式指定了画笔的颜色和被填充物体内部的颜色是如何与显示平面的颜色(称为屏幕颜色)相混合的,描述了两个变量(画笔或画刷的颜色与屏幕颜色)的所有可能的布尔组合(包括AND、OR和XOR)和单个变量布尔操作(NOT)。
int CDC::SetROP2(int nDrawMode);
Windows编程与MFC # 4 单文档应用程序(4)_第5张图片
(8)修改OnLButtonUp ()函数

void CMyDrawSystemView::OnLButtonUp(UINT nFlags, CPoint point) 
{
  CDC* pDC = GetDC();
  pDC->SetROP2(m_nDrawMode);  //松开鼠标,绘图模式也恢复 
  m_EndPos = point;  //松开鼠标左键即取得直线段终点坐标
 pDC->MoveTo(m_StartPos); pDC->LineTo(m_EndPos);
 m_bFinish = TRUE ; //标志生成了一条直线
 ReleaseCapture();     //释放鼠标,还原鼠标形状
 ReleaseDC(pDC);
 CView::OnLButtonUp(nFlags, point);
}

为什么绘好的直线就消失了?
现在的绘图程序MyDrawSystem还有一个问题,当改变窗口的大小或将窗口最小化再打开,原来绘制的线段就都“消失”了。为什么会这样呢?
原来当窗口大小被调整、窗口被另一个窗口覆盖后又恢复等情况发生时,系统就会向应用程序消息队列发送WM_PAINT消息,以通知窗口函数执行刷新处理,此时应用程序调用的是视图类的刷新函数OnDraw(),而在上面的例子中,图形的绘制并没有在该函数中实现。可见,图形刷新是绘图过程中必须考虑的重要问题。

保存每一条绘制好的直线
为了解决上面程序的问题,必须在OnDraw()中重绘之前用鼠标绘制的线段,这样使用鼠标单击时必须将线段的起点和终点坐标保存起来。
这样就需要为所有的直线段定义一个直线类CLine,使用直线类对象保存直线的数据。

如何存储一条直线
直线类Cline的数据成员和成员函数包括:
线段起点
线段终点
绘制过程
为项目增加一个C++新类CLine,为了后续使用链表和序列化的需要,该类需要公有继承根类CObject。

菜单“项目|添加类”,调出“添加类”对话框。添加一个“C++类”。
Windows编程与MFC # 4 单文档应用程序(4)_第6张图片
Line.h文件中的代码

#pragma once
class CLine :public CObject
{
    CPoint m_Begin,m_End;      //直线的起点和终点 
  public:
    CLine(void);
    CLine(CPoint begin, CPoint end); 
    virtual ~CLine(void);
    void Draw (CDC *pDC);
};
CLine::CLine(){
   m_Begin.x=m_Begin.y=0;
   m_End.x=m_End.y=0;
}
CLine::CLine(CPoint begin, CPoint end){	
	     m_Begin = begin;
	     m_End = end;
}
CLine::~CLine()
{}
void CLine::Draw (CDC *pDC) //绘制直线段的成员函数
{  
	  pDC->MoveTo(m_Begin);
       pDC->LineTo(m_End);
}

修改菜单命令响应函数
因为“绘图”菜单下将来会有许多绘制不同图形的命令,单击某个命令后就要根据所要绘制的图形来编写鼠标响应函数,这样在鼠标响应函数中需要判断哪个菜单命令被点击了。
为此,在视图类中添加一个整型成员变量m_nFigureType,点击不同的菜单项即在菜单响应函数中为其赋一个不同的整数值,用于代表所绘的图元类型。代码如下。

class CMyDrawSystemView : public CView
{
    protected: // create from serialization only
	……
	int  m_nFigureType;                      //图元类型
}
CMyDrawSystemView::CMyDrawSystemView()   
//视图类构造函数
{
	// TODO: add construction code here
            m_nMouseStep = 0;         //鼠标单击次数初始为0
	 m_nFigureType= 0;         //初始化为无图元类型
}

修改“直线”的响应函数
当点击“绘图|直线”命令后,即为m_nFigureType赋值为1,代表直线。为菜单项消息响应函数添加代码如下:

void CMyDrawSystemView::OnLineMenu()
{
   // TODO: 在此添加命令处理程序代码
   m_nFigureType=1;              //1代表图元为直线
   m_hCursor=AfxGetApp()
             ->LoadStandardCursor(IDC_CROSS);  
}

保存图元对象的数据结构
一个CLine对象代表一条绘制出来的直线,为了保存大量的且数目不确定的直线图形对象,可以考虑使用对象数组来保存很多条直线,这个数组应该是可以动态增长的;
考虑到程序能够绘制的图形除了直线,还有许多其他的图形,在此选用对象链表来保存绘制出来的各种图形对象。

使用对象链表
需要在文档类CMyDrawSystemDoc中为图形对象选择一个合适的动态数据结构。在文档类CMyDrawSystemDoc中定义以下公有成员变量:
CObList m_FigureList; //图元对象链表

改写鼠标左键按下的处理函数
OnLButtonDown()函数需要做的是首先判断当前要绘制是哪一种图形,根据绘制该图形时鼠标点击的含义获取该图形的相关数据。对于直线而言,在鼠标点击的过程中要将当前线段的起点和终点坐标保存下来。
之后动态建立当前线段的CLine对象,并将该对象加入图元对象链表。在OnLButtonDown()中修改添加代码如下。

void CMyDrawSystemView::OnLButtonDown(UINT nFlags, CPoint point)
{
   // TODO: 在此添加消息处理程序代码和/或调用默认值
  CMyDrawSystemDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);
   if (!pDoc)
        return;
   CDC *pDC=GetDC();
   switch(m_nFigureType)        //根据图元类型进行分支处理
   { 
      case 1:                    //绘制直线
           SetCapture();           //捕获鼠标
           ::SetCursor(m_hCursor); //设置十字光标
switch(m_nMouseStep)                         //鼠标单击次数
 {  case 0:                                     //第一次单击鼠标左键
        m_StartPos = m_EndPos = point;     //获取线段起点
        m_nMouseStep++;
        break;
     case 1:                                    //第二次单击鼠标左键
        m_EndPos = point;    //获得线段终点
        m_nMouseStep = 0;   //直线两点已有,单击次数清零
        ReleaseCapture();      //释放鼠标,还原鼠标形状
        //生成新的直线类CLine对象
        CLine * pLine = new CLine(m_StartPos, m_EndPos); 
        pLine->Draw(pDC);    //画出该直线
        pDoc->m_FigureList.AddTail(pLine); //将新生成的直线存入图元链表
  } //switch(m_nMouseStep)  
  break;
  case 2:   //绘制多段线
    break;
  case 3:   //绘制自由曲线
    break;
       //…       
  default:
     break;
}  // switch(m_nFigureType) 
ReleaseDC(pDC);           //释放设备环境
CView::OnLButtonDown(nFlags, point);
}


编写OnDraw()函数

void CMyDrawSystemView::OnDraw(CDC* pDC)
{
    CMyDrawSystemDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
    if (!pDoc)
        return;
    POSITION pos = pDoc->m_FigureList.GetHeadPosition();
    while(pos != NULL)
    {
     CLine* pFigure = (CLine*)pDoc->m_FigureList.GetNext(pos);
     pFigure->Draw(pDC);
    }
}

完成以上各步骤后,编译连接运行程序,绘制直线段后,调整窗口大小,原来绘制的图形仍然保留在窗口中。

你可能感兴趣的:(Windows编程)