绘制线条 、画刷绘图、绘制连续线条、绘制扇形效果的线条
插入符【文本插入符|图形插入符】、窗口重绘、路径、字符输入【设置字体|字幕变色】
菜单命令响应函数、菜单命令的路由、基本菜单操作、动态菜单操作、电话本实例
对话框的创建与显示、动态创建按钮、控件的访问【控件调整|静态文本控件|编辑框控件】、对话框伸缩功能、输入焦点的传递、默认按钮的说明
MFC对话框:逃跑按钮、属性表单、向导创建
在对话框程序中让对话框捕获WM_KEYDOWN消息
修改应用程序窗口的外观【窗口光标|图标|背景】、模拟动画图标、工具栏编程、状态栏编程、进度栏编程、在状态栏上显示鼠标当前位置、启动画面
设置对话框、颜色对话框、字体对话框、示例对话框、改变对话框和控件的背景及文本颜色、位图显示
先学习简单绘图:MFC–简单绘图,了解基本知识。
新建一个单文档类型的MFC工程,取名:Graphic。此程序将实现简单的绘图功能。
实现简单的绘图功能,包括点、直线和椭圆的绘制。为了实现这些功能:
⭕⭕1)首先为此程序添加一个子菜单,菜单名称为“绘图”;
⭕⭕2)为其添加四个子菜单项,分别用来控制不同图形的绘制。当用户选择其中的一个菜单项后,程序将按照当前的选择进行相应图形的绘制。添加的四个菜单项的ID及名称如下表:
⭕⭕3)然后分别为这四个菜单项添加命令响应,本程序让视类(CGraphicView)对这些菜单命令进行响应:
最终生成的代码:
CGraphicView.h:
public:
afx_msg void OnDot();
afx_msg void OnLine();
afx_msg void OnRectangle();
afx_msg void OnEllipse();
CGraphicView.cpp:
BEGIN_MESSAGE_MAP(CGraphicView, CView)
// 标准打印命令
ON_COMMAND(ID_FILE_PRINT, &CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT, &CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, &CGraphicView::OnFilePrintPreview)
ON_WM_CONTEXTMENU()
ON_WM_RBUTTONUP()
ON_COMMAND(IDM_DOT, &CGraphicView::OnDot)
ON_COMMAND(IDM_LINE, &CGraphicView::OnLine)
ON_COMMAND(IDM_RECTANGLE, &CGraphicView::OnRectangle)
ON_COMMAND(IDM_ELLIPSE, &CGraphicView::OnEllipse)
END_MESSAGE_MAP()
......
void CGraphicView::OnDot()
{
// TODO: 在此添加命令处理程序代码
}
void CGraphicView::OnLine()
{
// TODO: 在此添加命令处理程序代码
}
void CGraphicView::OnRectangle()
{
// TODO: 在此添加命令处理程序代码
}
void CGraphicView::OnEllipse()
{
// TODO: 在此添加命令处理程序代码
}
⭕⭕4)在程序运行以后,当用户单击某个菜单项时,应该把用户的选择保存起来,以便随后的绘图操作使用。因此,在CGraphicView类中添加一个私有变量用来保存用户的选择:
private:
UINT m_nDrawType;
接着,在视类的构造函数中将此变量初始化为0:
CGraphicView::CGraphicView() noexcept
{
// TODO: 在此处添加构造代码
m_nDrawType = 0;
}
⭕⭕5)当用户选择绘图菜单下的不同子菜单项时,将变量m_nDrawType设置为不同的值:
void CGraphicView::OnDot()
{
// TODO: 在此添加命令处理程序代码
m_nDrawType = 1;
}
void CGraphicView::OnLine()
{
// TODO: 在此添加命令处理程序代码
m_nDrawType = 2;
}
void CGraphicView::OnRectangle()
{
// TODO: 在此添加命令处理程序代码
m_nDrawType = 3;
}
void CGraphicView::OnEllipse()
{
// TODO: 在此添加命令处理程序代码
m_nDrawType = 4;
}
⭕⭕6)对于直线、矩形和椭圆,在绘图时都可以由2个点来确定其图形。当鼠标左键按下时得到一个点,当鼠标左键松开时又得到另外一个点。也就是说,在鼠标左键按下时将当前点保存为绘图原点,当鼠标左键松开时,就可以绘图了。
6.1)因此就需要为视类CGraphicView分别捕获鼠标左键按下和鼠标左键松开这两个消息。
void CGraphicView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CView::OnLButtonDown(nFlags, point);
}
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CView::OnLButtonUp(nFlags, point);
}
6.2)另外,当鼠标左键按下时,需要将鼠标当前按下点保存起来,因此为CGraphicView类再增加一个CPoint类型的私有成员变量:m_ptOrigin。
并在CGraphicView类构造函数中,将该变量的值设置为0,即将原点设置为(0,0)。
CGraphicView::CGraphicView() noexcept
{
// TODO: 在此处添加构造代码
m_nDrawType = 0;
m_ptOrigin = (0, 0);
}
6.3)在鼠标左键按下消息响应函数中,保存当前点:
void CGraphicView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_ptOrigin = point;
CView::OnLButtonDown(nFlags, point);
}
6.4)鼠标左键松开消息响应函数中实现绘图功能。通过前面的知识知道,为了进行绘图操作,首先需要有DC对象,所以首先定义了一个CClientDC类型的变量: dc。在具体绘图时应根据用户的选择来进行,该选择已经保存在变量:m_nDrawType中了。可以用switch/case语句,分别完成相应图形的绘制:
如果设置一个点,需要用到函数:SetPixel,这也是CDC类的一个成员方法,该函数是在指定的点设置一个像素。该函数两种声明形式,其中一种声明如下:
COLORREF SetPixel(POINT point,COLORREF crColor);
◼ point
指定的点
◼ crColor
指定的颜色。在程序中设定的颜色在系统颜色表中可能不存在,但系统会选择一种和这个颜色最接近的颜色。
当用户选择直线时,这时就需要绘制直线,首先调用MoveTo函数移动到原点,然后调用LineTo函数绘制到终点。
绘制矩形时可以使用Rectangle函数,该函数有一种声明:
BOOL Rectangle(LPCRECT IpRect);
该函数有一个指向CRect对象的参数,而CRect可以利用两个点来构造。
Rectangle函数需要的是指向CRect对象的指针,而传递的此参数却是CRect对象,但程序编译时却能成功通过,运行时也不会报错的原因:
C系列的语言都是强类型语言,如果类型不匹配的话,需寒进行强制类型转换。CRect类提供一个成员函数:重载LPCRECT操作符,其作用是将CRect转换为LPCRECT类型。因此,当在程序中给Rectangle函数的参数赋值时,如果它发现该参数是一个CRect对象,它就会隐式地调用LPCRECT操作符,将CRect类型的对象转换为LPCRECT类型。
当用户选择椭圆菜单项时,调用Ellipse函数绘制一个椭圆。
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
switch (m_nDrawType)
{
case 1:
dc.SetPixel(point, RGB(255, 0, 0));
break;
case 2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case 3:
dc.Rectangle(CRect(m_ptOrigin, point));
break;
case 4:
dc.Ellipse(CRect(m_ptOrigin, point));
break;
}
CView::OnLButtonUp(nFlags, point);
}
运行Graphic程序,由于DC中有一个默认的白色画刷,在绘制图形时会使用这个默认画刷填充其内部,因此在绘制时,如果存在重叠部分,那么先前绘制的图形会被后来绘制的图形所覆盖:
⭕⭕7)绘制其他颜色的线条。
绘制的直线,以及矩形和椭圆的边框都是黑色的。一般来说,在程序运行过程中,用户都希望能够使用他们自己指定的颜色来绘制各种图形。通过前面章节的介绍,知道线条的颜色是由DC中画笔的颜色确定的,为了绘制其他颜色的线条就需要:
- 构造一个CPen对象,
- 为它指定一种颜色,
- 将此画笔选入设备描述表中。
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
CPen pen(PS_SOLID, 20, RGB(150, 140, 32));
dc.SelectObject(&pen);
switch (m_nDrawType)
{
case 1:
dc.SetPixel(point,RGB(200,0, 0));
break;
case 2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case 3:
dc.Rectangle(CRect(m_ptOrigin, point));
break;
case 4:
dc.Ellipse(CRect(m_ptOrigin, point));
break;
}
CView::OnLButtonUp(nFlags, point);
}
⭕⭕8)把DC中的画刷设置为透明。
不想使用DC默认的白色画刷来填充矩形或椭圆的内部,而是希望能够看到这些图形内部的内容,可以把DC中的画刷设置为透明的。
- 利用参数NULL BRUSH调用GetStockObject函数可以创建透明画刷,
- 然后调用CBrush类的静态成员函数FromHandle将画刷句柄转换为指向画刷对象的指针,但是该函数的参数需要的是HBRUSH类型,GetStockObject函数返回的是HGDIOBJ类型,因此需要进行强制转换,将其转换为画刷的句柄,即HBRUSH类型对象。
- 将创建的新画刷选入设备描述表中。
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
//改变画笔颜色
CPen pen(PS_SOLID, 5, RGB(150, 140, 32));
dc.SelectObject(&pen);
//设置画刷透明
CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
dc.SelectObject(pBrush);
switch (m_nDrawType)
{
case 1:
dc.SetPixel(point,RGB(200,0, 0));
break;
case 2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case 3:
dc.Rectangle(CRect(m_ptOrigin, point));
break;
case 4:
dc.Ellipse(CRect(m_ptOrigin, point));
break;
}
CView::OnLButtonUp(nFlags, point);
}
再次运行Graphic程序,选择相应菜单项,然后绘制各种图形,这时可以看到所有绘制的线条都可以看到了,因为现在使用的是透明画刷。
许多软件都为用户提供了设置对话框,或者称为选项对话框,允许用户通过设置一些选项来改变软件的某些行为和特性。
实现:
给Graphic程序添加一个设置对话框,允许用户指定画笔的类型、线宽,并且让随后的绘图操作就使用用户指定的新设置值来进行绘制。
为了实现这一功能,首先需要为Graphic程序添加一个对话框资源,并按照下表所列内容修改其属性。
创建对话框:
⭕⭕1)首先实现线宽的设置。为新添加的设置对话框资源添加一个静态文本框,并将其Caption属性设置为: 线宽;
⭕⭕2)再添加个编辑框,让用户输入设定的线宽,将其ID设置为: IDC_LINE_WIDTH。
⭕⭕3)创建一个相应的对话框类。类名设置为CSettingDlg,基类:CDialog。
此时生成的类所关联的ID并不是自己想关联的对话框的ID。所以需要自己手动指定关联的对话框ID号。
需要更改的关联位置有两处:
⭕⭕4)为对话框中的编辑框控件增加一个成员变量:m_nLineWidth,类型:UINT。
因为对于线,不希望用户设置的值小于0,因此将它的类型选择为无符号整型(UINT)。
改正上面的视频内容:
m_nLineWidth设置为公开public类型——由于后面发现需要在别的类中访问此成员变量,所以需要设置为公开类型。public: UINT m_nLineWidth;
⭕⭕5)显示设置对话框。
5.1)为Graphic程序在绘图子菜单下再增加一个菜单项,名称为:设置,并将其ID设置为:IDM_SETTING:
5.2)用户单击该菜单项后,程序应立即显示刚才新建的设置对话框。因此为此菜单项添加一个命令响应,并选择视类(CGraphicView)对此消息做出响应:
5.3)将CSettingDlg的头文件包含到视类源文件CGraphicView.cpp中:
#include "CSettingDlg.h"
5.4)首先构造设置对话框对象(dlg),然后调用该对象的DoModal函数显示该对话框:
void CGraphicView::OnSetting()
{
// TODO: 在此添加命令处理程序代码
CSettingDlg dlg;
dlg.DoModal();
}
运行程序:
⭕⭕6)当用户在线宽编辑框中输入线宽值并确定此操作后,程序应把这个线宽值保存起来,然后随后的绘图都使用这个线宽值来设置线的宽度。
6.1)为CGraphicView类添加一个私有的成员变量:m_nLineWidth;类型:UINT,用来保存用户输入的线宽:
6.2)在CGraphicView类的构造函数中将其初始化为0。
CGraphicView::CGraphicView() noexcept
{
// TODO: 在此处添加构造代码
m_nDrawType = 0;
m_ptOrigin = (0, 0);
m_nLineWidth = 0;
}
6.3)判断用户关闭设置对话框时的选择。
在用户输入线宽后,应该是在用户单击OK按钮后才保存这个线宽值;如果用户选择的是Cancle按钮,并不需要保存这个线宽值。因此在CGraphicView类的OnSetting函数中需要判断一下用户关闭设置对话框时的选择,如果选择的是OK按钮,则保存用户输入的线宽值:
void CGraphicView::OnSetting()
{
// TODO: 在此添加命令处理程序代码
CSettingDlg dlg;
if (IDOK == dlg.DoModal()) {
m_nLineWidth = dlg.m_nLineWidth;
}
}
DoModal函数:
。。。。。。。。。。。。。。。。。。。
⭕⭕7)在构造画笔对象时,其宽度就可以利用m_nLineWidth这个变量来代替了:
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
//改变画笔颜色
//CPen pen(PS_SOLID, 5, RGB(150, 140, 32));
CPen pen(PS_SOLID, m_nLineWidth, RGB(150, 140, 32));
dc.SelectObject(&pen);
//设置画刷透明
CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
dc.SelectObject(pBrush);
switch (m_nDrawType)
{
case 1:
dc.SetPixel(point,RGB(200,0, 0));
break;
case 2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case 3:
dc.Rectangle(CRect(m_ptOrigin, point));
break;
case 4:
dc.Ellipse(CRect(m_ptOrigin, point));
break;
}
CView::OnLButtonUp(nFlags, point);
}
在程序运行时,当用户设置线宽后,在下一次绘图时,就会以用户输入的线宽创建画笔,那么随后的绘图就是按照用户设置的线宽来绘制的。
运行Graphic程序,单击绘图下的设置菜单项,在弹出的设置对话框中指定新线宽,并单击OK按钮关闭设置对话框。然后再绘图,可以发现程序使用的是用户指定的新线宽来绘制图形的。
但是,当再次打开设置对话框时,线宽编辑框的值又变回0了。一般来说,当再次回到这个设置对话框时,应该看到上次设置的值,但这里的情况并不是这样的。
原因:由于设置对话框对象dlg是一个局部对象。当再次单击单击绘图下的设置菜单项,即再次调用OnSetting函数时,又将重新构造dlg这个设置对话框对象。因此该对象的所有成员变量都将被初始化,而CSettingDlg对象的构造函数中m_nLineWidth初始化为0,所以每次打开设置对话框时,看到的编辑框内都为0:
CSettingDlg::CSettingDlg(CWnd* pParent /*=nullptr*/)
: CDialog(IDD_DLG_SETTING, pParent)
, m_nLineWidth(0)
{
}
解决:为了解决这个问题,当CSettingDlg对话框(dlg)对象产生之后,应该将CGraphicView类中保存的用户先前设置的线宽再传回给这个设置对话框:
void CGraphicView::OnSetting()
{
// TODO: 在此添加命令处理程序代码
CSettingDlg dlg;
//将CGraphicView类中保存的用户先前设置的线宽再传回给这个设置对话框
dlg.m_nLineWidth = m_nLineWidth;
if (IDOK == dlg.DoModal()) {
m_nLineWidth = dlg.m_nLineWidth;
}
}
实现:
为Graphic程序添加允许用户设置线型的功能。提供一些单选按钮让用户从多种线型中选择一种。
⭕⭕1)首先再为Graphic程序已有的设置对话框资源添加一个组框,并设置Caption:线型;ID:IDC_LINE_STYLE。
组框的作用通常是起标示作用,所以它的ID默认情况下是IDC_STATIC。但如果在程序中需要对组框进行操作的话,那么其ID就不能是默认的IDC_STATIC了,需要修改这个ID。
因为后面的程序会对这个组框进行一些操作,所以这里将它的ID修改为: IDC_LINE_STYLE。
⭕⭕2)接着在此组框内放置三个单选按钮,保持它们默认的ID值不变,将它们的名称分别设置为:实线、虚线、点线。
然后将这三个单选按钮设置成为一组。方法:在第一个单选按钮(实线)上单击鼠标右键,打开其属性对话框,选中Group选项。这时,这三个单选按钮就成为一组的了。
⭕⭕3)利用类向导为这组单选按钮关联一个成员变量:
这样在程序运行时:
生成代码:
CSettingDlg.h
public:
UINT m_nLineWidth;
// 记录线型的值
int m_nLineStyle;
CSettingDlg.cpp
CSettingDlg::CSettingDlg(CWnd* pParent /*=nullptr*/)
: CDialog(IDD_DLG_SETTING, pParent)
, m_nLineWidth(0)
, m_nLineStyle(0)
{
}
void CSettingDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Text(pDX, IDC_LINE_WIDTH, m_nLineWidth);
DDX_Radio(pDX, IDC_RADIO1, m_nLineStyle);
}
⭕⭕4)保存用户选择的线型。
同上,当用户单击设置对话框上的OK按钮关闭该对话框后,应该将用户选择的线型保存下来,因此需要:
4.1)为CGraphicView类再添加一个int类型的私有成员变量:m_nLineStyle:
4.2)并在该类的构造函数中将其初始化为0。
CGraphicView::CGraphicView() noexcept
{
// TODO: 在此处添加构造代码
m_nDrawType = 0;
m_ptOrigin = (0, 0);
m_nLineWidth = 0;
m_nLineStyle = 0;
}
4.3)然后在CGraphicView类的OnSetting函数中,当用户单击设置对话框的OK按钮关闭该对话框后,将用户选择的线型保存到CGraphicView类的m_nLineStyle变量中。
void CGraphicView::OnSetting()
{
// TODO: 在此添加命令处理程序代码
CSettingDlg dlg;
dlg.m_nLineWidth = m_nLineWidth;
if (IDOK == dlg.DoModal()) {
m_nLineWidth = dlg.m_nLineWidth;
//户选择的线型保存到CGraphicView类的m_nLineStyle变量中
m_nLineStyle = dlg.m_nLineStyle;
}
}
4.4)与前面线宽的设置一样,为了把上一次选择的线型保存下来,同样需要在CGraphicViev类中把已保存的线型值再设置回设置对话框的线型变量:
void CGraphicView::OnSetting()
{
// TODO: 在此添加命令处理程序代码
CSettingDlg dlg;
dlg.m_nLineWidth = m_nLineWidth;
//把已保存的线型值再设置回设置对话框的线型变量
dlg.m_nLineStyle = m_nLineStyle;
if (IDOK == dlg.DoModal()) {
m_nLineWidth = dlg.m_nLineWidth;
m_nLineStyle = dlg.m_nLineStyle;
}
}
⭕⭕5)根据用户指定的线型创建画笔。
获得用户指定的线型后,程序应根据此线型创建画笔。在wingdi.h文件中定义了一些符号常量:
可以在CGraphicView类OnLButtonUp函数中构造画笔的那行代码中的符号常量: PS_SOLID上单击鼠标右键打开wingdi.h文件,并定位于该常量符号的定义处。
从中可以看到PS_SOLID (实线)的值本身就是0,PS_DASH (虚线)就是1,PS_DOT(点线)就是2,这正好与CGraphicView类的成员变量m_nLineStyle的取值一一对应。这是因为在设置对话框中排列的线型顺序正好是按照实线、虚线和点线的顺序来做的。因此,程序在构造画笔对象时,可以直接使用m_nLineStyle变量作为线型参数的值:
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
//改变画笔颜色
//CPen pen(PS_SOLID, 5, RGB(150, 140, 32));
//直接使用m_nLineStyle变量作为线型参数的值
CPen pen(m_nLineStyle, m_nLineWidth, RGB(150, 140, 32));
dc.SelectObject(&pen);
//设置画刷透明
CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
dc.SelectObject(pBrush);
switch (m_nDrawType)
{
case 1:
dc.SetPixel(point,RGB(200,0, 0));
break;
case 2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case 3:
dc.Rectangle(CRect(m_ptOrigin, point));
break;
case 4:
dc.Ellipse(CRect(m_ptOrigin, point));
break;
}
CView::OnLButtonUp(nFlags, point);
}
运行Graphic程序,首先打开设置对话框,可以看到初始的选择是实线,这是因为在CGraphicView类的构造函数中将线型变量(m_nLineStyle)初始设置为0,即实线。然后程序在构造设置对话框对象之后,将CGraphicView类的线型变量赋给了这个对话框对象的线型变量,因此该对话框初始显示时,选中的线型是实线。
画笔宽度小于等于1时,虚线和点线线型才有效。否则,线型会自动改为实线。
- 若想设置虚线的线宽可以参考: 绘制粗虚线
颜色对话框类似于Windows提供的画图程序中选择编辑颜色莱单项后出现的对话框:
利用颜色对话框,可以让用户选择一种颜色,程序随后按照此颜色创建绘图所需的画笔。颜色对话框看起来比较复杂。实际上,MFC提供了一个类:CColorDialog,可以很方便地创建一个颜色对话框。该类的派生层次结构:
由此,可以知道颜色对话框也是一个对话框。
CColorDialog类的构造函数:
CColorDialog(COLORREF clrInit = 0, DWORD dwFlags = 0,CWnd* pParentWnd = NULL);
◼ clrInit
指定默认的颜色选择。默认是黑色。
◼dwFlags
指定一组标记,用于定制颜色对话框的功能和它的外观。
◼ pParentWnd
指向颜色对话框父窗口或拥有者窗口的指针。
为了在Graphic程序中增加颜色对话框的显示:
⭕⭕1)首先为该程序增加一个菜单项,当用户选择此菜单项时,程序将显示颜色对话框。
将这个新菜单项放置在已有的绘图子菜单下,并设置ID:IDM_COLOR、Caption:颜色。
⭕⭕2)为其增加一个命令响应,并选择CGraphicView类对此菜单项命令做应。
⭕⭕3)在响应函数OnColor中显示颜色。
void CGraphicView::OnColor()
{
// TODO: 在此添加命令处理程序代码
CColorDialog dlg;
dlg.DoModal();
}
运行Graphic程序,点击颜色菜单项,即可以看到出现了一个颜色对话框:
可以看到在该对话框左边颜色块的黑色块上有一个黑色的边框,说明默认选择的是黑色。
⭕⭕4)将用户选择的颜色保存下来。
4.1)CColorDialog类有一个CHOOSECOLOR结构体类型的成员变量:m_cc。CHOOSECOLOR结构体的定义:
typedef struct {
//指定结构的长度(字节)
DWORD lStructSize;
//拥有对话框的窗口的句柄。该成员可以是任意有效的窗口句柄,或在对话框没有所有者时,可为NULL
HWND hwndOwner;
//如果Flag成员设置了CC_ENABLETEMPLATEHANDLE标识符时,该成员是一个包含了对话框模板的内存对象的句柄。如果 CC_ENABLETEMPLATE 标识符被设置时,该成员是一个包含了对话框的模块句柄。如果上述两个标识符都未被设置,则该成员被忽略。
HWND hInstance;
//如果CC_RGBINIT标识符被设置时,该成员指定了对话框打开时默认的选择颜色。如果指定的颜色值不在有效的范围内,系统会自动选择最近的颜色值。如果该成员为0或CC_RGBINIT未被设置,初始颜色是黑色。如果用户单击OK按钮,该成员指定了用户选择的颜色。
COLORREF rgbResult;
//指向一个包含16个值的数组,该数组包含了对话框中自定义颜色的红、绿、蓝(RGB)值。如果用户修改这些颜色,系统将用新的颜色值更新这个数组。如果要在多个ChooseColor函数中保存这个新的数组,应该为该数组分配静态内存空间。
COLORREF* lpCustColors;
//一个可以让你初始化颜色对话框的位集。当对话框返回时,它用来这些标识符来标识用户的输入。该成员可以为一组标识符的任意组合。
DWORD Flags;
//指定应用程序自定义的数据,该数据会被系统发送给钩子程序。当系统的发送WM_INITDIALOG消息给钩子程序时,消息的lParam参数是一个指向CHOOSECOLOR结构的指针。钩子程序可以利用该指针获得该成员的值。
LPARAM lCustData;
//指向CCHookProc钩子程序的指针,该钩子可以处理发送给对话框的消息。该成员只在CC_ENABLEHOOK标识被设定的情况下才可用,否则该成员会被忽略。
LPCCHOOKPROC lpfnHook;
//指向一个NULL结尾的字符串,该字符串是对话框模板资源的名字。
LPCWSTR lpTemplateName;
}CHOOSECOLOR, *LPCHOOSECOLOR;
当用户单击颜色对话框上的OK按钮后,这个结构体中的rgbResult变量就保存了用户选择的颜色。因此,在程序中通过这个变量就可以获得用户选择的颜色。
4.2)在Graphic程序中,为了保存用户选择的颜色,为CGraphicView类再增加一个COLORREF类型的私有成员变量:m_clr。右键点击类视图类中的CGraphicView,选项添加–>添加变量
4.3)并在CGraphicView类的构造函数中将其初始化为红色:
CGraphicView::CGraphicView() noexcept
{
// TODO: 在此处添加构造代码
m_nDrawType = 0;
m_ptOrigin = (0, 0);
m_nLineWidth = 0;
m_nLineStyle = 0;
m_clr = RGB(255,0, 0);
}
4.4)在颜色子菜单的响应函数OnColor函数中进行判断:如果用户单击的OK按钮,就将用户选择的颜色保存下来。
void CGraphicView::OnColor()
{
// TODO: 在此添加命令处理程序代码
CColorDialog dlg;
if (IDOK == dlg.DoModal()) {
m_clr = dlg.m_cc.rgbResult;
}
}
⭕⭕5)当用户选择颜色后,随后进行的绘图操作都应用此颜色来绘制,也就说应该按此颜色创建绘图用的画笔。
所以修改CGraphicVview类OnLButtonUp函数中已有的创建画笔的代码,将用户当前选择的颜色(即m_clr变量)传递给CPen构造函数的第三个参数。此外,还需要修改该函数中绘制点图形的代码,用用户当前选择的颜色来设置像素点的颜色。
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
//改变画笔颜色
//CPen pen(PS_SOLID, 5, RGB(150, 140, 32));
CPen pen(m_nLineStyle, m_nLineWidth, m_clr);
dc.SelectObject(&pen);
//设置画刷透明
CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
dc.SelectObject(pBrush);
switch (m_nDrawType)
{
case 1:
dc.SetPixel(point,m_clr);
break;
case 2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case 3:
dc.Rectangle(CRect(m_ptOrigin, point));
break;
case 4:
dc.Ellipse(CRect(m_ptOrigin, point));
break;
}
CView::OnLButtonUp(nFlags, point);
}
运行Graphic程序,打开颜色对话框,选择某种颜色,然后进行绘图操作,可以发现这时所绘制的图形边框的颜色就是刚才选择的颜色。但是当再次打开颜色对话框时,它默认选择的仍是黑色,而不是刚才选择的颜色。
⭕⭕6)颜色值设置回颜色对话框。
6.1)自然就会想到应该像上面的处理一样,将用户选择的颜色,即CGraphicView类的m_clr变量保存的颜色值设置回颜色对话框对象,因此修改CGraphicView类的OnColor函数:
void CGraphicView::OnColor()
{
// TODO: 在此添加命令处理程序代码
CColorDialog dlg;
dlg.m_cc.rgbResult = m_clr;
if (IDOK == dlg.DoModal()) {
m_clr = dlg.m_cc.rgbResult;
}
}
再次运行Graphic程序,先选择一种颜色,然后进行图形的绘制,可是当再次打开颜色对话框时,将会发现结果仍不对,默认选中的颜色仍是黑色。
6.2)实际上如果想要设置颜色对话框初始选择的颜色,则需要设置该对话框的CC_RGBINIT标记:
采用后一种方法,修改CGraphicView类的OnColor函数:
void CGraphicView::OnColor()
{
// TODO: 在此添加命令处理程序代码
CColorDialog dlg;
dlg.m_cc.Flags = CC_RGBINIT;
dlg.m_cc.rgbResult = m_clr;
if (IDOK == dlg.DoModal()) {
m_clr = dlg.m_cc.rgbResult;
}
}
再次运行Graphic程序,选择颜色菜单项,出现一个非法操作提示对话框:
6.3)实际上,当在创建CColorDialog对象dlg时,它的数据成员m_cc中的Flags成员已经具有了一些初始的默认标记。将CC_RGBINIT标记直接赋给Flags成员时,就相当于将Flags成员初始默认的标记都去掉了。所以不能给Flags标记直接赋值,应利用或操作(|)将CC_RGBINIT标记与Flags先前的标记组合起来。
void CGraphicView::OnColor()
{
// TODO: 在此添加命令处理程序代码
CColorDialog dlg;
dlg.m_cc.Flags = CC_RGBINIT| dlg.m_cc.Flags;
dlg.m_cc.rgbResult = m_clr;
if (IDOK == dlg.DoModal()) {
m_clr = dlg.m_cc.rgbResult;
}
}
再次运行Graphic程序,打开颜色对话框,可以看到初始选择的就是红色。接着,选择其他某种颜色并关闭该对话框。然后再打开颜色对话框,这时就可以看到现在选中的是先前选择的颜色了。
6.4)另外, Flags成员的取值还有一个常用标记: CC-FULLOPEN,该标记的作用就是让颜色对话框完全展开。
void CGraphicView::OnColor()
{
// TODO: 在此添加命令处理程序代码
CColorDialog dlg;
dlg.m_cc.Flags = CC_RGBINIT| dlg.m_cc.Flags|CC_FULLOPEN;
dlg.m_cc.rgbResult = m_clr;
if (IDOK == dlg.DoModal()) {
m_clr = dlg.m_cc.rgbResult;
}
}
再次运行Graphic程序,打开颜色对话框,这时可以看到这个颜色对话框处于完全展开状态:
下面为Graphic程序添加字体对话框应用,该对话框类似于在对话框资源的属性对话框中所看到的字体对话框。
与上面的颜色对话框一样,字体对话框的创建也很简单,因为MFC也提供了一个相应的类: CFontDialog,该类的派生层次结构:
由此可知该类派生于CDialog,所以其也是一个对话框类。
CFontDialog类的构造函数如下:
CFontDialog(
LPLOGFONT lplfInitial = NULL,
DWORD dwFlags = CF_EFFECTS | CF_SCREENFONTS,
CDC* pdcPrinter = NULL,
CWnd* pParentWnd = NULL
);
CFontDialog(
const CHARFORMAT& charformat,
DWORD dwFlags = CF_SCREENFONTS,
CDC* pdcPrinter = NULL,
CWnd* pParentWnd = NULL
);
◼ lplfInitial
指向LOGFONT结构体的指针,允许用户设置一些字体的特征。
◼ dwFlags
s主要设置一个或多个与选择的字体相关的标记。
◼ pdcPrinter
指向打印设备上下文的指针。
◼ pParentWnd
指向字体对话框父窗口的指针。
由CFontDialog类的构造函数的声明可知,它的参数都有默认值,因此在构造字体对话框时可以不用指定任何参数。字体对话框的创建与前面的颜色对话框的一样,首先构造一个字体对话框对象,然后调用该对象的DoModal函数显示这个对话。
⭕⭕1)为Graphic程序增加一个菜单项用来显示字体对话框,将其ID设置为: IDM_FONT;Caption:字体。
⭕⭕2)接着为其增加一个命令响应,并选择CGraphic类对此菜单项命令做出响应。
⭕⭕3)然后在此响应函数中添加创建并显示字体对话框的代码。
void CGraphicView::OnFont()
{
// TODO: 在此添加命令处理程序代码
CFontDialog dlg;
dlg.DoModal();
}
运行程序,点击字体菜单项:
⭕⭕4)程序保存用户通过字体对话框选择的字体信息。
当用户通过字体对话框选择某种字体后,程序应该把当前选择保存下来,然后在CGraphicView类中利用此字体将用户选择的字体名称显示出来。
CFontDialog类有一个CHOOSEFONT结构体类型的数据成员:m_cf。CHOOSEFONT结构体的定义如下:
typedef struct {
//指定这个结构的大小,以字节为单位
DWORD lStructSize;
//调用窗口句柄,指向所有者对话框窗口的句柄。这个成员可以是任意有效窗口句柄,或如果对话框没有所有者它可以为NULL。
HWND hwndOwner;
//显示的设备环境句柄,一般为NULL;
HDC hDC;
//选中的字体返回值,这里的字体是逻辑字体
LPLOGFONTW lpLogFont;
//字体的大小
INT iPointSize;
//字体的位标记,用来初始化对话框。当对话框返回时,这些标记指出用户的输入。
DWORD Flags;
//字体颜色
COLORREF rgbColors;
//自定义数据,这数据是能被lpfnHook成员识别的系统传到的钩子程序
LPARAM lCustData;
//做钩子程序用的回调函数
LPCFHOOKPROC lpfnHook;
//指向一个以空字符结束的字符串,字符串是对话框模板资源的名字,资源保存在能被hInstance成员识别的模块中
LPCWSTR lpTemplateName;
//实例句柄
HINSTANCE hInstance;
//字体风格
LPWSTR lpszStyle;
//字体类型
WORD nFontType;
//字体允许的最小尺寸
INT nSizeMin;
//字体允许的最大尺寸
INT nSizeMax;
} CHOOSEFONT *LPCHOOSEFONT;
其中成员IpLogFont是指向逻辑字体(LOGFONT类型)的指针。LOGFONT结构的定义:
typedef struct tagLOGFONT
{
LONG lfHeight;
LONG lfWidth;
LONG lfEscapement;
LONG lfOrientation;
LONG lfWeight;
BYTE lfItalic;
BYTE lfUnderline;
BYTE lfStrikeOut;
BYTE lfCharSet;
BYTE lfOutPrecision;
BYTE lfClipPrecision;
BYTE lfQuality;
BYTE lfPitchAndFamily;
WCHAR lfFaceName[LF_FACESIZE];
} LOGFONT;
各成员的含义:LOGFONT——百度百科
其中成员lfFaceName中存放的就是字体的名称。也就是说,可以通过此成员得到字体的名称。
至于字体对象的创建:
- 首先可以利用CFont类构造一个字体对象,
- 然后利用CFont类的CreateFontIndirect成员函数根据指定特征的逻辑字体(LOGFONT类型)来初始化这个字体对象。
CreateFontIndirect函数的功能就是利用参数IpLogFont指向的LOGFONT结构体中的一些特征来初始化CFont对象。该函数的声明:
BOOL CreateFontIndirect( CONST LOGFONT* lpLogFont);
所以,为了保存用户选择的字体:
4.1)首先通过类向导为Graphic工程的CGraphicView类增加两个私有的成员变量:
▪▪▪ CFont :m_font,用于存放字体对象;
▪▪▪ CString:m_strFontName,用来保存所选字体的名称。
4.2)然后在CGraphicView类的构造函数中初始化m_strFontName为空字符串。
类向导创建的成员变量已经自动被初始化。
4.3)然后在CGraphicView类的OnFont函数中进行判断,如果用户单击的是字体对话框的OK按钮,就用所选字体信息初始化m_font对象,并保存所选字体的名称。
void CGraphicView::OnFont()
{
// TODO: 在此添加命令处理程序代码
CFontDialog dlg;
if (IDOK == dlg.DoModal()) {
//保存字体对话框选择的字体样式
m_font.CreateFontIndirectW(dlg.m_cf.lpLogFont);
//保存设置的字体名称
m_strFontName = dlg.m_cf.lpLogFont->lfFaceName;
}
}
4.4)在视类窗口中把字体名称用选择的字体显示出来。
void CGraphicView::OnFont()
{
// TODO: 在此添加命令处理程序代码
CFontDialog dlg;
if (IDOK == dlg.DoModal()) {
//保存字体对话框选择的字体样式
m_font.CreateFontIndirectW(dlg.m_cf.lpLogFont);
//保存设置的字体名称
m_strFontName = dlg.m_cf.lpLogFont->lfFaceName;
//让窗口无效
Invalidate();
}
}
void CGraphicView::OnDraw(CDC* pDC)
{
CGraphicDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;
// TODO: 在此处为本机数据添加绘制代码
CFont* pOldFont = pDC->SelectObject(&m_font);
pDC->TextOutW(0, 0, m_strFontName);
pDC->SelectObject(pOldFont);
}
运行Graphic程序,选择字体菜单项,这时将打开字体对话框,可以选择任一种字体、字形,还可以指定字体的大小。然后单击字体对话框的OK按钮,这时在视类窗口中就可以看到用选定的字体、字形和大小输出了所选字体的名称:
可是当再次选择字体菜单项后,程序将出现非法操作。
原因:这是因为当第一次选择字体后,OnFont函数中就把m_font对象与这种字体资源相关联了。当再次选择一种字体后,OnFont函数又会试图把m_font对象与新字体资源相关联,这时当然就会出错。
解决:在点击OK菜单项后,在字体菜单项的命令响应函数中实现:在保存用户选择的字体资源前应该进行一个判断,如果mfont对象已经与一个字体资源相关联了,首先就需要切断这种关联,释放该字体资源,然后才能与新资源相关联。
如何释放CFont资源:
要想释放先前的资源,可以利用CGdiObject类(CPen、 CFont、CBitmap、CBrush都派生于该类)的成员函数DeleteObject来实现。该函数通过释放所有为Windows GDI对象所分配的资源,从而删除与CGdiObject对象相关联的Windows GDI对象,同时与CGdiObjec对象相关的存储空间并不会受此调用影响。
CGdiObjec对象 和 Windows GDI对象区别:
CGdiObjec对象是一个类的对象;而Windows GDI是一种资源对象。就好像窗口类的对象和窗口的关系一样。例如视类对象和视类窗口,它们之间的联系在于视类对象有一个数据成员:m_hWnd保存了窗口资源的句柄值。
CGdiObject类的对象和Windows GDI资源对象是两个不同的概念,它们之间也是一个数据成员来维系的。当删除Windows GDI资源对象后,对于CGdiObiect类所定义的对象来说,并不受影响,只是它们之间的联系被切断了。
如果想判断m_font对象是否已经与某个字体资源相关联了,最简单的方法就是利用CGdiObject对象的数据成员m_hObject来判断,该变量保存了与CGdiObject对象相关联的Windows GDI资源的句柄。如果已经有关联了,则调用DeleteObject释放这个字体资源,然后再和新的资源相关联。
void CGraphicView::OnFont()
{
// TODO: 在此添加命令处理程序代码
CFontDialog dlg;
if (IDOK == dlg.DoModal()) {
//判断m_font对象是否已经与某个字体资源相关联了
//m_hObject变量保存了与CGdiObject对象相关联的Windows GDI资源的句柄,
//若不为空,即为真,则说明已经关联过
if (m_font.m_hObject)
{
//如果已经有关联了,则调用DeleteObject释放这个字体资源
m_font.DeleteObject();
}
//保存字体对话框选择的字体样式
m_font.CreateFontIndirectW(dlg.m_cf.lpLogFont);
//保存设置的字体名称
m_strFontName = dlg.m_cf.lpLogFont->lfFaceName;
//让窗口无效
Invalidate();
}
}
再为Graphic程序添加一个类似的功能,也就是在已有的设置对话框中作一个示例区,当用户改变线宽或选择不同的线型时,在示例区也能看到这种改变。
⭕⭕1)首先在设置对话框中增加一个组框,并将它的Caption设置为:示例。组框的默认ID:IDC_STATIC。如果在程序中需要对组框进行操作的话,需要修改它的ID。因为后面的程序会对这个示例组框进行一些操作,所以ID设置为: IDC_SAMPLE。
⭕⭕2)为了反映用户对线宽和线型所做的改变,CSettingDlg类需要捕获编辑框控件和单选按钮的通知消息,以反映出用户所做的改变。
2.1)首先利用类向导为CSettingDlg类添加编辑框控件(IDC_LINE_WIDTH)的EN_CHANGE消息的响应函数OnChangeLineWidth。
2.2)然后分别对三个单选按钮(IDC_RADIO1、IDC_RADIO2和IDC_RADIO3),都选择BN_CLICKED消息,用3种方式添加它们的消息响应函数:OnRadio1、OnRadio2和OnRadio3。
2.3)完成示例线条的绘制。
如果把在示例组框中绘制线条的代码在这四个消息响应函数中都写一遍,代码重复性则太高,不太合适。可以考虑这么做:
2.3.1)在这四个函数中调用Invalidate函数让窗口无效,当下一次发生WM_PAINT消息时重绘窗口,
void CSettingDlg::OnChangeLineWidth()
{
// TODO: 如果该控件是 RICHEDIT 控件,它将不
// 发送此通知,除非重写 CDialog::OnInitDialog()
// 函数并调用 CRichEditCtrl().SetEventMask(),
// 同时将 ENM_CHANGE 标志“或”运算到掩码中。
// TODO: 在此添加控件通知处理程序代码
Invalidate();
}
void CSettingDlg::OnClickedRadio1()
{
// TODO: 在此添加控件通知处理程序代码
Invalidate();
}
void CSettingDlg::OnBnClickedRadio2()
{
// TODO: 在此添加控件通知处理程序代码
Invalidate();
}
void CSettingDlg::OnBnClickedRadio3()
{
// TODO: 在此添加控件通知处理程序代码
Invalidate();
}
2.3.2)然后在该消息响应函数: OnPaint中完成示例线条的绘制。为CSettingDlg类添加WM_PAINT消息的响应函数,并在此函数中完成示例线条的绘制。
void CSettingDlg::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: 在此处添加消息处理程序代码
// 不为绘图消息调用 CDialog::OnPaint()
//首先根据指定的线宽和线型创建画笔,先将画笔的颜色设置为红色
CPen pen(m_nLineStyle, m_nLineWidth, RGB(255, 0, 0));
//然后将该画笔对象选入设备描述表中
dc.SelectObject(&pen);
//获得IDC_SAMPLE组框的矩形
CRect rect;
//通过调月GetDlgltem函数来得到指向组框窗口对象的指针
//然后利用GetWindowRect函数获得组相窗口矩形区域的大小
GetDlgItem(IDC_SAMPLE)->GetWindowRect(&rect);
//用选择的画笔线型和线宽画一条示例线,
//示例线的坐标参考组框矩形的坐标位置,起点横坐标:矩形左上角右移20;起点纵坐标:矩形中间位置的纵坐标
//即画一条距组框左侧20pixel到距组框右侧20像素处的线。
dc.MoveTo(rect.left + 20, rect.top + rect.Height() / 2);
dc.LineTo(rect.right - 20, rect.top + rect.Height() / 2);
}
运行Graphic程序,打开设置对话框,改变线宽的值,但是在示例组框中并没有发现绘制的线条。但是把设置对话框移动到屏幕左则,会出现示例内容:
原因:之所以发生这种现象的原因主要是因为GetWindowRect函数的调用,该函数的声明:
void GetwindowRect(LPRECT IpRect) const;
该函数的参数是指向CRect或RECT结构体的变量,接收屏幕坐标,也就是说,通过该函数接收到的是屏幕坐标。而在绘图时是以对话框左上角为原点的客户区坐标,而通过GetWindowRect函数得到的屏幕坐标是以屏幕左上角为原点的,这样得到的rect对象的各个坐标值是相对屏幕左上角的,值都比较大。但是绘制线条时又是以对话框客户区的左上角为原点进行的,因此就绘制到对话框的外面,就看不到线条了。
解决:通过上面的分析,可以知道在得到组框的矩形区域大小后,需要将其由屏幕坐标转为客户坐标。这可以利用ScreenToClient函数来实现。该函数的声明:
void ScreenToclient(LPRECT IpRect) const;
ScreenToClient函数的声明中要求该函数的参数是LPRECT类型,但下面添加的代码中传递的却是CRect对象,程序却能成功编译,这与之前介绍的情况一样,因为CRect类重载了LPRECT操作符。一般情况下,为了明白起见,通常还是为此参数添加一个取地址符,表示传递的是指针。即:
void CSettingDlg::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: 在此处添加消息处理程序代码
// 不为绘图消息调用 CDialog::OnPaint()
CPen pen(m_nLineStyle,m_nLineWidth, RGB(244,0,0));
dc.SelectObject(&pen);
//获得IDC_SAMPLE组框的矩形
CRect rect;
GetDlgItem(IDC_SAMPLE)->GetWindowRect(&rect);
ScreenToClient(&rect);
//用选择的画笔线型和线宽画一条示例线,
//示例线的坐标参考组框矩形的坐标位置,起点横坐标:矩形左上角右移20;起点纵坐标:矩形中间位置的纵坐标
//即画一条距组框左侧20pixel到距组框右侧20像素处的线。
dc.MoveTo(rect.left + 20, rect.top + rect.Height() / 2);
dc.LineTo(rect.right - 20, rect.top + rect.Height() / 2);
}
再次运行程序,发现示例框中出现了一条示例线条。
改变线宽的值,但是发现示例组框中线条的宽度并没有发生变化。当一个控件与一个成员变量关联时,
2.3.3)如果想让控件上的值反映到成员变量上,必须调用UpdateData函数。
void CSettingDlg::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: 在此处添加消息处理程序代码
// 不为绘图消息调用 CDialog::OnPaint()
UpdateData();
CPen pen(m_nLineStyle,m_nLineWidth, RGB(244,0,0));
dc.SelectObject(&pen);
//获得IDC_SAMPLE组框的矩形
CRect rect;
GetDlgItem(IDC_SAMPLE)->GetWindowRect(&rect);
ScreenToClient(&rect);
//用选择的画笔线型和线宽画一条示例线,
//示例线的坐标参考组框矩形的坐标位置,起点横坐标:矩形左上角右移20;起点纵坐标:矩形中间位置的纵坐标
//即画一条距组框左侧20pixel到距组框右侧20像素处的线。
dc.MoveTo(rect.left + 20, rect.top + rect.Height() / 2);
dc.LineTo(rect.right - 20, rect.top + rect.Height() / 2);
}
再次运行Graphic程序,打开设置对话框,改变线宽,或在线宽为1时,选择不同线型,这时在设置对话框的示例组中随时可以看到用户所做的改变。
⭕⭕3)选择的颜色也要反映到示例线条上。
如果希望用户选择了颜色之后,也要反映到示例线条上,就要:
3.1)为设置对话框再添加一个COLORREF类型的成员变量: m_clr,因为将在CGraphicView类中访问这个变量,因此将此变量设置为公有的。
3.2)并在对话框的构造函数中将此变量初始化红色:
CSettingDlg::CSettingDlg(CWnd* pParent /*=nullptr*/)
: CDialog(IDD_DLG_SETTING, pParent)
, m_nLineWidth(0)
, m_nLineStyle(0)
{
m_clr = RGB(255, 0, 0);
}
3.3)因为对现在的Graphic程序来说,当用户利用颜色对话框选择某种颜色后,选择的结果就会保存到CGraphicView类的m_clr变量,所以应该在CGraphicView类中在设置对话框显示时将该变量保存的颜色值传递给设置对话框的m_clr变量,即在CGraphicView类的OnSetting函数中,在调用CSettingDlg对象的DoModal函数之前将变量保存的颜色值传递给设置对话框的m_clr变量:
void CGraphicView::OnSetting()
{
// TODO: 在此添加命令处理程序代码
CSettingDlg dlg;
dlg.m_nLineWidth = m_nLineWidth;
dlg.m_nLineStyle = m_nLineStyle;
//变量保存的颜色值传递给设置对话框的m_clr变量
dlg.m_clr = m_clr;
if (IDOK == dlg.DoModal()) {
m_nLineWidth = dlg.m_nLineWidth;
m_nLineStyle = dlg.m_nLineStyle;
}
}
3.4)然后修改CSettingDlg类的OnPaint函数中创建画笔的代码,将m_clr的值传递给该创建函数的第三个参数:
void CSettingDlg::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: 在此处添加消息处理程序代码
// 不为绘图消息调用 CDialog::OnPaint()
UpdateData();
CPen pen(m_nLineStyle,m_nLineWidth, m_clr);
dc.SelectObject(&pen);
//获得IDC_SAMPLE组框的矩形
CRect rect;
GetDlgItem(IDC_SAMPLE)->GetWindowRect(&rect);
ScreenToClient(&rect);
//用选择的画笔线型和线宽画一条示例线,
//示例线的坐标参考组框矩形的坐标位置,起点横坐标:矩形左上角右移20;起点纵坐标:矩形中间位置的纵坐标
//即画一条距组框左侧20pixel到距组框右侧20像素处的线。
dc.MoveTo(rect.left + 20, rect.top + rect.Height() / 2);
dc.LineTo(rect.right - 20, rect.top + rect.Height() / 2);
}
再次运行Graphic程序,打开颜色对话框选择某种颜色,然后打开设置对话框,这时可以看到示例组框中线条的颜色就是刚才所选的颜色。
通常,看到的对话框及其上的控件的背景都是浅灰色的,有时为了使程序的界面更加美观,需要更改它们的背景,以及控件上的文本的颜色。
首先介绍一个消息:WM_CTLCOLOR,它的响应函数是CWnd类的OnCtIColor。该函数的声明:
afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
■ pDC
指向当前要绘制的控件的显示上下文的指针。
◼ pWnd
指向当前要绘制的控件的指针。
◼ nCtlColor
指定当前要绘制的控件的类型,它的取值如下:
该函数将返回将被用来绘制控件背景的画刷的句柄。当一个子控件将要被绘制时,它都会向它的父窗口发送一个WM_CTLCOLOR(这个父窗口通常都是对话框)消息来准备一个设备上下文(即上述pDC参数),以便使用正确的颜色来绘制该控件。如果想要改变该控件上的文本颜色,可以在OnCtlColor函数中以指定的颜色为参数调用SetTextColor函数来实现。
对对话框来说,它上面的每一个控件在绘制时都要向它发送WM_CTLCOLOR消息,它需要为每一个控件准备一个DC,该DC将通过pDC参数传递给OnCtlColor函数。也就是说,对话框对象的OnCtlColor这个消息响应函数会被多次调用。
下面为Graphic程序的设置对话框(CSettingDlg对象)捕获WM_CTLCOLOR消息,即添加该消息的响应处理:
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: 在此更改 DC 的任何特性
// TODO: 如果默认的不是所需画笔,则返回另一个画笔
return hbr;
}
可以看到,在OnCtlColor这个消息响应函数中,首先调用对话框基类: CDialog的OnCtIColor函数,返回一个画刷句柄(hbr),然后该函数直接返回这个画刷句柄。之后,系统就会使用这个画刷句柄来绘制对话框及其子控件的背景。如果想要改变对话框的背景色,只需要自定义一个画刷,然后让OnCtIColor函数返回这个画刷句柄即可。
⭕⭕1)首先为CSettingDlg类定义一个CBrush类型的私有成员变量: m_brush:
private:
CBrush m_brush;
⭕⭕2)在其构造函数中利用CreateSolidBrush函数将该画刷初始化为一个粉色的画刷:
CSettingDlg::CSettingDlg(CWnd* pParent /*=nullptr*/)
: CDialog(IDD_DLG_SETTING, pParent)
, m_nLineWidth(0)
, m_nLineStyle(0)
{
m_clr = RGB(255, 0, 0);
//初始化为一个粉色的画刷
m_brush.CreateSolidBrush(RGB(255,193,193));
}
⭕⭕3)然后在OnCtIColor响应函数返回时返回上述自定义的画刷:m_brush:
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: 在此更改 DC 的任何特性
// TODO: 如果默认的不是所需画笔,则返回另一个画笔
//return hbr;
return m_brush;
}
运行Graphic程序,打开设置对话框:
可以看到对话框和控件的背景都变成了粉色:这是因为在该对话框绘制时,会调用OnCtlColor函数绘制整个对话框的背景,即用m_brush画刷来绘制对话框背景。当绘制其子控件时,也是调用这个OnCtlColor函数,也是用m_brush这个画刷来绘制背景的。因此我们看到子控件和对话框的背景都是粉色的。而对于OK和Cancel两个按钮的背景不改变的原因,稍后介绍。
如果想要精确控制对话框上某个控件(例如本例中的线型组框)的背景的绘制,就需要判断当前绘制的是哪一个控件。
⭕⭕1)通过上面的介绍,通过OnCtlColor函数的第二个参数:pWnd能够知道当前绘制的控件窗口对象,这时可以通过调用Cwnd类的GetDlgCtrlID成员函数得到该控件的ID,然后判断该ID是否就是需要控制其背景绘制的控件ID,若是就处理。GetDlgCtrllD函数的声明:
int GetDlgCtrlID() const;
该函数不仅能返回对话框子控件的ID,还能返回子窗口的ID。但是因为顶层窗口不具有ID值,所以如果调用该函数的CWnd对象是一个顶层窗口,该函数返回的值就是一个无效值。
⭕⭕2)在CSettingDlg类的OnCtIColor函数中就可以利用传递进来的pWnd来调用GetDlgCtrlD函数,然后判断一下如果其返回值等于线型组框的ID (IDC_LINE_STYLE),那么就可以知道当前绘制是的线型组框,那就需要改变该控件的背景色,即返回自定义的画刷,对其他控件仍使用先前的画刷。
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: 在此更改 DC 的任何特性
// TODO: 如果默认的不是所需画笔,则返回另一个画笔
//ID=IDC_LINE_STYLE的控件的背景设置为粉色
if (pWnd->GetDlgCtrlID()==IDC_LINE_STYLE)
{
return m_brush;
}
//其他的背景色保存默认。
return hbr;
}
运行Graphic程序,打开设置对话框,可以发现线型组框的背景色变成了粉色。
OnCtlColor函数要求返回HBRUSH类型的画刷句柄,但上述代码中返回了一个CBrush类型的画刷对象。这是因为CBrush类重载了HBRUSH操作符。
⭕⭕3)为了改变线型组框控件上的文本颜色,应在OnCtlColor消息响应函数对当前绘制的控件进行判断,如果判断出当前绘制的控件就是线型组框控件,在返回自定义的画刷之前,就调用SetTextColor函数将该控件上的文本设置为希望的颜色。本例将线型组框控件上的文本设置为绿色:
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: 在此更改 DC 的任何特性
// TODO: 如果默认的不是所需画笔,则返回另一个画笔
//ID=IDC_LINE_STYLE的控件的背景设置为粉色
if (pWnd->GetDlgCtrlID()==IDC_LINE_STYLE)
{
//将线型组框控件上的文本设置为绿色
pDC->SetTextColor(RGB(0, 100, 0));
return m_brush;
}
//其他的背景色保存默认。
return hbr;
}
设置的颜色无效,暂时不知道原因。
⭕⭕4)实现编辑框控件背景的改变。实现原理同上,这时在OnCtlColor函数中如果判断当前绘制的是编辑框控件,就设置文本颜色,并返回自定义的画刷。
因此可以参照上面修改线型组框控件的代码来实现编辑框控件背景色和文本颜色的改变:
4.1)改变编辑框的背景颜色
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: 在此更改 DC 的任何特性
// TODO: 如果默认的不是所需画笔,则返回另一个画笔
/*ID=IDC_LINE_STYLE的控件的背景设置为粉色
if (pWnd->GetDlgCtrlID()==IDC_LINE_STYLE)
{
//将线型组框控件上的文本设置为绿色
pDC->SetTextColor(RGB(0,100,0));
//将线型组框上的文字的背景设置为透明的
pDC->SetBkMode(TRANSPARENT);
return m_brush;
}*/
if (pWnd->GetDlgCtrlID()== IDC_LINE_WIDTH)
{
pDC->SetTextColor(RGB(0, 100, 0));
return m_brush;
}
//其他的背景色保存默认。
return hbr;
}
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: 在此更改 DC 的任何特性
// TODO: 如果默认的不是所需画笔,则返回另一个画笔
/*ID=IDC_LINE_STYLE的控件的背景设置为粉色
if (pWnd->GetDlgCtrlID()==IDC_LINE_STYLE)
{
//将线型组框控件上的文本设置为绿色
pDC->SetTextColor(RGB(0,100,0));
//将线型组框上的文字的背景设置为透明的
pDC->SetBkMode(TRANSPARENT);
return m_brush;
}*/
if (pWnd->GetDlgCtrlID()== IDC_LINE_WIDTH)
{
pDC->SetTextColor(RGB(255, 0, 0));
return m_brush;
}
//其他的背景色保存默认。
return hbr;
}
4.3)有些控件上的文本本身也有背景色,设置背景色为透明模式,将编辑框上的文字的背景设置为透明的:
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: 在此更改 DC 的任何特性
// TODO: 如果默认的不是所需画笔,则返回另一个画笔
/*ID=IDC_LINE_STYLE的控件的背景设置为粉色
if (pWnd->GetDlgCtrlID()==IDC_LINE_STYLE)
{
//将线型组框控件上的文本设置为绿色
pDC->SetTextColor(RGB(0,100,0));
//将线型组框上的文字的背景设置为透明的
pDC->SetBkMode(TRANSPARENT);
return m_brush;
}*/
if (pWnd->GetDlgCtrlID()== IDC_LINE_WIDTH)
{
pDC->SetTextColor(RGB(255, 0, 0));
pDC->SetBkMode(TRANSPARENT);
return m_brush;
}
//其他的背景色保存默认。
return hbr;
}
4.4)如果要改变单行编辑框控件的背景颜色,可以调用SetBkColor函数设置其背景色。设置其背景色为黑色:
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: 在此更改 DC 的任何特性
// TODO: 如果默认的不是所需画笔,则返回另一个画笔
/*ID=IDC_LINE_STYLE的控件的背景设置为粉色
if (pWnd->GetDlgCtrlID()==IDC_LINE_STYLE)
{
//将线型组框控件上的文本设置为绿色
pDC->SetTextColor(RGB(0,100,0));
//将线型组框上的文字的背景设置为透明的
pDC->SetBkMode(TRANSPARENT);
return m_brush;
}*/
if (pWnd->GetDlgCtrlID()== IDC_LINE_WIDTH)
{
pDC->SetTextColor(RGB(255, 0, 0));
//pDC->SetBkMode(TRANSPARENT);
pDC->SetBkColor(RGB(0, 0, 0));
return m_brush;
}
//其他的背景色保存默认。
return hbr;
}
- SetTextColor()函数很明显是设置文本颜色的;
- SetBkColor()函数不是用来设置控件背景颜色的,而是用来设置文本背景颜色的,就是包含文本的矩形;
- SetBkMode()是用来设定文字背景模式的,参数只有两个选择OPAQUE、TRANSPARENT表示是否透明。
接下来,改变控件上文本的字体。也就是说,在绘制控件时为其准备一种字体,让它按照此种字体显示。
⭕⭕1)为了显示控件字体的改变效果,再为Graphic程序的设置对话框资源增加一个静态文本控件,设置其ID为: IDC_TEXT, Caption:WaitFoF,你好啊!:
⭕⭕2)然后在程序中修改该控件的文本字体。先为CSettingDlg类增加一个CFont类型的私有成员变量: m_font:
⭕⭕3)其构造函数中初始化该变量,创建一个大小为70,名称为“汉仪综艺体简”的字体。:
CSettingDlg::CSettingDlg(CWnd* pParent /*=nullptr*/)
: CDialog(IDD_DLG_SETTING, pParent)
, m_nLineWidth(0)
, m_nLineStyle(0)
{
m_clr = RGB(255, 0, 0);
//初始化为一个粉色的画刷
m_brush.CreateSolidBrush(RGB(255, 193, 193));
//创建一个大小为70,名称为“汉仪综艺体简”的字体
m_font.CreatePointFont(70, _T("汉仪综艺体简"));
}
⭕⭕4)然后在CSettingDlg类的OnCtlColor函数中判断当前绘制的如果是静态文本框控件,那么就将新建的字体m_font选入设备描述表中,这样DC中的字体就被改变了,它就会使用新创建的字体来显示文本:
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: 在此更改 DC 的任何特性
// TODO: 如果默认的不是所需画笔,则返回另一个画笔
//ID=IDC_LINE_STYLE的控件的背景设置为粉色
if (pWnd->GetDlgCtrlID()==IDC_LINE_STYLE)
{
//将线型组框控件上的文本设置为绿色
pDC->SetTextColor(RGB(0,100, 0));
//将线型组框上的文字的背景设置为透明的
pDC->SetBkMode(TRANSPARENT);
return m_brush;
}
if (pWnd->GetDlgCtrlID()== IDC_LINE_WIDTH)
{
pDC->SetTextColor(RGB(255,0,0));
//pDC->SetBkMode(TRANSPARENT);
pDC->SetBkColor(RGB(0, 0, 0));
return m_brush;
}
if (pWnd->GetDlgCtrlID() == IDC_TEXT)
{
pDC->SelectObject(&m_font);
}
//其他的背景色保存默认。
return hbr;
}
接下来,根据上面的知识。按照同样的方法改变Graphic程序中设置对话框上的OK按钮的背景色及其文字的颜色。
⭕⭕1)在CSettingDlg类的OnCtlColor函数中,如果判断出当前绘制的控件的ID等于OK按钮的ID:IDOK,那么就返回自定义的画刷:
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: 在此更改 DC 的任何特性
// TODO: 如果默认的不是所需画笔,则返回另一个画笔
//ID=IDC_LINE_STYLE的控件的背景设置为粉色
if (pWnd->GetDlgCtrlID()==IDC_LINE_STYLE)
{
//将线型组框控件上的文本设置为绿色
pDC->SetTextColor(RGB(255, 0, 0));
//pDC->SetBkColor(RGB(255, 0, 0));
//将线型组框上的文字的背景设置为透明的
//pDC->SetBkMode(TRANSPARENT);
return m_brush;
}
if (pWnd->GetDlgCtrlID()== IDC_LINE_WIDTH)
{
pDC->SetTextColor(RGB(255,0,0));
//pDC->SetBkMode(TRANSPARENT);
//背景黑色
pDC->SetBkColor(RGB(0, 0, 0));
return m_brush;
}
if (pWnd->GetDlgCtrlID() == IDC_TEXT)
{
pDC->SelectObject(&m_font);
}
if (pWnd->GetDlgCtrlID() == IDOK)
{
//红色字体
pDC->SetTextColor(RGB(255, 0, 0));
return m_brush;
}
//其他的背景色保存默认。
return hbr;
}
运行Graphic程序,发现新加的代码并没有起作用,OK按钮的背景色和文字颜色并没有被改变。
对于按钮来说,使用上述方法来改变其背景和文本颜色是无效的,只能寻找其他的解决方法。
⭕⭕2)实际上,如果想要改变按钮控件的背景色和文本颜色,需要使用CButton类的一个成员函数:DrawItem,该函数的声明:
virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
从其声明形式,可以知道这个函数是一个虚函数。当一个自绘制按钮(具有BS_OWNERDRAW风格的按钮)在绘制时,框架将会调用这个虚函数。因此,如果想要实现一个自绘制按钮控件的绘制,应该重载这个虚函数。该函数的参数是DRAWITEMSTRUCT结构类型,其定义如下所示:
typedef struct tagDRAWITEMSTRUCT {
UINT CtlType;
UINT CtlID;
UINT itemID;
UINT itemAction;
UINT itemState;
HWND hwndItem;
HDC hDC;
RECT rcItem;
ULONG_PTR itemData;
} DRAWITEMSTRUCT;
该结构体中有一个hDC成员,指向将要绘制的按钮的DC。为了绘制这个按钮,可以向该DC中选入自定义的颜色、画刷等对象。但此重载函数结束前,一定要恢复hDC中原有对象。
因此,如果想要改变OK按钮的文本颜色:
这样,在绘制OK按钮时,框架就会调用这个自定义的按钮类的Drawltem函数来绘制。
◼▪◼ 改变按钮文字颜色
2.1)Graphic程序创建一个派生于CButton的类,新建MFC类。将新增的类命名为:CTestBtn、基类: CButton。
CTestBtn.h:
#pragma once
// CTestBtn
class CTestBtn : public CButton
{
DECLARE_DYNAMIC(CTestBtn)
public:
CTestBtn();
virtual ~CTestBtn();
protected:
DECLARE_MESSAGE_MAP()
};
CTestBtn.cpp:
// CTestBtn.cpp: 实现文件
//
#include "pch.h"
#include "Graphic.h"
#include "CTestBtn.h"
// CTestBtn
IMPLEMENT_DYNAMIC(CTestBtn, CButton)
CTestBtn::CTestBtn()
{
}
CTestBtn::~CTestBtn()
{
}
BEGIN_MESSAGE_MAP(CTestBtn, CButton)
END_MESSAGE_MAP()
// CTestBtn 消息处理程序
2.2)为此类添加DrawItem虚函数的重写:
void CTestBtn::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
// TODO: 添加您的代码以绘制指定项
UINT uStyle = DFCS_BUTTONPUSH;
//这段代码只对按钮有效。
ASSERT(lpDrawItemStruct->CtlType == ODT_BUTTON);
//如果选择绘图,将样式添加到DrawFrameControl。
if (lpDrawItemStruct->itemState & ODS_SELECTED)
uStyle |= DFCS_PUSHED;
// 绘制按钮框架
::DrawFrameControl(lpDrawItemStruct->hDC, &lpDrawItemStruct->rcItem, DFC_BUTTON, uStyle);
//获取按钮的文本。
CString strText;
GetWindowText(strText);
//按钮文字使用文字的颜色为绿色。
COLORREF crOldColor = ::SetTextColor(lpDrawItemStruct->hDC, RGB(0, 255, 0));
::DrawText(lpDrawItemStruct->hDC,strText,strText.GetLength(),
&lpDrawItemStruct->rcItem,DT_SINGLELINE | DT_VCENTER | DT_CENTER);
::SetTextColor(lpDrawItemStruct->hDC, crOldColor);
}
2.3)为设置对话框上的OK按钮关联一个成员变量。将变量名称设置为:m_btnTest,类型选择为:CTestBtn:
在SettingDlg.h文件中包含CTestBtn类的定义文件:
#include "CTestBtn.h"
此外自绘制控件应该具有BS_OWNERDRAW风格,这可以通过设置控件属性对话框中的Owner Draw选项为True:
运行Graphic程序,打开设置对话框,这时可以看到OK按钮的文字变成绿色,即CTestBtn类的设置起作用:
此时可以删除CSettingDlg类中OnCtlColor中关于OK按钮的代码。
◼▪◼ 改变按钮背景颜色
为了改变OK按钮的背景色,使用CButtonST类,此类是在网上找到的一个按钮类,这个类的功能比较丰富,可以用于实际开发中。
CButtonST类的源码中含有如下文件:
为了演示CButtonST类的使用,在Graphic程序中的设置对话框资源上再添加一个按钮控件,将其ID设置:IDC_BTN_ST,Caption设置为:ButtonST:
下面演示使用这个类:
2.1)将类的头文件和源文件复制到自己的Graphic工程所在目录下,并将它们添加到自己的Graphic工程中。方法如下:
2.2)在SettingDlg.h文件中包含CButtonST类的头文件:
#include "BtnST.h"
2.3)为CButtonST按钮添加一个关联的成员变量:
与上面设置OK按钮背景色和文本颜色的实现方法不同的是,使用CButtonST这个类时,并不需要通过按钮属性对话框设置其Owner Draw选项为True,这个类的内部会自动设置BS_OWNERDRAW分格。
2.4)接下来初始化m_btnST变量,可以放到CSettingDlg类的OnInitDialog函数中进行,所以在CSettingDlg中重写OnInitDialog函数。
初始化工作包括两个部分:设置背景色和前景色,后者也就是按钮活动时文字的颜色。
BOOL CSettingDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// TODO: 在此添加额外的初始化
//造成了m_btnST的重复子类化————屏蔽
//m_btnST.SubclassDlgItem(IDC_BTN_ST,this);
//普通状态时的背景色 #9400D3
m_btnST.SetColor(CButtonST::BTNST_COLOR_BK_OUT, RGB(148,0,211));
//普通状态时的前景色
m_btnST.SetColor(CButtonST::BTNST_COLOR_FG_OUT, RGB(255,0,0));
//鼠标放在按钮内时的背景色
m_btnST.SetColor(CButtonST::BTNST_COLOR_BK_IN, RGB(205,155,155));
//鼠标放在按钮内时的前景色
m_btnST.SetColor(CButtonST::BTNST_COLOR_FG_IN, RGB(72, 118, 255));
//按钮被按下后的背景色
m_btnST.SetColor(CButtonST::BTNST_COLOR_BK_FOCUS, RGB(255,127, 0));
return TRUE; // return TRUE unless you set the focus to a control
// 异常: OCX 属性页应返回 FALSE
}
使用的工具类,可点击下载:CButtonST
有多种方式可以实现在窗口中显示位图:
第一步是创建位图,这可以先利用CBitmap构造一个位图对象,然后利用LoadBitmap函数加载一幅位图资源。
第二步是创建兼容DC。其中CreateCompatibleDC函数将创建一个内存设备上下文,与参数pDC所指定的DC相兼容。内存设备上下文实际上是一个内存块,表示一个显示的表面。如果想把图像复制到实际的DC中,可以先用其兼容的内存设备上下文在内存中准备这些图像,然后再将这些数据复制到实际DC中。
第三步是将位图选入兼容DC中。当兼容的内存设备上下文被创建时,它的显示表面是标准的一个单色像素宽和一个单色像素高。在应用程序中可以使用内存设备上下文进行绘图操作之前,必须将一个具有正确高度和宽度的位图选入设备上下文,这时内存设备上下文的显示表面大小就由当前选入的位图决定了。
第四步是将兼容DC中的位图贴到当前DC中。有多个函数可以以几种不同的方式完成这一操作。调用BitBlt函数将兼容DC中的位图复制到当前DC中。BitBlt函数的功能是把源设备上下文中的位图复制到目标设备上下文中。该函数的声明形式如下所示:
BOOL BitBlt(int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc,int ySrc, DWORD dwRop);
◼ x和y
指定目标矩形区域左上角的x和y坐标。
◼ nWidth和nHeight
指定源和目标矩形区域的逻辑宽度和高度。因为该函数在复制时是按照1:1的比例进行的,所以源矩形区域和目标矩形区域的宽度和高度是相同的。
◼ pSrcDC
指向源设备上下文对象。
◼ xSrc 和 ySrc
指定源矩形区域左上角的x和y坐标。
◼ dwRop
指定复制的模式,也就是指定源矩形区域的颜色数据,如何与目标矩形区域的颜色数据组合以得到最后的颜色。
如果函数调用成功,则BitBIt函数将返回非0值;否则返回0。
按照上述四个步骤,来完成在窗口显示位图的功能。
⭕⭕1)首先需要准备一幅位图。
调整位图:
⭕⭕2)完成位图的显示。
首先需要了解一下窗口的绘制过程,包含两个步骤:
◼▪◼擦除窗口背景时完成位图的显示。
当擦除窗口背景时,程序会发送一个WM_ERASEBKGND消息,因此可以在此响应函数中完成位图的显示。另外,根据前面的知识,可以知道应该在视类窗口中进行位图的绘制。
因此为Graphic工程的CGraphicView类添加WM_ERASEBKGND消息的响应处理函数:OnEraseBkgnd。
BOOL CGraphicView::OnEraseBkgnd(CDC* pDC)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
//第一步:
//首先构建位图对象: bitmap
CBitmap bitmap;
//加载图像
bitmap.LoadBitmap(IDB_BITMAP1);
//第二步:创建兼容的DC
//创建与当前DC (pDC)兼容的DC: dcCompatible
CDC dcCompatible;
dcCompatible.CreateCompatibleDC(pDC);
//第三步:将位图选入兼容DC中
//调用SelectObject函数将位图选入兼容DC中,从而确定兼容DC显示表面的大小
dcCompatible.SelectObject(&bitmap);
//第四步:将兼容DC (dcCompatible)中的位图复制到目的DC (pDC)中
//因为要指定复制目的矩形区域的宽度和高度,首先需要得到目的DC客户区大小,所以就构造一个CRect对象
CRect rect;
//然后调用GetClientRect函数得到客户区大小
GetClientRect(&rect);
//源DC就是先前创建的兼容DC,
//复制模式选择: SRCCOPY:就是将源位图复制到目的矩形区域中
pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &dcCompatible, 0, 0, SRCCOPY);
//最后,调用视类的基类(即CView)的OnEraseBkgnd函数。
return CView::OnEraseBkgnd(pDC);
}
运行Graphic程序,但是看到程序窗口在显示时,位图只是闪了一下就消失了。主要是因为在上述OnEraseBkgnd代码中,在调用BitBIt函数复制位图之后,又调用了视类的基类(即CView)的OnEraseBkgnd函数,该函数的调用把窗口的背景擦除了,所以位图只是闪了一个就消失了。
⭕⭕3)对OnEraseBkgnd函数来说,如果其擦除了窗口背景,将返回非0值。因此CGraphicView类的OnEraseBkgnd函数的最后,不应该再调用其基类的OnEraseBkgnd函数,而是应该直接返回TRUE值:
BOOL CGraphicView::OnEraseBkgnd(CDC* pDC)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
//第一步:
//首先构建位图对象: bitmap
CBitmap bitmap;
//加载图像
bitmap.LoadBitmap(IDB_BITMAP1);
//第二步:创建兼容的DC
//创建与当前DC (pDC)兼容的DC: dcCompatible
CDC dcCompatible;
dcCompatible.CreateCompatibleDC(pDC);
//第三步:将位图选入兼容DC中
//调用SelectObject函数将位图选入兼容DC中,从而确定兼容DC显示表面的大小
dcCompatible.SelectObject(&bitmap);
//第四步:将兼容DC (dcCompatible)中的位图复制到目的DC (pDC)中
//因为要指定复制目的矩形区域的宽度和高度,首先需要得到目的DC客户区大小,所以就构造一个CRect对象
CRect rect;
//然后调用GetClientRect函数得到客户区大小
GetClientRect(&rect);
//源DC就是先前创建的兼容DC,
//复制模式选择: SRCCOPY:就是将源位图复制到目的矩形区域中
pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &dcCompatible, 0, 0, SRCCOPY);
return TRUE;
}
⭕⭕4)位图压缩和拉伸。
4.1)若要在窗口中完整地显示一幅位图,如果位图比窗口大,就要压缩位图;如果小,就可以拉伸位图。然而BitBlt函数是没有办法实现压缩和拉伸的,因为它是按照1:1的比例进行复制的。再介绍另一个显示位图的函数:StretchBlt
,其声明:
BOOL StretchBlt (int x, int y, int nwidth, int nHeight, CDC* pSrcDc, int xSrc, int ysrc, int nSrcwidth, int nSrcHeight, DWORD dwRop );
该函数与前面介绍的BitBlt函数的功能基本相同,都是从源矩形区域中复制一个位图到目标矩形,但是与BitBlt函数不同的是:StretchBlt函数可以实现位图的拉伸或压缩,以适合目的矩形区域的尺寸。
与BitBlt函数相比,StretchBlt函数只是多了两个参数: nSrcWidth和nSrcHeight,分别用来指示源矩形区域的宽度和高度。
4.2)前面已经说过,兼容DC原始只有1个像素大小。它的大小由选入的位图的大小所决定,也就是说,如果想要得到源矩形的宽度和高度,就要想办法得到选入的位图的宽度和高度。选入的位图的宽度和高度可以通过调用CBitmap类的GetBitmap函数来得到,该函数的原型声明如下:
int GetBitmap(BITMAP* pBitMap);
该函数有一个参数,是一个指向BITMAP结构体的指针,该函数将用位图的信息填充,BITMAP结构体的定义如下:
typedef struct tagBITMAP
{
LONG bmType;
LONG bmWidth;
LONG bmHeight;
LONG bmWidthBytes;
WORD bmPlanes;
WORD bmBitsPixel;
LPVOID bmBits;
} BITMAP;
其中bmWidth指示位图的宽度,bmHeight指示位图的高度。
4.3)因此在CGraphicView类的OnEraseBkgnd函数中,在位图对象bitmap成功加载位图资源之后,通过调用CBitmap类的GetBitmap函数来得到选入的位图的宽度和高度,接下来就可以调用StretchBlt函数复制位图了。
BOOL CGraphicView::OnEraseBkgnd(CDC* pDC)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
//第一步:
//首先构建位图对象: bitmap
CBitmap bitmap;
//加载图像
bitmap.LoadBitmap(IDB_BITMAP1);
BITMAP bmp;
bitmap.GetBitmap(&bmp);
//第二步:创建兼容的DC
//创建与当前DC (pDC)兼容的DC: dcCompatible
CDC dcCompatible;
dcCompatible.CreateCompatibleDC(pDC);
//第三步:将位图选入兼容DC中
//调用SelectObject函数将位图选入兼容DC中,从而确定兼容DC显示表面的大小
dcCompatible.SelectObject(&bitmap);
//第四步:将兼容DC (dcCompatible)中的位图复制到目的DC (pDC)中
//因为要指定复制目的矩形区域的宽度和高度,首先需要得到目的DC客户区大小,所以就构造一个CRect对象
CRect rect;
//然后调用GetClientRect函数得到客户区大小
GetClientRect(&rect);
//源DC就是先前创建的兼容DC,
//复制模式选择: SRCCOPY:就是将源位图复制到目的矩形区域中
//pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &dcCompatible, 0, 0, SRCCOPY);
pDC->StretchBlt(0, 0, rect.Width(), rect.Height(), &dcCompatible, 0, 0, bmp.bmWidth, bmp.bmHeight, SRCCOPY);
return TRUE;
}
运行Graphic程序,这时就可以看到整幅位图都显示出来了:
◼▪◼窗口重绘时完成位图的显示。
本例是在窗口显示更新的第一步,即擦除窗口背景这一步实现位图的显示。也可以在窗口显示更新的第二步,即重绘窗口时实现这一功能。窗口重绘时会调用OnDraw函数,因此可以把显示位图的代码放到这个函数中。
void CGraphicView::OnDraw(CDC* pDC)
{
CGraphicDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;
// TODO: 在此处为本机数据添加绘制代码
CFont* pOldFont = pDC->SelectObject(&m_font);
pDC->TextOutW(0, 0, m_strFontName);
pDC->SelectObject(pOldFont);
//第一步:
//首先构建位图对象: bitmap
CBitmap bitmap;
//加载图像
bitmap.LoadBitmap(IDB_BITMAP1);
BITMAP bmp;
bitmap.GetBitmap(&bmp);
//第二步:创建兼容的DC
//创建与当前DC (pDC)兼容的DC: dcCompatible
CDC dcCompatible;
dcCompatible.CreateCompatibleDC(pDC);
//第三步:将位图选入兼容DC中
//调用SelectObject函数将位图选入兼容DC中,从而确定兼容DC显示表面的大小
dcCompatible.SelectObject(&bitmap);
//第四步:将兼容DC (dcCompatible)中的位图复制到目的DC (pDC)中
//因为要指定复制目的矩形区域的宽度和高度,首先需要得到目的DC客户区大小,所以就构造一个CRect对象
CRect rect;
//然后调用GetClientRect函数得到客户区大小
GetClientRect(&rect);
//源DC就是先前创建的兼容DC,
//复制模式选择: SRCCOPY:就是将源位图复制到目的矩形区域中
//pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &dcCompatible, 0, 0, SRCCOPY);
pDC->StretchBlt(0, 0, rect.Width(), rect.Height(), &dcCompatible, 0, 0, bmp.bmWidth, bmp.bmHeight, SRCCOPY);
}
总结比较:程序显示的结果是一样的,但是产生的效果是不一样的。