MFC中进行与绘图相关的控制,首先先为VIEW类添加一个成员变量 m_nDrawType,用来指定要画的图形 ,增加相应的菜单项用来绘制 点 直线 矩形 和 椭圆 在菜单项的响应函数中写下面的代码:
view plain
void CGraphicView::OnDot()
{
// TODO: Add your command handler code here
m_nDrawType = 1;
}
void CGraphicView::OnLine()
{
// TODO: Add your command handler code here
m_nDrawType = 2;
}
void CGraphicView::OnRectangle()
{
// TODO: Add your command handler code here
m_nDrawType = 3;
}
void CGraphicView::OnEllipse()
{
// TODO: Add your command handler code here
m_nDrawType = 4;
}
知道每种图形相应的序号,然后就要绘图,先是相应LBUTTONDOWM消息,记录下当前点,然后再响应 LBUTTONUP消息 进行绘图操作,代码如下:
view plain
void CGraphicView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
m_ptOrigin = point;
CView::OnLButtonDown(nFlags, point);
}
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
CClientDC dc(this);
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);
}
其中m_pOrigin 是CPoint类的对象,变量 m_nLineStyle m_nLineWidth 在后面会用到,分别是线的样式和线的宽度。
为了在绘图过程中用透明画刷填充图形内部,所以用GetSocokObect()函数
view plain
HGDIOBJ GetStockObject( int fnObject // stock object type);
关于它的参数可以在MSDN中查到,他的返回值是HGDIOBJ类型的,我们需要将他转换为HRUSH类型的 因为我们要调用 CBrush类的一个静态成员函数FromHandle将画刷句柄转换为相应的画刷指针。
接下来要创建一个设置绘画相关属性的对话框 该对话框最终的样子是下面这样的:
相应的需要创建一个新的编辑框的类,这里类名是 CSettingDlg
现在先从创建线宽这个编辑框开始,编辑框ID设为 IDC_LINE_WIDTH ,然后在菜单项中增加一个设置的菜单项,在响应该菜单项的函数中显示该对话框,为了将用户输入的线宽保存下来,当然要为该编辑框关联一个变量 UINT类型的 m_nLineWidth 然后在用户输入完线宽按OK后将该值保存到VIEW类的m_nLineWidth中,为了用户再次打开的时候可以看到自己上次设的线宽是多少,我们将VIEW类中m_nLineWidth的值赋给对话框的m_nLineWidth ,下面是VIEW类的设置菜单项的响应函数:
view plain
void CGraphicView::OnSetting()
{
CSettingDlg dlg;
dlg.m_nLineWidth = m_nLineWidth;
dlg.m_nLineStyle=m_nLineStyle;
dlg.m_clr = m_clr;
if(IDOK==dlg.DoModal())
{
m_nLineWidth = dlg.m_nLineWidth;
m_nLineStyle = dlg.m_nLineStyle;
}
}
里面还有其他变量的赋值操作,在下面会说到。
当我们得到用户给定的线宽后,在LBUTTONUP响应函数中,就要将画笔对象的线宽参数改为m_nLineWidth了,这样就可以了。
现在再添加一个设置线形的组框,在其中添加三个单选按钮,标题如上图所示。我们在MFC APPWizard中要为单选框关联变量时,并没有看到他们的ID,这是因为在一个组框中的单选框,要将第一个单选按钮的属性中的Groud勾上,这样就可以找到第一个单选按钮的ID了,为它关联一个变量 m_nLineStyle,可以看到在对话框的构造函数中,它被初始化为-1了,当我们选中第一个单选按钮的时候,它的值是0,以此类推。和上面的线宽的一样,也是要让VIEW类保存用户选择的线型。
这里要再说一个知识点,就是画笔的属性比如 PS_SOLID PS_DASH PS_DOT 这些都是在MFC底层代码中定义好的一些常量,我们可以通过在CPEN类的对象构造函数的相应属性,点击右键 然后GO to Definition of... 去看到那些画笔样式的定义。
view plain
/* Pen Styles */
#define PS_SOLID 0
#define PS_DASH 1 /* ------- */
#define PS_DOT 2 /* ....... */
#define PS_DASHDOT 3 /* _._._._ */
#define PS_DASHDOTDOT 4 /* _.._.._ */
#define PS_NULL 5
#define PS_INSIDEFRAME 6
#define PS_USERSTYLE 7
#define PS_ALTERNATE 8
#define PS_STYLE_MASK 0x0000000F
在这里我们看到,实线、虚线、点线的值和它们所对应的单选按钮的值是一样的,当然这里是故意这么排列的,实际中应该先判断值,然后再用相应的画笔样式去构造画笔。
下面要创建一个颜色对话框,样子就是我们在绘图中看到的那个,自己要写的话,很麻烦,MFC为我们提供了一个类:CColorDialog ,可以很方便地创建一个颜色对话框。
在菜单项中添加颜色选项,然后在VIEW类的响应函数中创建该对话框。下面我们要做的事就是保存用户选择的颜色。在CColorDialog类中有一个CHOOSECOLOR结构体类型的成员变量 m_cc
view plain
typedef struct tagCHOOSECOLOR {
DWORD lStructSize;
HWND hwndOwner;
HINSTANCE hInstance;
COLORREF *lpCustColors;
DWORD Flags;
LPARAM lCustData;
LPCCHOOKPROC lpfnHook;
LPCTSTR lpTemplateName;
} CHOOSECOLOR, *LPCHOOSECOLOR;
该结构体中COLORREF rgbResult 保存了用户选择的颜色。在VIEW类增加一个COLORREF 变量 m_clr(初始化为红色) 用来保存用户的选择。
就算我们选择了其他颜色进行绘画,但是当我们再次打开时,它的默认颜色还是显示黑色。我们想让它显示用户上次选择的颜色。如果要设置让该对话框满足我们的要求,那么就需要设置该对话框的CC_RGBINIT标志,这个标志可以在创建对话框对象时通过其构造函数的第二个参数来设置,也可以在该对话框对象创建之后,设置其m_cc成员变量中的Flags成员。我们采用后一种方法。在这里设置Flags参数 不可以用直接赋值的方法,因为这样会使得该对话框原来的那些默认的标记都去掉了,我们应该用组合的方式,将新的标准加到已有标准中去,代码如下:
view plain
void CGraphicView::OnColor()
{
CColorDialog dlg;
dlg.m_cc.Flags |= CC_RGBINIT | CC_FULLOPEN;
dlg.m_cc.rgbResult=m_clr;
if(IDOK==dlg.DoModal())
{
m_clr=dlg.m_cc.rgbResult;
}
}
在VIEW类获得用户的这些设置之后都应该在创建CPEN类时,添加到对应的参数中。上面的代码中 还有一个CC_FULLOPEN标记,这个也是一个常用的标记,使得颜色对话框创建的时候会完全展开。
下面创建一个字体对话框,就和MFC中选择字体的那个一样,和颜色对话框一样,字体对话框也是有一个MFC类与之对应,CFontDialog类。下面我们实现当用户选择了某种字体后,将它的名字用该字体显示出来。
在CFontDialog中有个CHOOSEFONT结构体的成员变量 m_cf
view plain
typedef struct {
DWORD lStructSize;
HWND hwndOwner;
HDC hDC;
LPLOGFONT lpLogFont;
INT iPointSize;
DWORD Flags;
COLORREF rgbColors;
LPARAM lCustData;
LPCFHOOKPROC lpfnHook;
LPCTSTR lpTemplateName;
HINSTANCE hInstance;
LPTSTR lpszStyle;
WORD nFontType;
WORD ___MISSING_ALIGNMENT__;
INT nSizeMin;
INT nSizeMax;
} CHOOSEFONT, *LPCHOOSEFONT;
其中有个 lpLogFont成员变量是指向逻辑字体(LOGFONT)的指针。
view plain
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;
TCHAR lfFaceName[LF_FACESIZE];
} LOGFONT, *PLOGFONT;
在这个结构体中,lfFaceName 就是该字体的名字。
字体对象的创建,首先是利用CFont类构造一个字体对象,然后利用CFont类的CteateFontIndirect函数根据指定特征的逻辑字体来初始化这个字体对象。
view plain
BOOL CreateFontIndirect(const LOGFONT* lpLogFont );
为了保存用户选择的字体,为VIEW类增加一个CString m_strFontName变量来保存,初始化为空。当然要创建字体,所以要为VIEW类增加一个 CFont 类的成员 m_font
view plain
void CGraphicView::OnFont()
{
CFontDialog dlg;
if(IDOK==dlg.DoModal())
{
//if(m_font.m_hObject)
//m_font.DeleteObject();
m_font.CreateFontIndirect(dlg.m_cf.lpLogFont);
m_strFontName=dlg.m_cf.lpLogFont->lfFaceName;
Invalidate();
}
}
在上面代码的最后 调用了Invalidate()函数 使窗口无效,重绘。所以我们就可以在ONPAINT函数中输出该字体了。
view plain
void CGraphicView::OnDraw(CDC* pDC)
{
CGraphicDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
CFont *pOldFont=pDC->SelectObject(&m_font);
pDC->TextOut(0,0,m_strFontName);
pDC->SelectObject(pOldFont);
}
按照上面的代码运行后,选择字体,显示 OK。再选择一次,按下OK 就会出现非法操作的错误的提示了。会出现这样的原因主要是因为第一次选择字体之后,m_font对象已经和某种字体资源关联了。当再一次选择字体之后,m_font对象试图与新的字体资源关联这时就出错了。要让对象与新的资源关联之前,应该先切断当前关联的资源。要释放先前的资源,可以用CGdiObject类(字体、画笔、画刷等都是派生于它)的DeleteObject函数来实现。该对象通过通过释放所有为Windows GDI对象所分配的资源,从而删除与CGdiObject对象相关联的Windows GDI对象,同时与CGdiObject对象相关的存储空间并不会受到影响。
想判断m_font对象是否已经关联,最简单的方法就是用CGdiObject对象的数据成员m_hObject来判断,该变量保存了与CGdiObject对象相关联的GDI资源句柄。代码就上面写的那个,把注释去掉就OK了。
下面再创建一个示例对话框,用来显示线型和线宽所生产的示例,首先是创建一个组框,ID改为 ID_SAMPLE 。对于编辑框控件来说,当用户在其上面进行对文本进行改变时,它会向其父窗口,即对话框发送一个EN_CHANGE通知信息。同样的,当用户单击单选按钮的时候,该按钮会向对话框发送一个BN_CLICKED信息。为了获取这两个通知信息,以反应出用户所做的选择,先生成这几个控件的响应函数,直接在相应空间上双击就可以了。我们在这四个响应函数中都添加一个让窗口失效的函数,然后在ONPAINT函数中去绘制线条。
view plain
void CSettingDlg::OnChangeLineWidth()
{
// TODO: If this is a RICHEDIT control, the control will not
// send this notification unless you override the CDialog::OnInitDialog()
// function and call CRichEditCtrl().SetEventMask()
// with the ENM_CHANGE flag ORed into the mask.
// TODO: Add your control notification handler code here
Invalidate();
}
void CSettingDlg::OnRadio1()
{
Invalidate();
}
void CSettingDlg::OnRadio2()
{
Invalidate();
}
void CSettingDlg::OnRadio3()
{
Invalidate();
}
void CSettingDlg::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: Add your message handler code here
UpdateData();
CPen pen(m_nLineStyle,m_nLineWidth,m_clr);
dc.SelectObject(&pen);
CRect rect;
GetDlgItem(IDC_SAMPLE)->GetWindowRect(&rect);
ScreenToClient(&rect);
dc.MoveTo(rect.left+20,rect.top+rect.Height()/2);
dc.LineTo(rect.right-20,rect.top+rect.Height()/2);
// Do not call CDialog::OnPaint() for painting messages
}
在上面代码中,要记得添加 UpdateData函数,不然用户做出的改变,函数并不会更新。然后呢在获取组框的坐标时,用了 GetWinDowRect函数,如果直接用它获取到的坐标然后去相应地画线,那么在程序运行的时候是看不到线条的,因为该函数返回的坐标的相对于屏幕左上方为源点的坐标,而我们要获取的是相对于客户区左上角为远点的坐标,两个坐标相差较多,所以会看不到线,这时候,可以调用ScreenToClient函数来转换该坐标,然后就可以进行绘画了。
接下来介绍一下怎么改变对话框和控件的背景以及文本颜色,是通过响应WM_CTLCOLOR消息来实现的。
view plain
afx_msg HBRUSH OnCtlColor( CDC* pDC, CWnd* pWnd, UINT nCtlColor );
详情在MSDN中查询。该函数返回将被用来绘制控件背景的画刷的句柄。当一个子控件将要被绘制的时候,它都会向它的父窗口发送一个WM_CTLCOLOR消息来准备一个DC(即pDC参数),以便于使用正确的颜色来绘制控件。
首先为对话框添加一个画刷对象,并在构造函数中初始化它
view plain
m_brush.CreateSolidBrush(RGB(0,0,255));
要修改某个指定控件的背景和文本颜色的话,可以先用GetDlgCtrlID()这个函数来获取ID,然后再进行相应的操作。例如要是编辑框和组框的背景为蓝色,字体为红色,那么代码如下:
view plain
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: Change any attributes of the DC here
// TODO: Return a different brush if the default is not desired
if(pWnd->GetDlgCtrlID()==IDC_LINE_STYLE)
{
pDC->SetTextColor(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,255));
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;
}
发现上面对OK按钮的操作是没有作用的。实际上,要想改变按钮控件的背景色和文本颜色,需要使用CButton类的一个成员函数 DrawItem
view plain
virtual void DrawItem( LPDRAWITEMSTRUCT lpDrawItemStruct );
该函数是一个虚函数,当一个自绘按钮(具有BS_OWNWEDRAE风格的按钮)在绘制的时候,框架就会自动调用这个虚函数。因此想要实现自绘按钮就要重载DraeItem函数。
该函数的参数是以一个 DRAWITEMSTRUCT 的指针,该结构体可以去MSDN中查,在该结构体中有一个hDC的成员,为了绘制按钮,可以在DC中加入自定义的颜色、画刷等对象,但是在该函数结束前,一定有要记得恢复hDC中原有对象。
下面我们添加一个新类 CTestBtn 派生于 CButton类 ,然后在该函数中重载 DrawItem函数,然后将MSDN上的例子复制进去:
view plain
void CTestBtn::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
// TODO: Add your code to draw the specified item
UINT uStyle = DFCS_BUTTONPUSH;
// This code only works with buttons.
ASSERT(lpDrawItemStruct->CtlType == ODT_BUTTON);
// If drawing selected, add the pushed style to DrawFrameControl.
if (lpDrawItemStruct->itemState & ODS_SELECTED)
uStyle |= DFCS_PUSHED;
// Draw the button frame.
::DrawFrameControl(lpDrawItemStruct->hDC, &lpDrawItemStruct->rcItem,
DFC_BUTTON, uStyle);
// Get the button's text.
CString strText;
GetWindowText(strText);
// Draw the button text using the text color red.
COLORREF crOldColor = ::SetTextColor(lpDrawItemStruct->hDC, RGB(255,0,0));
::DrawText(lpDrawItemStruct->hDC, strText, strText.GetLength(),
&lpDrawItemStruct->rcItem, DT_SINGLELINE|DT_VCENTER|DT_CENTER);
::SetTextColor(lpDrawItemStruct->hDC, crOldColor);
}
写完这个函数之后,我们现在用OK按钮来试试,首先要为该按钮关联一个 CtestBtn 类的变量,然后在OK按钮的属性中将 owner Draw 选上,再运行,就会发现该按钮变红色了。下面的这个DrawItem函数是孙鑫老师写的,显示效果就是图中的显示按钮:
view plain
void CSXBtn::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
UINT uStyle = BS_DEFPUSHBUTTON ;//DFCS_BUTTONPUSH;
// This code only works with buttons.
ASSERT(lpDrawItemStruct->CtlType == ODT_BUTTON);
// If drawing selected, add the pushed style to DrawFrameControl.
if (lpDrawItemStruct->itemState & ODS_SELECTED)
uStyle |= DFCS_PUSHED;
// Draw the button frame.
::DrawFrameControl(lpDrawItemStruct->hDC, &lpDrawItemStruct->rcItem,
DFC_BUTTON, uStyle);
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
// Get the button's text.
CString strText;
GetWindowText(strText);
// Draw the button text using the text color red.
CBrush B;
CRect rect;
CRect focusRect;
focusRect.CopyRect(&lpDrawItemStruct->rcItem);
DrawFocusRect(lpDrawItemStruct->hDC, (LPRECT)&focusRect);
focusRect.left += 4;
focusRect.right -= 4;
focusRect.top += 4;
focusRect.bottom -= 4;
rect.CopyRect(&lpDrawItemStruct->rcItem);
pDC->Draw3dRect(rect, ::GetSysColor(COLOR_BTNSHADOW), ::GetSysColor(COLOR_BTNHILIGHT));
B.CreateSolidBrush(RGB(0,255,0));
::FillRect(lpDrawItemStruct->hDC,&rect, (HBRUSH)B.m_hObject);
::SetBkMode(lpDrawItemStruct->hDC,TRANSPARENT);
COLORREF crOldColor = ::SetTextColor(lpDrawItemStruct->hDC, RGB(255,0,0));
::DrawText(lpDrawItemStruct->hDC, strText, strText.GetLength(),
&lpDrawItemStruct->rcItem, DT_SINGLELINE|DT_VCENTER|DT_CENTER);
::SetTextColor(lpDrawItemStruct->hDC, crOldColor);
}
自己写一个好看的按钮的 DrawItem函数还是很有难度的,现在很多人都是用强大的CButtonST类,在网上可以找到,不过该类的方法有很多,还没去研究。
最后是位图的显示。要显示一副位图,主要的过程是:先创建位图,然后创建兼容DC,将位图放入兼容DC中,最后将兼容DC中的位图贴到当前DC中。
兼容DC的创建是使用 CreateCompatible 函数。
view plain
virtual BOOL CreateCompatibleDC( CDC* pDC );
该函数参数就是当前DC的指针,创建的兼容DC就是要与之兼容。兼容DC实际上是一个内存块,表示一个显示的表面。如果想把图像复制到实际的DC中,可以先用兼容DC在内存中准备这些图像,然后再将其复制到实际DC中。当兼容DC被创建的时候,它的显示表明标准是一个像素的大小。在应用程序可以使用内存设备上下文进行绘图操作之前,必须将一个具有正确高度和宽度的位图选入设备上下文,这时,内存设备上下文的显示表面大小就由当前选入的位图决定了。
窗口绘制的过程是分两个步骤的:首先擦除窗口背景,然后在进行窗口重绘。
当窗口背景被擦除的时候会发送一个 WM_ERASEBKGND 消息,因此可以在该消息的响应函数中进行位图的显示:
view plain
BOOL CGraphicView::OnEraseBkgnd(CDC* pDC)
{
// TODO: Add your message handler code here and/or call default
CBitmap bitmap;
bitmap.LoadBitmap(IDB_BITMAP2);
BITMAP bmp;
bitmap.GetBitmap(&bmp);
CDC dcCompatible;
dcCompatible.CreateCompatibleDC(pDC);
dcCompatible.SelectObject(&bitmap);
CRect rect;
GetClientRect(&rect);
// 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;
//return CView::OnEraseBkgnd(pDC);
}
在该函数的最后 不可以返回调用基类的函数,不然位图只会显示完马上就被擦除,应该然后TRUE。
然后呢,在显示位图的时候,如果想1:1显示的话,就用BitBlt函数,如果想位图可以根据客户区大小进行拉伸或者压缩,使得可以显示整张位图的话,就要用StretchBlt函数。
view plain
BOOL BitBlt ( int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc,
int ySrc, DWORD dwRop );
view plain
BOOL StretchBlt( int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, int nSrcWidth, int nSrcHeight, DWORD dwRop );
第二个函数就比第一个多了两个参数,就是原来那副位图的宽度和高度。
作者“Kay's space”