目录
1. 按键消息
了解了MFC的消息映射机制,下面来完成画线功能。在先前创建的Draw项目中,可以看到消息响应函数OnLButtonDown有两个参数(如下)。
// 生成的消息映射函数
protected:
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
其中第二个参数是CPoint类型,CPoint类表示一个点。也就是说,当按下鼠标左键时,鼠标单击处的坐标点已由此参数传递给 OnLButtonDown 这一消息响应函数。这样,我们所需要做的工作就是在此消息响应函数中保存该点的信息。为此,需要在视类中增加一个成员变量。给一个类增加成员变量可以通过在类视图中用鼠标右键单击该类,并从弹出的快捷菜单中选择【添加】→【添加变量】,变量名称为m_ptOrigin,类型为CPoint,访问权限设置为private,如下图所示。
单击【确定】按钮,完成成员变量的添加操作。接下来在CDrawView构造函数(DrawView.cpp)中初始化这个变量,将其值初始化为0。
CDrawView::CDrawView() noexcept
{
// TODO: 在此处添加构造代码
m_ptOrigin = 0;
}
CDrawView::~CDrawView()
{
}
然后,在消息响应函数OnLButtonDown中保存鼠标按下点的信息,代码如下所示。
void CDrawView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_ptOrigin = point;
CView::OnLButtonDown(nFlags, point);
}
此时,我们就得到了将要绘制的线条的起点,现在还要获得线条的终点才能绘制出一个线条。终点是在鼠标左键弹起来时获得的,因此,在 CDrawView 类中还需要对WM_LBUTTONUP 消息进行响应。利用前面介绍的方法添加该消息响应函数,该函数的初始代码如下所示。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CView::OnLButtonUp(nFlags, point);
}
可以看到,Visual Studio自动产生的这个消息响应函数也有一个CPoint类型的参数,表示鼠标左键弹起时的位置点,也就是需要绘制的线条的终点。有了这两个点,就可以绘制线条了。cwind的核心代码都在wincore.cpp里。
2. 绘制线条
2.1 利用SDK全局函数实现画线功能
如下是利用SDK函数实现画线功能的代码。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
// 首先获得窗口的设备描述表
HDC hdc;
hdc = ::GetDC(m_hWnd);
// 移动到线条的起点
MoveToEx(hdc, m_ptOrigin.x, m_ptOrigin.y, NULL);
// 画线
LineTo(hdc, point.x, point.y);
// 释放设备描述表
::ReleaseDC(m_hWnd, hdc);
CView::OnLButtonUp(nFlags, point);
}
之前介绍过,为了进行绘图操作,必须获得一个设备描述表(DC)。因此,如上所示代码首先定义一个HDC类型的变量:hdc,接着调用全局函数GetDC获得当前窗口的设备描述表。前面讲述过,CWnd类有一个成员变量(m_hWnd)用于保存窗口句柄,而CDrawView类派生于CWnd类,因此该类也有这样的一个成员变量,这里的GetDC函数可以直接把这个成员变量作为参数来使用。
接下来进行画线操作,首先调用MoveToEx函数将当前位置移动到需要绘制的线条的起点处。
BOOL MoveToEx(
HDC hdc,
int x,
int y,
LPPOINT lppt
);
该函数有四个参数,其中第一个参数是设备描述表的句柄;第二个和第三个参数分别是新位置处的X坐标和Y坐标;第四个参数是指向POINT结构体的指针,用于保存移动操作前鼠标的位置坐标,在本例中不需要这个坐标值,将此参数设置为NULL。
然后调用 LineTo 函数绘制一条到指定点的线。该函数有三个参数,其中第一个参数是设备描述表的句柄,第二个和第三个参数分别是线条终点的X坐标和Y坐标。在前面已经讲述过,在绘图操作结束后,一定要释放设备描述表资源。因此,代码最后调用ReleaseDC函数来完成这一功能。编译并运行 Draw 程序,拖动鼠标就可以在窗口中绘制线条了。
2.2 利用MFC的CDC类实现画线功能
MFC为我们提供一个设备描述表的封装类CDC,该类封装了所有与绘图相关的操作。该类提供一个数据成员m_hDC,用来保存与CDC类相关的DC句柄,其道理与CWnd类提供m_hWnd成员变量保存与窗口相关的窗口句柄的道理是一样的。如例所示就是利用MFC的CDC类实现画线功能的代码。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
/*
// TODO: 在此添加消息处理程序代码和/或调用默认值
// 首先获得窗口的设备描述表
HDC hdc;
hdc = ::GetDC(m_hWnd);
// 移动到线条的起点
MoveToEx(hdc, m_ptOrigin.x, m_ptOrigin.y, NULL);
// 画线
LineTo(hdc, point.x, point.y);
// 释放设备描述表
::ReleaseDC(m_hWnd, hdc);
*/
CDC* pDC = GetDC();
pDC->MoveTo(m_ptOrigin);
pDC->LineTo(point);
ReleaseDC(pDC);
CView::OnLButtonUp(nFlags, point);
}
为了能更好地理解使用CDC类和使用SDK函数实现画线功能的异同,这里并未删除先前利用SDK函数编写的画线代码,只是将其注释起来,这样可以更直观地比较这两种实现方式。从以上代码可以看出,在利用MFC类实现画线功能时,首先需要定义一个CDC类型的指针,并利用CWnd类的成员函数GetDC获得当前窗口的设备描述表对象的指针;然后利用CDC类的成员函数MoveTo和LineTo完成画线操作;最后调用CWnd类的成员函数ReleaseDC释放设备描述表资源。编译并运行Draw程序,拖动鼠标同样可以在窗口中绘制线条。
提示: 因为CWnd类提供了成员函数GetDC和ReleaseDC,因此上面在利用 SDK 函数实现画线功能时,这两个函数前面都加上了两个冒号,表明它们是全局SDK函数。否则,VC++编译器将认为它们是CWnd类的成员函数
2.3 利用MFC的CClientDC类实现画线功能
下面再介绍一种画线的实现方法,这里将利用MFC提供的CClientDC类来实现这一功能。
这个类派生于CDC类,并且在构造时调用GetDC函数,在析构时调用ReleaseDC函数。也就是说,当一个CClientDC对象在构造时,它在内部会调用GetDC函数,获得一个设备描述表对象;在这个CClientDC对象析构时,会自动释放这个设备描述表资源。
这样的话,如果在程序中使用CClientDC类型定义DC对象,就不需要显式地调用GetDC函数和ReleaseDC函数了。只需要定义一个CClientDC对象,就可以利用该对象提供的函数进行绘图操作了。当该对象的生命周期结束时,会自动释放其所占用的设备资源,这就是CClientDC对象的好处。如下所示就是利用CClientDC类实现画线功能的代码。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CClientDC dc(this);
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
CView::OnLButtonUp(nFlags, point);
}
如上所示的代码中,在构造CClientDC对象时,需要一个CWnd类型的指针作为参数。如果这时我们想在视类窗口中绘图,就应该传递 CDrawView 对象的指针。在前面讲述C++知识时就曾经介绍过,每个对象都有一个this指针指向自己本身。因为本例想要构造一个与视类窗口有关的CClientDC对象,所以就把代表视类对象的this指针作为参数传递给了该对象的构造函数。
提示: 这里的CClientDC类型的变量(dc)是一个对象,因此使用点操作符(.)来调用该对象的函数。
以上几种实现方式都可以实现在程序窗口的客户区绘制线条,但是发现它们都不能在工具栏和菜单栏上画线。另外,上面代码构造了一个与视类相关的CClientDC对象,那么如何构造一个与视窗口的父窗口相关的CClientDC对象呢?
视窗口的父窗口就是框架窗口,即与CMainFrame类相关联的窗口。前面已经讲述过,在编写程序时,如果不知道某个函数的具体名称,那么可以根据函数的功能来猜测其名称。例如,这里想要获得父窗口的指针,那么我们可以猜测其函数名是不是“Get”加上“Parent”,于是以此名称在 MSDN 中查找,发现确实存在一个这样的函数,仔细阅读该函数的帮助信息,发现它就是我们所需要的那个函数。在实际编程时,可以试试这种猜测方法,相信会大有帮助。现在,将上面所示代码中构造CClientDC对象的代码替换为下面这行代码。
CClientDC dc(GetParent());
编译并运行Draw程序,拖动鼠标在窗口中绘制线条,发现此时线条可以画到程序的工具栏上。
视类窗口和框架窗口的位置关系:整个程序窗口就是框架窗口,而工具栏以下的白色区域部分才是视类窗口。视类窗口只有客户区(即视类窗口本身),而框架窗口既有客户区(即菜单栏以下部分),也有非客户区(就是程序运行界面中的标题栏和菜单栏)。而绘图操作一般都是在窗口的客户区进行的。因此,对上面所示的代码来说,因为其构造的设备描述表与视类窗口相关,所以程序只能在视窗口的客户区中画线。而如果构造的设备描述表与框架窗口相关,那么就可以在工具栏上绘制图形了,因为工具栏所在位置也属于框架窗口的客户区。
为什么线条出现在工具栏上时,线条的起点都是同一个点。要想在工具栏上画出线条,那么鼠标左键按下的位置就要在工具栏上,此时,视类窗口能否接收到 WM_LBUTTONDOWN 消息呢?如果接收不到,那么代表线条起点的成员变量m_ptOrigin的值应该是多少?想清楚这些问题,就能知道答案了。
提示: 工具栏是浮动的,可以利用鼠标把它拖动到程序窗口的任意位置处。
2.4 利用MFC的CWindowDC类实现画线功能
先介绍一个MFC类:CWindowDC,这个类也派生于CDC类,并且在构造时调用GetWindowDC函数获得相应的设备描述表对象,在析构时调用ReleaseDC函数释放该设备描述对象所占用的资源。也就是说,当我们利用CWindowDC对象绘图时,也不需要显式地调用GetDC和ReleaseDC函数,该对象会自动获取和释放设备描述表资源。使用CWindowDC对象有哪些好处呢?该对象可以访问整个窗口区域,包括框架窗口的非客户区和客户区。该对象的构造与CClientDC对象相同,如果要构造一个与视类窗口相关的设备描述表,则可以利用视类对象的指针来构造这个CWindowDC对象。如下所示是利用CWindowDC对象实现画线功能的代码。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CWindowDC dc(this);
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
CView::OnLButtonUp(nFlags, point);
}
编译并运行Draw程序,将会发现这段代码实现的功能与利用CClientDC类画线时没什么区别,也只能在视类窗口中画线,因为这时创建的设备描述表与视类窗口相关。
接着,把上面代码中构造设备描述表对象时使用的参数this指针换为指向视类父窗口的指针。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CWindowDC dc(GetParent());
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
CView::OnLButtonUp(nFlags, point);
}
编译并运行Draw程序,将会发现此时线条可以画到工具栏和菜单栏上,
知识点:通常都是在客户区中绘图。但是如果利用 CWindowDC 类,就可以实现在工具栏和菜单上绘图。
3. 在桌面窗口中画线
如果获得了一个与桌面窗口相关的设备描述表,就可以在桌面窗口中绘图。CWnd类的 GetDesktopWindow 成员函数可以获得 Windows 桌面窗口的句柄。修改构造设备描述表的代码,结果如下所示。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CWindowDC dc(GetDesktopWindow());
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
CView::OnLButtonUp(nFlags, point);
}
再次 Build 并运行 Draw 程序,并拖动鼠标画线,发现这时可以在整个屏幕窗口上画线。
3.1 绘制彩色线条
上述各种方法实现的画线功能所绘制的都是黑色线条。这是因为在设备描述表中有一个默认的黑色画笔,因此绘制的线条都是黑色的。如果想要绘制其他颜色的线条,首先需要创建一个特定颜色的画笔,然后将此画笔选入设备描述表中,接下来绘制的线条的颜色就由这个新画笔的颜色决定了。可以利用MFC提供的类CPen来创建画笔对象。该类封装了与画笔相关的操作,它有三个构造函数。
CPen( );
CPen(
int nPenStyle,
int nWidth,
COLORREF crColor
);
CPen(
int nPenStyle,
int nWidth,
const LOGBRUSH* pLogBrush,
int nStyleCount = 0,
const DWORD* lpStyle = NULL
);
nPenStyle:指定钢笔的样式。 在构造函数的第一个版本的此参数可以是下列值之一:
- PS_SOLID 创建实心钢笔。
- PS_DASH 创建一个虚线钢笔。 有效,仅当钢笔的宽度为1或更小,在组件度量单位。
- PS_DOT 创建一个虚线钢笔的。 有效,仅当钢笔的宽度为1或更小,在组件度量单位。
- PS_DASHDOT 使用交替短划线和点创建一个钢笔。 有效,仅当钢笔的宽度为1或更小,在组件度量单位。
- PS_DASHDOTDOT 使用交替短划线和double点创建一个钢笔。 有效,仅当钢笔的宽度为1或更小,在组件度量单位。
- PS_NULL 创建一个空钢笔。
- PS_INSIDEFRAME 创建例如绘制在Windows GDI输出功能生成的闭合的形状内部帧的一行指定一个边框的一个笔(, Ellipse、 Rectangle、 RoundRect、 Pie和 Chord 成员函数)。 在此样式使用Windows GDI未指定一个边框的输出功能(例如, LineTo 成员函数),钢笔的绘图区未由帧限制。
CPen :构造函数的第二个版本指定类型,样式,终止线帽的组合,并连接属性。 应按位组合可以使用或运算符,从每个类别的值(|)。 钢笔类型可为下列值之一:
- PS_GEOMETRIC 创建一个几何图形钢笔。
- PS_COSMETIC 创建一个装饰性的钢笔。
- CPen 构造函数的第二个版本添加 nPenStyle的以下钢笔样式:
- PS_ALTERNATE 创建设置其他像素的一个钢笔。 (此样式为装饰性的笔只适用)。
- PS_USERSTYLE 创建使用用户提供的一个样式数组的一个钢笔。
终止线帽可为下列值之一:
- PS_ENDCAP_ROUND 终止线是圆形的。
- PS_ENDCAP_SQUARE 终止线是正方形。
- PS_ENDCAP_FLAT 终止线保持不变。
连接可为下列值之一:
- PS_JOIN_BEVEL Joins为、。
- 并在 SetMiterLimit 函数时,设置的当前限制内 PS_JOIN_MITER Joins是斜接。 如果连接超出此限制,其转换为、。
- PS_JOIN_ROUND Joins是圆形的。
nWidth:指定钢笔的宽度。
- 对于构造函数的第一个版本,因此,如果该值为0,无论该映射模式,如组件单位的宽度始终为1像素。
- 对于构造函数的第二个版本,因此,如果 nPenStyle 是 PS_GEOMETRIC,该宽度(以逻辑单位给定。 如果 nPenStyle 是 PS_COSMETIC,必须将宽度为1。
crColor:包含钢笔的一个RGB颜色。
其中,第一个参数(nPenStyle)指定笔的线型(实线、点线、虚线等);第二个参数(nWidth)指定笔的线宽;第三个参数(crColor)指定笔的颜色,这个参数是COLORREF类型,利用RGB宏可以构建这种类型的值。RGB宏的声明如下所示。
COLORREF RGB(BYTE bRed, BYTE bGreen, BYTE bBlue color);
可以看到,RGB宏有三个参数,分别代表红、绿、蓝三种颜色的值。这三个参数都是BYTE类型,取值范围为0~255。如果将RGB宏的三个分量全部设置为0,则得到黑色;如果全部设置为255,则得到白色;……可以将这三个分量设置成0~255之间的任意值,从而得到各种不同的颜色。
另外,在程序中,当构造一个 GDI 对象后,该对象并不会立即生效,必须选入设备描述表,它才会在以后的绘制操作中生效。利用SelectObject函数可以实现把GDI对象选入设备描述表中,并且该函数会返回指向先前被选对象的指针。这主要是为了在完成当前绘制操作后,还原设备描述表。例如,当我们在某个局部范围内绘图时,可能需要改变画笔的颜色,并把新画笔选入设备描述表。当这部分绘图操作完成之后,需要恢复到原来的画笔颜色,然后完成其他部分的绘图操作。在一般情况下,在完成绘图操作之后,都要利用SelectObject函数把先前的GDI对象选入设备描述表,以便使其恢复到先前的状态。
如下代码所示是在Draw程序中绘制彩色线条的程序代码。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CPen pen(PS_SOLID,1, RGB(255, 0, 0));
CClientDC dc(this);
CPen* pOldPen = dc.SelectObject(&pen);
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
dc.SelectObject(pOldPen);
CView::OnLButtonUp(nFlags, point);
}
在上面所示代码中,首先创建一个实线画笔,其宽度为1,颜色为红色。接着利用SelectObject函数将新画笔对象选入设备描述表。然后利用画线函数绘制线条。最后,再次调用SelectObject函数恢复设备描述表中的画笔对象。编译并运行Draw程序,并拖动鼠标画线,这时可以看到这次绘制的是红色的线条。可以试着修改画笔的颜色,将会绘制出其他各种颜色的线条。也可以改变画笔的宽度,例如改为10,此时程序运行结果如下图所示。也可以改变画笔的线型,例如选择虚线线型,即用下面这行代码替上面构造画笔对象的那行代码。
CPen pen(PS_DASH,10, RGB(255, 0, 0));
编译并运行Draw程序,并拖动鼠标左键进行画线操作,将会发现绘制的还是一条实线,并不是想像中的虚线。这是因为当画笔的宽度小于等于1时,虚线线型才有效。因此,可以修改构造画笔对象的代码,将其宽度设置为1,再次编译并运行 Draw 程序,并拖动鼠标左键绘制线条,这时可以看到绘制的是虚线,如下图所示。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CPen pen(PS_DASH,1, RGB(255, 0, 0));
CClientDC dc(this);
CPen* pOldPen = dc.SelectObject(&pen);
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
dc.SelectObject(pOldPen);
CView::OnLButtonUp(nFlags, point);
}
另外,还可以绘制点线(将画笔的线型改为 PS_DOT),程序运行结果如下图所示。
4. 使用画刷绘图
MFC提供了一个CBrush类,可以用来创建画刷对象。画刷通常用来填充一块区域。
4.1 简单画刷
如下所示代码实现的功能是利用一个红色画刷填充鼠标拖曳过程中形成的一块矩形区域。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
//创建一个红色画刷
CBrush brush(RGB(255, 0, 0));
//创建并获得设备描述表
CClientDC dc(this);
//利用红色画刷填充鼠标拖拽过程中形成的矩形区域
dc.FillRect(CRect(m_ptOrigin, point), &brush);
CView::OnLButtonUp(nFlags, point);
}
在上例代码中,首先创建一个红色画刷;接着创建设备描述表对象;然后调用设备描述表对象的成员函数 FillRect,利用指定的画刷填充一块指定的矩形区域,而鼠标拖动过程中的起点和终点就决定了需要填充的矩形区域的大小,因此,在代码中通过CRect类利用鼠标拖动的起点和终点构造了这块矩形区域。CRect类提供了多个构造函数,本例使用的是下面这个构造函数,即通过指定矩形区域的左上角和右下角这两个点来构造一块矩形区域。
CRect(
POINT topLeft,
POINT bottomRight
) throw( );
在上面代码中使用CDC类的成员函数FillRect,该函数的功能是用指定的画刷填充一个矩形。该函数将填充全部的矩形,包括左边和上部边界,但不填充右边和底部边界。FillRect函数的声明如下所示。
void FillRect(
LPCRECT lpRect,
CBrush* pBrush
);
该函数有两个参数,各自的含义如下所述。
■ lpRect指向一个RECT结构体或CRect对象的指针,该结构体或对象中包含了要填充的矩形的逻辑坐标。
■ pBrush指向用于填充矩形的画刷对象的指针。编译并运行Draw程序,并在程序窗口中任意拖动鼠标,将会得到多个红色区域。
提示: 这里我们只是用指定的画刷填充一块区域,因此,并不需要把画刷选入设备描述表中。在设备描述表中存在一个默认的白色画刷。
4.2 位图画刷
CBrush类有下面这样一个构造函数。
explicit CBrush(
CBitmap* pBitmap
);
该构造函数要求一个CBitmap类型的指针,CBitmap类是位图类,于是我们就会这样想:利用这个构造函数是否可以创建一个位图画刷呢?事实确实如此。在创建CBitmap对象时,仅调用其构造函数并不能得到一个有用的位图对象,还需要调用一个初始化函数来初始化这个位图对象。CBitmap 类提供多个初始化函数,例如, LoadBitmap、CreateBitmap、CreateBitmapIndirect等。本例使用LoadBitmap函数来加载一幅位图,该函数的声明如下。
BOOL LoadBitmap(
LPCTSTR lpszResourceName
);
BOOL LoadBitmap(
UINT nIDResource
);
其中第二种声明需要一个资源ID作为参数。
首先需要给 Draw 程序增加一个位图资源。在Visual Studio 开发环境右侧的“解决方案资源管理器”窗口中,在“资源文件”文件夹上单击鼠标右键,在弹出菜单中选择【添加】→【资源】,出现如图所示的“添加资源”对话框。
选择Bitmap资源类型,单击【新建】按钮,即可创建一个默认名称为IDB_BITMAP1的位图资源,并在Visual Studio集成开发环境左边的代码编辑区域中打开位图编辑器,如图所示。
可以利用编辑器左边的颜色面板和上图矩形框中的图像编辑器工具栏按钮来编辑位图资源,还可以通过拉伸位图编辑器中网格周围的黑色方点来调整位图的大小。
提示: 项目中的资源都是在一个资源文件中定义的,针对本例,该文件名为Draw.rc,这是一个文本文件,在了解其格式的情况下,我们可以手动编写资源文件。而在集成开发环境中,显然不需要我们这么做。在Visual Studio 2017开发环境中,单击菜单栏上的【视图】菜单,选择【其他窗口】→【资源视图】,就可以打开资源视图,在该视图窗口中,我们可以查看项目中的所有资源,并以可视化的方式编辑它,如下图所示。
在创建了位图资源之后,就可以利用代码来创建位图画刷了,具体的实现代码如下所示。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
//创建位图对象
CBitmap bitmap;
//加载位图资源
bitmap.LoadBitmap(IDB_BITMAP1);
//创建位图画刷
CBrush brush(&bitmap);
//创建并获得设备描述表
CClientDC dc(this);
//利用位图画刷填充鼠标拖拽过程中形成的矩形区域
dc.FillRect(CRect(m_ptOrigin, point), &brush);
CView::OnLButtonUp(nFlags, point);
}
同时在CDrawView的源文件的头部添加下面一句包含语句。
#include "Resource.h"
这是因为我们刚添加的位图资源,其ID名IDB_BITMAP1是在该文件中定义的。
编译并运行Draw程序,然后在程序窗口内拖动鼠标,即可看到利用所创建的位图画刷填充的效果,如下图所示。
4.3 透明画刷
下面利用CDC的Rectangle函数绘制一个矩形,代码如下所示。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
//创建并获得设备描述表
CClientDC dc(this);
//绘制一个矩形
dc.Rectangle(CRect(m_ptOrigin, point));
CView::OnLButtonUp(nFlags, point);
}
编辑并运行Draw程序,然后拖动鼠标即可在程序窗口中绘制矩形。但是,当我们绘制两个相互重叠的矩形时,后绘制的矩形就会遮盖住先前绘制的矩形,如下图所示。这是因为在设备描述表中有一个默认的白色画刷,在绘图时,它会利用这个画刷来填充矩形内部。所以,当位置存在重叠时,后绘制的矩形就会把先前绘制的矩形遮挡住。
如果希望矩形内部是透明的,能够看到被遮挡的图形,那么就要创建一个透明画刷。此时,我们会立即想到CBrush类是否有创建透明画刷的方法。但很遗憾,CBrush 类并没有提供这样的函数,我们只能另想其他的办法了。
在前面介绍过GetStockObject这个函数,利用该函数可以获取一个黑色或白色的画刷句柄。这个函数是否能够获得一个透明画刷的句柄呢?从 MSDN 提供的帮助信息中,可以看到该函数的参数取值之一可以是NULL_BRUSH,以获取一个空画刷。那么,这个空画刷是否就是我们所需要的透明画刷呢?可以试试看。但这时存在一个问题,利用GetStockObject函数获取的是一个画刷句柄,而我们在进行绘制操作时需要的是一个画刷对象。如何从画刷句柄转换为画刷对象呢?CBrush类提供了一个FromHandle函数用来实现这样的功能。该函数的声明如下所示。
static CBrush* PASCAL FromHandle(
HBRUSH hBrush
);
如下就是具体的创建透明画刷并进行相应绘制操作的实现代码。
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
//创建并获得设备描述表
CClientDC dc(this);
//创建一个空画刷
CBrush *pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
//将空画刷选入设备描述表
CBrush *pOldBrush = dc.SelectObject(pBrush);
//绘制一个矩形
dc.Rectangle(CRect(m_ptOrigin, point));
//恢复先前的画刷
dc.SelectObject(pOldBrush);
CView::OnLButtonUp(nFlags, point);
}
上面所示代码中有以下几处需要注意的地方。
■ FromHandle函数的调用方式,这是调用类的静态成员函数的方式。
■ 由于GetStockObject 函数返回的类型是HGDIOBJECT,需要进行一个强制类型转换,将其转换为HBRUSH类型。
■ 这里创建的pBrush变量本身就是一个指针类型,因此在调用SelectObject函数时,该变量前面不用再加上取址符(&)。
■ 另外,我们可以比较一下前面使用的FillRect函数和这里的Rectangle函数。首先,二者都能绘制矩形,但前者在参数中提供了绘制使用的画刷,因此它就直接利用此画刷来填充矩形,并不需要先把需要的画刷选入设备描述表中。而后者并没有提供画刷这个参数,因此先要把需要的画刷选入设备描述表中,然后再调用此函数来绘制矩形。
编译并运行Draw程序,拖动鼠标在窗口中任意绘制多个相互重叠的矩形。此时能够看到被覆盖的矩形线条了,如下图所示。由此可见,利用参数NULL_BRUSH调用GetStockObject函数可以实现透明画刷这一功能。
5. 绘制连续线条
Windows系统为我们提供了一个画图应用程序,在该程序中,利用画笔可以绘制连续的线条,下面我们就在Draw程序中实现这样的功能。
为了绘制连续的线条,首先需要得到线条的起点,这在前面的内容已经实现了。然后需要捕获鼠标移动过程中的每一个点,这可以通过捕获鼠标移动消息(WM_MOUSEMOVE)来实现。在此消息响应函数中,在依次捕获到的各个点之间绘制一条条非常短的线段,从而就可以绘制出一条连续的线条。
遵照这一思路,我们开始增加 Draw 程序的功能。首先为视类增加鼠标移动消息(WM_MOUSEMOVE)的响应函数(默认名称为OnMouseMove)。这样,只要鼠标在应用程序窗口中移动时,就会进入这个消息响应函数中。但这并不是我们所期望的,我们希望在鼠标左键按下去之后才开始绘图。因此,我们需要有一个变量来标识鼠标左键是否按下去了这一状态,然后在鼠标移动消息响应函数中对这一变量进行判断。当此变量为真,即鼠标左键已经按下去时,我们才开始绘图。为此,我们在视类中添加一个BOOL型的私有成员变量m_bDraw,当鼠标左键按下去时,此变量为真;当鼠标左键弹起来时,此变量为假,这时,我们就不再绘制线条了。
// 该变量在视类头文件中的定义代码如下所示。
private:
BOOL m_bDraw;
// 接下来在视类的构造函数中,将此变量初始化为FALSE。
m_bDraw = FALSE;
// 当鼠标左键按下去时,即在视类的OnLButtonDown函数中将此变量设置为真。
m_bDraw = TRUE;
// 当鼠标左键弹起来时,即在视类的OnLButtonUp函数中将此变量设置为假。
m_bDraw = FALSE;
然后在OnMouseMove函数中对m_bDraw变量进行判断,如果其值为真,则说明鼠标左键已经按下去了,这时就可以开始进行画线操作。还有一点需要注意,因为每绘制一条线段后,在下一次都应该从这条线段的终点开始继续绘制。因此,在绘制完当前线段后,应该修改线段的起点,将当前线段的终点作为下一条线段的起点,具体代码如下所示。
void CDrawView::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
if (m_bDraw == TRUE)
{
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
//修改线段起点
m_ptOrigin = point;
}
CView::OnMouseMove(nFlags, point);
}
编译执行Draw程序,按下鼠标左键并在程序窗口中拖动鼠标,发现可以像在Windows提供的画图程序中的画笔那样绘制连续的线条了,结果如图所示。
如果希望给线条增加颜色,就需要使用前面介绍的画笔类(CPen 类)来实现。如下所示代码创建了一个红色的画笔,于是绘出的线条就是红色的。
void CDrawView::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
//创建一个红色的,宽度为1的实线画笔
CPen pen(PS_SOLID, 1, RGB(255, 0, 0));
//把创建的画笔选入设备描述表
CPen *pOldPen = dc.SelectObject(&pen);
if (m_bDraw == TRUE)
{
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
//修改线段起点
m_ptOrigin = point;
}
//恢复设备描述表
dc.SelectObject(pOldPen);
CView::OnMouseMove(nFlags, point);
}
6. 绘制扇形效果的线条
如果在上面绘制连续线条的程序中,保持每段小直线的起点不变,即以鼠标左键按下时的点为起点不变,分别绘制到鼠标移动点的直线,这时就会出现扇形的效果。也就是去掉上面OnMouseMove函数中修改线段起点的那行代码,编译运行 Draw 程序,按下鼠标左键并拖动鼠标,就可以看到类似扇形的效果,如图所示。
void CDrawView::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
//创建一个红色的,宽度为1的实线画笔
CPen pen(PS_SOLID, 1, RGB(255, 0, 0));
//把创建的画笔选入设备描述表
CPen *pOldPen = dc.SelectObject(&pen);
if (m_bDraw == TRUE)
{
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
//修改线段起点
//m_ptOrigin = point;
}
//恢复设备描述表
dc.SelectObject(pOldPen);
CView::OnMouseMove(nFlags, point);
}
// 头文件
private:
CPoint m_ptOld;
// 源文件
CDrawView::CDrawView() noexcept
{
// TODO: 在此处添加构造代码
m_ptOrigin = 0;
m_bDraw = FALSE;
m_ptOld = 0;
}
void CDrawView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_ptOrigin = m_ptOld = point;
m_bDraw = TRUE;
CView::OnLButtonDown(nFlags, point);
}
void CDrawView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_ptOrigin = m_ptOld = point;
m_bDraw = TRUE;
CView::OnLButtonDown(nFlags, point);
}
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
//创建并获得设备描述表
//CClientDC dc(this);
//创建一个空画刷
//CBrush *pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
//将空画刷选入设备描述表
//CBrush *pOldBrush = dc.SelectObject(pBrush);
//绘制一个矩形
//dc.Rectangle(CRect(m_ptOrigin, point));
//恢复先前的画刷
//dc.SelectObject(pOldBrush);
m_bDraw = FALSE;
CView::OnLButtonUp(nFlags, point);
}
void CDrawView::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
//创建一个红色的,宽度为1的实线画笔
CPen pen(PS_SOLID, 1, RGB(255, 0, 0));
//把创建的画笔选入设备描述表
CPen *pOldPen = dc.SelectObject(&pen);
if (m_bDraw == TRUE)
{
dc.MoveTo(m_ptOrigin);
//dc.LineTo(point);
dc.LineTo(m_ptOld);
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
//修改线段起点
//m_ptOrigin = point;
m_ptOld = point;
}
//恢复设备描述表
dc.SelectObject(pOldPen);
CView::OnMouseMove(nFlags, point);
}
如果想要绘制一个带边线的扇形,就需要为视类再增加一个成员变量,用来保存鼠标的上一个移动点,并在OnMouseMove函数中添加代码,以实现从鼠标当前点到鼠标上个移动点的连线,也就是绘制一条边线,同时,还要保存当前鼠标点,为下一条边线做准备。因此,我们首先给CDrawView类增加一个CPoint类型的私有成员变量m_ptOld,代码如下所示。
void CDrawView::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
//创建一个红色的,宽度为1的实线画笔
CPen pen(PS_SOLID, 1, RGB(255, 0, 0));
//把创建的画笔选入设备描述表
CPen *pOldPen = dc.SelectObject(&pen);
if (m_bDraw == TRUE)
{
dc.MoveTo(m_ptOrigin);
//dc.LineTo(point);
dc.LineTo(m_ptOld);
//dc.MoveTo(m_ptOrigin);
dc.MoveTo(m_ptOld);
dc.LineTo(point);
//修改线段起点
//m_ptOrigin = point;
m_ptOld = point;
}
//恢复设备描述表
dc.SelectObject(pOldPen);
CView::OnMouseMove(nFlags, point);
}
MFC还为我们提供一个设置绘图模式的函数SetROP2,该函数的声明如下所示。
int SetROP2(
int nDrawMode
);
SetROP2函数带有一个参数,用来指定绘图模式。该参数有多种取值,例如R2_BLACK、R2_WHITE、R2_MERGENOTPEN等。这里,可以简单地看看这个函数的作用。代码如下:
void CDrawView::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
//创建一个红色的,宽度为1的实线画笔
CPen pen(PS_SOLID, 1, RGB(255, 0, 0));
//把创建的画笔选入设备描述表
CPen *pOldPen = dc.SelectObject(&pen);
if (m_bDraw == TRUE)
{
dc.SetROP2(R2_BLACK);
dc.MoveTo(m_ptOrigin);
dc.LineTo(m_ptOld);
dc.LineTo(m_ptOld);
dc.LineTo(point);
m_ptOld = point;
}
//恢复设备描述表
dc.SelectObject(pOldPen);
CView::OnMouseMove(nFlags, point);
}
知识点 R2_MERGENOTPEN模式的作用是:先把画笔的颜色取反,再与屏幕颜色进行“或”操作,从而得到像素最终显示的颜色。编译并运行Draw程序,在程序窗口中按下鼠标左键,并拖动鼠标,发现在程序窗口中看不到绘制的线条。这就是设置了R2_MERGENOTPEN这种绘图模式的结果。如果将绘图模式换成R2_BLACK,再次运行程序,就会发现绘制的线条颜色始终都是黑色的。自行查看 MSDN 中关于此函数的帮助信息,看看该参数的各种取值及其作用。
7. 本章小结
介绍了类向导的使用,详细讲解了 MFC 消息映射机制的实现过程。在了解了消息映射机制后,介绍了图形绘制的基本操作,包括画线、画笔和画刷的使用等,在学习本章时,要理解DC的概念,并通过学习基本图形的绘制操作掌握复杂图形的绘制。