• 1. 试验三 面向对象程序设计实验——一个简单的可复用时钟控件
  • 2. 本实验的开发环境使用C++语言实现 使用VC++6.0集成开发环境开发 使用MFC应用程序开发框架
  • 3. MFC版本简介MFC全称Microsoft Foundation Classes. 1989年微软公司成立Application Framework技术团队,开发C++面向对象工具给Windows应用程序开发人员使用。这个小组即AFX小组,就是他们,开始了MFC的开发历程。 微软公司于1992年4月推出C/C++7.0产品时,初次向世人介绍了MFC 1.0,其32位版本也在1992年7月随着Win32 SDK推出。 Visual C++1.0(也就是C/C++8.0)搭配MFC 2.0于1993年3月推出。同年8月推出在Windows NT上的Visual C++1.1 for Windows NT,搭配的是MFC 2.1。
  • 4. 微软在1993年12月又推出了16为的Visual C++ 1.5, 搭配的是MFC 2.5。 1994年9月,微软推出Visual C++ 2.0,搭配MFC 3.0。 1995年1月,微软推在增加了MAPI和WinSock支持的基础上,推出MFC 3.1。 1995年7月,MFC有了3.2版,这是一个小的改版。 然后就是1995年9月的32位版MFC 4.0。这个版本有了相当大的改进。 1996年上半年又有了MFC4.1…… 时至今日MFC在Visual C++ 7.0中已经到了7.0版本。 我们常用的Visual C++6.0使用的是MFC 4.2版本。 微软正不断地为“为什么要使用MFC”加上各式各样地强烈你有,并强烈导引它成为Windows程序设计的C++标准界面。正如我们所看到的,越来越多的MFC程序出现了并继续在产生。
  • 5. 试验设计目标完成一个可重用的C++类,可以通过该类实现一个简单的模拟时钟 编写一个简单的MFC程序测试验证这个时钟控件类 关于这个时钟控件,如果按照课件继续完善下去,请将他做得更好更强大。比如说:可以考虑为时钟控件的各项设置(如颜色)提供外部接口,供CClockEx的使用者调用,可以考虑丰富背景绘制,加入背景图片设置,在刻度上标上数字,为指针加上阴影效果,在时钟上显示日期等。有兴趣的同学有空不妨试试。
  • 6. 应用程序框架设计过程(仅供参考)打开VC++6.0,新建一个MFC工程,输入工程名,选择“MFC AppWizard”如图所示,然后点击OK。
  • 7. 为简单起见,这里选择“Dialog based”程序,然后可以直接选择finish了。
  • 8. 在类视图的工程名上右键单击,选择New Class(如下图所示)来建立一个新类。
  • 9. 我们可以从CStatic类(静态控件类)派生出类CClockEx来实现时钟控件类,这样,就可以继承MFC静态控件的很多已经具备的功能和特点。在建立新类的对话框中,按下图输入。
  • 10. 按Ctrl+W调出Class Wizard,在class name中选择刚才建立的新类CClockEx,然后 重载虚函数PreSubClassWindow,对时钟控件的基本初始化工作将在该函数中进行。 添加消息响应WM_PAINT,我们必须用自己完成时钟的绘制工作。 添加消息响应WM_SIZE,重载该消息响应以便我们的时钟控件能够自动适应其大小的变化。 为了时钟能够走动,我们得定时刷新时钟控件让它走动,还得添加WM_TIMER的消息响应。最终效果如下图所示。
  • 11. 到目前为止,时钟控件的轮廓和已经有了,下面考虑时钟绘制的具体实现。 一个时钟大致可由时钟背景、时针、分针、秒针四个部分构成。 以上的时钟四个部分可是看作四个不同的对象,可以考虑建立类CClockBackground(背景)CClockHourHand(时针)、CClockMinHand(分针)、CClockSecHand(秒针)来分别实现。(此处为了意义明确,类名都比较长) 这四个类有它们的共同点,譬如都有自己的绘图函数,如果要具体能够设置颜色,它们都应该有自己的绘图颜色,同样应该有颜色设置函数,此外都有绘图区域和区域设置函数等等。因此,可以为它们的共性建立一个基类,此处命名为CClockElement(时钟元素)。
  • 12. 下面逐步实现各个类,首先实现各个组成类的基类——CClockElement。同样,在ClassView的工程名上面点右键,选择New Class,在ClassType里面选择Genenric Class,类CClockElement不需要从其它类派生,所以Base Class可以不填,效果如下图所示。
  • 13. 下面对类CClockElement的源代码进行编写。 因为在后面的绘图中需要用到sin和cos这两个数学计算公式,因此需包含头文件以便使用数学函数库。为后面计算使用,还需定义数学常量PI(3.1415926535)。 时钟的每个组成部分都有绘图区域区域,因此,可在基类中定义绘图区域变量。同理,可以定义颜色变量,为了是颜色不显得单调,这里为每个控件设置两种颜色(如果想使程序更加绚丽,可以设置更多颜色)。 对应的,这些变量需增加接口函数来进行访问。 对于每个组成部分都应该有的Draw函数,当然也需在基类中定义,但是,该函数对于基类来说是无需函数实现的,因此可定义为纯虚函数。
  • 14. 综合刚才这几点,CClockElement的实现的头文件代码如下: #include #define PI 3.1415926535 class CClockElement { public: void SetColor(COLORREF crMain, COLORREF crOther); //设置颜色 void SetTime(const CTime &tmCur); //设置当前时间 void SetRegion(LPRECT lprcRect); //设置绘图区域 virtual void Draw(CDC *pDC) = 0; //绘图函数 CClockElement(); virtual ~CClockElement(); protected: COLORREF m_crMain; //主要颜色 COLORREF m_crOther; //辅助的其他颜色 CTime m_tmCur; //当前时刻 CRect m_rcRegion; //绘图区域 int m_nRadius; //时钟半径
  • 15. CClockElement的实现的源文件关键代码如下: CClockElement::CClockElement() { m_nRadius = 0; m_crMain = RGB(255, 255, 255); m_crOther = RGB(128, 128, 128); } CClockElement::~CClockElement() { } void CClockElement::SetRegion(LPRECT lprcRect) { m_rcRegion = lprcRect; m_nRadius = m_rcRegion.Width() / 2; if (m_rcRegion.Width() > m_rcRegion.Height()) { m_nRadius = m_rcRegion.Height() / 2; } }
  • 16. 续上页: void CClockElement::SetTime(const CTime &tmCur) { m_tmCur = tmCur; } void CClockElement::SetColor(COLORREF crMain, COLORREF crOther) { m_crMain = crMain; m_crOther = crOther; }
  • 17. 下面实现时钟背景类——CClockBackground。同样,在ClassView的工程名上面点右键,选择New Class,在ClassType里面选择Genenric Class,类CClockBackground需要从类CClockElement派生,所以Base Class需选择类CClockElement,效果如下图所示。
  • 18. CClockBackground只需实现基类的虚函数Draw即可,因此这里重载基类的虚函数Draw,其头文件实现代码如下: #include "ClockElement.h" class CClockBackground : public CClockElement { public: CClockBackground(); virtual ~CClockBackground(); };
  • 19. CClockBackground源文件代码如下: CClockBackground::CClockBackground() { //为时钟背景定义默认的颜色设置 m_crMain = RGB(0, 255, 0); m_crOther = RGB(0, 128, 0); } CClockBackground::~CClockBackground() { } void CClockBackground::Draw(CDC *pDC) { //准备设备环境 CPen penMain(PS_SOLID, 1, m_crMain), penOther(PS_SOLID, 1, m_crOther); CBrush brMain(m_crMain), brOther(m_crOther); CPen *pOldPen = pDC->SelectObject(&penOther); CBrush *pOldBrush = pDC->SelectObject(&brMain);
  • 20. CClockBackground源文件代码如下(续上页): //绘制60个小圆点,表示分针和秒针的刻度 CPoint ptCenter = m_rcRegion.CenterPoint(); int nRadius = m_nRadius - 8; for(int i=0; i<60; i++) { CPoint ptEnd = ptCenter; ptEnd.Offset((int)(nRadius * sin(2 * PI * (i % 60) / 60)), (int)(-nRadius * cos(2 * PI * (i % 60) / 60))); CRect rcDot(-2, -2, 2, 2); rcDot.OffsetRect(ptEnd); pDC->Ellipse(rcDot); } //绘制12个小方框,表示12个正点 pDC->SelectObject(&penMain); pOldBrush = pDC->SelectObject(&brOther); for(i=0; i<12; i++) { CPoint ptEnd = ptCenter; double fRadian = 2 * PI * (i % 12) / 12; ptEnd.Offset((int)(nRadius * sin(fRadian)), (int)(-nRadius * cos(fRadian))); CRect rcDot(-3, -3, 3, 3); rcDot.OffsetRect(ptEnd); pDC->Rectangle(rcDot); } //还原设备环境 pDC->SelectObject(pOldPen); pDC->SelectObject(pOldBrush); }
  • 21. 下面实现时针类CClockHourHand,该类新建的过程与CClockBackground一样, CClockHourHand同样只需实现基类的虚函数Draw即可,其头文件实现代码如下: #include "ClockElement.h" class CClockHourHand : public CClockElement { public: CClockHourHand(); virtual ~CClockHourHand(); virtual void Draw(CDC *pDC); };
  • 22. CClockHourHand源文件代码如下: CClockHourHand::CClockHourHand() { //定义默认颜色 m_crMain = RGB(0, 255, 100); m_crOther = RGB(128, 128, 0); } CClockHourHand::~CClockHourHand() { } void CClockHourHand::Draw(CDC *pDC) { //初始化设备环境 CPen penMain(PS_SOLID, 1, m_crMain), penOther(PS_SOLID, 1, m_crOther); CBrush brMain(m_crMain), brOther(m_crOther); CPen *pOldPen = pDC->SelectObject(&penOther); CBrush *pOldBrush = pDC->SelectObject(&brMain); //确定当前指针的弧度 int nTime = (m_tmCur.GetHour() % 12) * 3600; nTime += m_tmCur.GetMinute() * 60; nTime += m_tmCur.GetSecond(); double fRadian = 2 * PI * nTime / 3600 / 12;
  • 23. CClockHourHand源文件代码如下(续上页): //确定绘制菱形指针所需的四个角的坐标 CPoint ptDiamond[4]; for(int i=0; i<4; i++) { ptDiamond[i] = m_rcRegion.CenterPoint(); } int nRadus = m_nRadius / 2; ptDiamond[0].Offset((int)(nRadus * sin(fRadian)), (int)(- nRadus * cos(fRadian))); fRadian += 0.5 * PI; nRadus = m_nRadius / 20; ptDiamond[1].Offset((int)(nRadus * sin(fRadian)), (int)(- nRadus * cos(fRadian))); fRadian += 0.5 * PI; nRadus = m_nRadius / 10; ptDiamond[2].Offset((int)(nRadus * sin(fRadian)), (int)(- nRadus * cos(fRadian))); fRadian += 0.5 * PI; nRadus = m_nRadius / 20; ptDiamond[3].Offset((int)(nRadus * sin(fRadian)), (int)(- nRadus * cos(fRadian)));
  • 24. CClockHourHand源文件代码如下(续上页): //绘制菱形时针 pDC->Polygon(ptDiamond, 4); //恢复设备环境 pDC->SelectObject(pOldPen); pDC->SelectObject(pOldBrush);
  • 25. 下面实现分针类CClockMinHand,该类新建的过程与CClockHourHand一样,类的实现也基本相似,其头文件实现代码如下: #include "ClockElement.h" class CClockMinHand : public CClockElement { public: CClockMinHand(); virtual ~CClockMinHand(); virtual void Draw(CDC *pDC); };
  • 26. CClockMinHand源文件代码如下: CClockMinHand::CClockMinHand() { //定义默认颜色 m_crMain = RGB(0, 255, 100); m_crOther = RGB(128, 128, 0); } CClockMinHand::~CClockMinHand() { } void CClockMinHand::Draw(CDC *pDC) { //初始化设备环境 CPen penMain(PS_SOLID, 1, m_crMain), penOther(PS_SOLID, 1, m_crOther); CBrush brMain(m_crMain), brOther(m_crOther); CPen *pOldPen = pDC->SelectObject(&penOther); CBrush *pOldBrush = pDC->SelectObject(&brMain); //确定分针所在位置的弧度 int nTime = m_tmCur.GetMinute() * 60; nTime += m_tmCur.GetSecond(); double fRadian = 2 * PI * nTime / 3600;
  • 27. CClockMinHand源文件代码如下(续上页): //确定绘制菱形指针所需的四个角的坐标 CPoint ptDiamond[4]; for(int i=0; i<4; i++) { ptDiamond[i] = m_rcRegion.CenterPoint(); } int nRadus = m_nRadius / 2; ptDiamond[0].Offset((int)(nRadus * sin(fRadian)), (int)(- nRadus * cos(fRadian))); fRadian += 0.5 * PI; nRadus = m_nRadius / 20; ptDiamond[1].Offset((int)(nRadus * sin(fRadian)), (int)(- nRadus * cos(fRadian))); fRadian += 0.5 * PI; nRadus = m_nRadius / 10; ptDiamond[2].Offset((int)(nRadus * sin(fRadian)), (int)(- nRadus * cos(fRadian))); fRadian += 0.5 * PI; nRadus = m_nRadius / 20; ptDiamond[3].Offset((int)(nRadus * sin(fRadian)), (int)(- nRadus * cos(fRadian)));
  • 28. CClockMinHand源文件代码如下(续上页): //绘制菱形分针 pDC->Polygon(ptDiamond, 4); //恢复设备环境 pDC->SelectObject(pOldPen); pDC->SelectObject(pOldBrush);
  • 29. 下面实现秒针类CClockSecHand,该类新建的过程与CClockHourHand一样,类的实现也基本相似,其头文件实现代码如下: #include "ClockElement.h" class CClockSecHand : public CClockElement { public: CClockSecHand(); virtual ~CClockSecHand(); virtual void Draw(CDC *pDC); };
  • 30. CClockSecHand源文件代码如下: CClockSecHand::CClockSecHand() { //设定秒针的默认颜色 m_crMain = RGB(0, 200, 200); m_crOther = RGB(0, 200, 200); } CClockSecHand::~CClockSecHand() { } void CClockSecHand::Draw(CDC *pDC) { int nTime = m_tmCur.GetSecond(); CPoint ptStart = m_rcRegion.CenterPoint(); CPoint ptEnd = ptStart; int nRadius = m_nRadius - 10; ptEnd.Offset((int)(nRadius * sin(2 * PI * nTime / 60)), (int)(-nRadius * cos(2 * PI * nTime / 60))); CPen penMain(PS_SOLID, 1, m_crMain); CPen *pOldPen = pDC->SelectObject(&penMain); pDC->MoveTo(ptStart); pDC->LineTo(ptEnd); pDC->SelectObject(pOldPen); }
  • 31. 到目前为止,时钟的各个部件都已经设计完毕,现在该把它们组装起来了。 刚才建立的四个类CClockBackground、CClockHourHand、CClockMinHand、CClockSecHand要为CClockEx服务,正如CClockElement为它们服务一样,不过服务的方式发生了变化,刚才是以继承的方式使用,现在需要将上面四个类实例化供CClockEx使用。因此,首先应该在类CClockEx为上四个类分别建立对象。在CClockEx的头文件中恰当位置加入以下代码: #include "ClockBackground.h" #include "ClockHourHand.h" #include "ClockMinHand.h" #include "ClockSecHand.h" class CClockEx : public CStatic { private: CClockBackground m_clockBK; //时钟背景 CClockHourHand m_clockHour; //时指针 CClockMinHand m_clockMin; //分指针 CClockSecHand m_clockSec; //秒指针
  • 32. 为了存储绘图区域的大小,需要在CClockEx中加入一个变量m_rcClient: CRect m_rcClient; //客户区域 在PreSubclassWindow函数中进行时钟的初始化工作。首先得为各个部件设定区域大小,然后启动定时刷新的定时器(100毫秒的定时器已经足够)。由于每个部件都有自己的默认颜色配置,因此这里暂且先使用默认配置,当然也可以自定义颜色: GetClientRect(m_rcClient); //获取当前客户区域 m_clockBK.SetRegion(m_rcClient); m_clockHour.SetRegion(m_rcClient); m_clockMin.SetRegion(m_rcClient); m_clockSec.SetRegion(m_rcClient); SetTimer(1, 100, NULL); //每100毫秒刷新一次
  • 33. 当控件大小发生变化时,控件状态要能够自动更新: void CClockEx::OnSize(UINT nType, int cx, int cy) { CStatic::OnSize(nType, cx, cy); GetClientRect(m_rcClient); m_clockBK.SetRegion(m_rcClient); m_clockHour.SetRegion(m_rcClient); m_clockMin.SetRegion(m_rcClient); m_clockSec.SetRegion(m_rcClient); } 每个定时器时间触发时,需要刷新控件以保证时钟的走动: void CClockEx::OnTimer(UINT nIDEvent) { Invalidate(FALSE); CStatic::OnTimer(nIDEvent); }
  • 34. 下面实现时钟的绘制,由于控件在不断刷新,按一般方式绘图,屏幕会不断闪烁,因此此处使用双缓冲绘图——就是在内存中绘图,绘制完毕时在Copy到屏幕上,这样可以有效的防止屏幕闪烁: void CClockEx::OnPaint() { CPaintDC dc(this); // device context for painting //实现双缓冲绘图——防止屏幕闪烁 CDC dcMem; dcMem.CreateCompatibleDC(&dc); CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, m_rcClient.Width(), m_rcClient.Height()); dcMem.SelectObject(&bmp); DrawClock(&dcMem); //绘制时钟 dc.BitBlt(0, 0, m_rcClient.Width(), m_rcClient.Height(), &dcMem, 0, 0, SRCCOPY); }
  • 35. 为了程序结构更清晰,我们将绘图函数放在一个单独的函数DrawClock中,下面室DrawClock的函数实现: void CClockEx::DrawClock(CDC *pDC) { CTime tmCur = CTime::GetCurrentTime(); m_clockBK.SetTime(tmCur); m_clockHour.SetTime(tmCur); m_clockMin.SetTime(tmCur); m_clockSec.SetTime(tmCur); m_clockBK.Draw(pDC); m_clockMin.Draw(pDC); m_clockHour.Draw(pDC); m_clockSec.Draw(pDC); }
  • 36. 下面是完整的CClockEx源码: #if !defined(AFX_CLOCKEX_H__EB35C434_A727_43BE_B10E_121F6307EA73__INCLUDED_) #define AFX_CLOCKEX_H__EB35C434_A727_43BE_B10E_121F6307EA73__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // ClockEx.h : header file // ///////////////////////////////////////////////////////////////////////////// // CClockEx window #include "ClockBackground.h" #include "ClockHourHand.h" #include "ClockMinHand.h" #include "ClockSecHand.h" class CClockEx : public CStatic { private: CRect m_rcClient; //客户区域 CClockBackground m_clockBK; //时钟背景 CClockHourHand m_clockHour; //时指针 CClockMinHand m_clockMin; //分指针 CClockSecHand m_clockSec; //秒指针 void DrawClock(CDC *pDC); // Construction public: CClockEx();
  • 37. 下面是完整的CClockEx源码(续上页): // Attributes public: // Operations public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CClockEx) protected: virtual void PreSubclassWindow(); //}}AFX_VIRTUAL // Implementation public: virtual ~CClockEx(); // Generated message map functions protected: //{{AFX_MSG(CClockEx) afx_msg void OnPaint(); afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg void OnTimer(UINT nIDEvent); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; ///////////////////////////////////////////////////////////////////////////// //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before the previous line. #endif // !defined(AFX_CLOCKEX_H__EB35C434_A727_43BE_B10E_121F6307EA73__INCLUDED_)
  • 38. 下面是完整的CClockEx源码(续上页): // ClockEx.cpp : implementation file #include "stdafx.h" #include "ClockProject.h" #include "ClockEx.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ////////////////////////////////////////////////////////////////////// // CClockEx CClockEx::CClockEx() { } CClockEx::~CClockEx() { } BEGIN_MESSAGE_MAP(CClockEx, CStatic) //{{AFX_MSG_MAP(CClockEx) ON_WM_SIZE() ON_WM_PAINT() ON_WM_TIMER() //}}AFX_MSG_MAP END_MESSAGE_MAP()
  • 39. 下面是完整的CClockEx源码(续上页): //////////////////////////////////////////////////////////////////////// // CClockEx message handlers void CClockEx::PreSubclassWindow() { //获取当前客户区域 GetClientRect(m_rcClient); m_clockBK.SetRegion(m_rcClient); m_clockHour.SetRegion(m_rcClient); m_clockMin.SetRegion(m_rcClient); m_clockSec.SetRegion(m_rcClient); SetTimer(1, 100, NULL); CStatic::PreSubclassWindow(); } void CClockEx::OnSize(UINT nType, int cx, int cy) { CStatic::OnSize(nType, cx, cy); GetClientRect(m_rcClient); m_clockBK.SetRegion(m_rcClient); m_clockHour.SetRegion(m_rcClient); m_clockMin.SetRegion(m_rcClient); m_clockSec.SetRegion(m_rcClient); }
  • 40. 下面是完整的CClockEx源码(续上页): void CClockEx::OnPaint() { CPaintDC dc(this); // device context for painting //实现双缓冲绘图——防止屏幕闪烁 CDC dcMem; dcMem.CreateCompatibleDC(&dc); CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, m_rcClient.Width(), m_rcClient.Height()); dcMem.SelectObject(&bmp); DrawClock(&dcMem); dc.BitBlt(0, 0, m_rcClient.Width(), m_rcClient.Height(), &dcMem, 0, 0, SRCCOPY); } void CClockEx::OnTimer(UINT nIDEvent) { Invalidate(FALSE); CStatic::OnTimer(nIDEvent); }
  • 41. 下面是完整的CClockEx源码(续上页): void CClockEx::DrawClock(CDC *pDC) { CTime tmCur = CTime::GetCurrentTime(); m_clockBK.SetTime(tmCur); m_clockHour.SetTime(tmCur); m_clockMin.SetTime(tmCur); m_clockSec.SetTime(tmCur); m_clockBK.Draw(pDC); m_clockMin.Draw(pDC); m_clockHour.Draw(pDC); m_clockSec.Draw(pDC); }
  • 42. 到目前为止,整个控件已经编写完毕,下面,我们来测试这个控件看看时钟的实现效果。 打开资源编辑器,在对话框中拖入一个Static控件,调整外观至合适,将其ID改为IDC_CLOCK。如下图效果所示:
  • 43. 打开Class Wizard,为IDC_CLOCK映射一个变量,变量类型为CClockEx。如下图效果所示:
  • 44. 然后,不要忘记在对话框类的头文件中包含头文件“ClockEx.h”。 这样,程序代码基本编写完毕。编译整个工程,执行可执行程序,就可以看到整个时钟控件的效果图了。
  • 45. 结束语本实例采用面向对象的程序设计方法实现了一个简单的时钟控件。这里主要在介绍一种方法,一种基本的设计方法。介绍过程中进行的操作或者用到的代码,如果有不清楚的,可以参阅更详细的介绍MFC程序设计的资料。
  • 46. 完!谢谢观看!