程序源码: https://github.com/JeffChen95/Draw_Image
画图时,需要准备好画纸、画笔、颜料、刷子等工具,与此对应,在MFC中需要做的准备就是
CClientDC dc(this); //创建dc,设备描述表也就是设备环境,可理解成画纸
CPen pen(m_nLineStyle, m_nLineWidth, m_color); //创建绘制的画笔,画笔中有风格、宽度、颜色CBrush *pbrush = new CBrush(); //创建画刷
pbrush->CreateSolidBrush(m_color); //创建绘制时颜料填充的画刷(实心)
//pbrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH)); //创建绘制时透明的画刷
找到笔和颜料,要把他拿到画纸跟前放一起,这就是dc选入画笔和刷子。
dc.SelectObject(&pen); //将画刷和画笔选入设备描述表中
dc.SelectObject(pbrush);
在画图中,由主要三个关键动作:鼠标按下、鼠标移动、鼠标抬起。分别对应有三个消息映射函数
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
所以要画出点线和形状时,无非就是对三个动作内容的完善。
1.第一个动作就是鼠标按下,要画图,按下的时候就要保存按下时的坐标点,这是实现这几个功能中这个函数主要的内容,声明如下:(这里需要注意多边形需要记录多个点,所以还需要一个记录点个数的变量)
CPoint m_ptOrigin; //起始点坐标
CPoint m_ptOld; //上一次的旧点
CPoint m_ptPolyFirst; //折线图中的第一个点
CPoint m_ptPolyLast; //折线图中最后点
CPoint m_arrayP[255]; //存储折线图中所有点
int m_PolyCount; //折线的点的计数
以上是变量的声明,下面是在OnLButtonDown函数中对他们的操作,point是函数的传入参数,表示当前鼠标位置。
m_ptOrigin = point;
m_ptOld = point;
if (m_PolyCount == 0)
{
m_ptPolyFirst = point;
}
2.记录好点之后,接下来的动作是鼠标移动,对应OnMouseMove函数,在这个函数中,就是实现画图的核心部分。我采用的是枚举的方法区别不同的绘图类型。
enum DTYPE //画图形类型
{
DLINE, //画线
DCURVE, //画曲线
DRECT, //画矩形
DRRECT, //画圆角矩形
DARC, //画圆弧
DELLI, //画椭圆
DCIRCLE, //画圆
DPOLY, //画多边
};
同样,在这个方法中,也需要创建dc,创建并选入画笔、刷子等设备。然后再根据不同的绘图类型实现不同的方法操作。
//针对画直线、曲线、多边形来说,核心主要靠两句
dc.MoveTo(m_ptOrigin);
dc.LineTo(m_ptOld); //擦去上一次的线
//针对矩形,就靠一句
dc.Rectangle(CRect(m_ptOrigin, m_ptOld));
//针对圆,也是靠一句
dc.Ellipse(CRect(m_ptOrigin, m_ptOld));
在这里要注意,如果直接这样写,会出现很多问题,比如密集画线等,如图
解决这个问题,我采用的是WindowsAPI中的SetROP2方法,原型如下
int SetROP2( HDC hdc, int fnDrawMode); //函数原型
在MFC中,第一个参数已经设置,只需要传入第二个参数,此处要选“R2_NOTXORPEN”宏作为输入参数,表示使用当前画笔的反色绘图,所以上述的操作完善后应该是:
case DLINE: //直线
dc.SetROP2(R2_NOTXORPEN); //逆转当前屏幕颜色来画线的绘图方式
dc.MoveTo(m_ptOrigin);
dc.LineTo(m_ptOld); //擦去上一次的线
dc.MoveTo(m_ptOrigin);
dc.LineTo(point); //绘制当前的临时线
m_ptOld = point;
break;
case DRECT: //矩形
dc.SetROP2(R2_NOTXORPEN); //逆转当前屏幕颜色来画线的绘图方式
dc.Rectangle(CRect(m_ptOrigin, m_ptOld));
dc.Rectangle(CRect(m_ptOrigin, point));
m_ptOld = point;
break;
case DELLI: //椭圆
dc.SetROP2(R2_NOTXORPEN); //逆转当前屏幕颜色来画线的绘图方式
dc.Ellipse(CRect(m_ptOrigin, m_ptOld));
dc.Ellipse(CRect(m_ptOrigin, point));
m_ptOld = point;
break;
case DCURVE: //曲线
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
m_ptOrigin = point;
break;
case DPOLY: //折线
if (m_PolyCount != 0)
{
dc.SetROP2(R2_NOTXORPEN); //逆转当前屏幕颜色来画线的绘图方式
dc.MoveTo(m_ptPolyLast);
dc.LineTo(point);
dc.MoveTo(m_ptPolyLast);
dc.LineTo(point); //绘制当前的临时线
}
else
{
dc.SetROP2(R2_NOTXORPEN); //逆转当前屏幕颜色来画线的绘图方式
dc.MoveTo(m_ptOrigin);
dc.LineTo(m_ptOld); //擦去上一次的线
dc.MoveTo(m_ptOrigin);
dc.LineTo(point); //绘制当前的临时线
}
m_ptOld = point;
break;
3.三步操作中最后一步就是鼠标按键抬起,对应OnLButtonUp函数,初始化的操作与上都相似,包括创建、选入和复制,
CClientDC dc(this); //创建dc
CPen pen(m_nLineStyle, m_nLineWidth, m_color); //创建绘制的画笔
CBrush *pbrush = new CBrush();
if (m_flagFullPaint == true)
{
pbrush->CreateSolidBrush(m_color); //创建绘制时填充的画刷
}
else
{
pbrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH)); //创建绘制时填充的画刷
}
dc.SelectObject(&pen); //将画刷和画笔选入设备描述表中
dc.SelectObject(pbrush);
if (!m_pMDC->m_hDC)
{
m_pMDC->CreateCompatibleDC(&dc);
m_pMDC->SetViewportOrg(-m_scRollpt);
}
在对各种绘图类型的操作实现中也与MouseMove中的方法类似,关键注意抬起时要擦除上一条记录,保留抬起时的记录作为显示在屏幕上的图像。
case DLINE: //画线
dc.SetROP2(R2_NOTXORPEN); //逆转当前屏幕颜色来画线的绘图方式
dc.MoveTo(m_ptOrigin);
dc.LineTo(m_ptOld); //擦去上一次的线
dc.SetROP2(R2_COPYPEN); //缺省绘图模式,像素为画笔颜色
dc.MoveTo(m_ptOrigin);
dc.LineTo(point); //绘制固定线
m_pMDC->MoveTo(m_ptOrigin);
m_pMDC->LineTo(m_ptOld); //在内存中备份
break;
case DRECT: //画矩形
dc.SetROP2(R2_NOTXORPEN); //逆转当前屏幕颜色来画线的绘图方式
dc.Rectangle(CRect(m_ptOrigin, m_ptOld));
dc.SetROP2(R2_COPYPEN); //缺省绘图模式,像素为画笔颜色
dc.Rectangle(CRect(m_ptOrigin, point));
m_pMDC->Rectangle(CRect(m_ptOrigin, m_ptOld));
break;
case DELLI: //画椭圆
dc.SetROP2(R2_NOTXORPEN); //逆转当前屏幕颜色来画线的绘图方式
dc.Ellipse(CRect(m_ptOrigin, m_ptOld));
dc.SetROP2(R2_COPYPEN); //缺省绘图模式,像素为画笔颜色
dc.Ellipse(CRect(m_ptOrigin, point));
m_pMDC->Ellipse(CRect(m_ptOrigin, m_ptOld));
break;
case DCURVE: //画曲线
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
m_ptOrigin = point;
break;
case DPOLY: //画多边形
m_arrayP[m_PolyCount++] = point;
m_ptPolyLast = point;
if (m_PolyCount > 1) //点超过1个时,画折线
{
dc.Polyline(m_arrayP, m_PolyCount);
m_pMDC->Polyline(m_arrayP, m_PolyCount);
}
break;
可能你们会发现上述多了一句m_pMDC的操作,因为你画完图或者移动改变窗口时需要重绘,重绘就需要把内存中的图像显示在屏幕上,所以相对应的在鼠标抬起时,要将屏幕上的内容保存到内存DC中,以便绘图时可以取出来。
相对于多边形来说,还需要最后一步,就是“封口”,将最后画完的点与第一个点连接起来,这样才是一个完整的图形,我是在右键抬起的操作中实现的这部分,核心代码如下。
case DPOLY:
if (m_PolyCount != 0)
{
dc.MoveTo(m_ptPolyLast);
dc.LineTo(point);
dc.MoveTo(point);
dc.LineTo(m_ptPolyFirst);
m_pMDC->MoveTo(m_ptPolyLast);
m_pMDC->LineTo(point);
m_pMDC->MoveTo(point);
m_pMDC->LineTo(m_ptPolyFirst);
m_PolyCount = 0;
}
break;
这三步都实现了,图像基本就可以显示了。当然后续还有一些操作才能更完善。
我的效果如下图
除此之外,我还实现了很多画图板的功能,包括画图板上没有的功能,大致功能如下:
有兴趣的朋友,可以看看,欢迎补充:https://github.com/JeffChen95/Draw_Image