时钟的实现(MFC)

文章目录

    • 1.预备知识
      • 1.日期和时间类
        • 1.概述
        • 2.构造
        • 3.`CTime`类主要成员函数
        • 3.`CTimeSpan`类主要成员函数
      • 2.计时器
        • 1.创建计时器
        • 2.销毁计时器
      • 3.位图类
        • 1.构造
        • 2.初始化
        • 3.属性
        • 4.操作
    • 2.实验目的
    • 3.实验内容
    • 4.代码实现
      • 1.准备工作
      • 2.基类`CClockBase`
        • ClockBase.h
        • ClockBase.cpp
      • 3.时钟背景类`CClockBackground`
      • 4.时钟指针类`CClockHourHand`、`CClockMinuteHand`、`CClockSecondHand`
      • 5.封装为控件`CClockWidget`
      • 6.实例化控件
    • 5.运行结果
    • 6.总结
      • 1.实验中遇到的困难
        • 如何实现时钟指针
      • 2.项目改进
      • 3.心得体会

1.预备知识

1.日期和时间类

1.概述

在MFC中,用CTime类表示绝对的时间和日期,用CTimeSpan类表示时间间隔。它们都没有基类,是不可派生的。因为没有虚函数,CTimeCTimeSpan对象的大小都正好是4个字节。其大多数成员函数都是内联的。

CTimeCTimeSpan类引入了ANSI time_t数据类型以及其相关的运行时函数,其中包括向或自一个Gregorian日期和24小时时间的转换功能。这些函数将秒转换为日、时、分和秒的各种组合。

CTime值是基于世界标准时间(UCT)的,UCT时间等于格林威治(Greenwich)时间(GMT)。其表示的日期上限是3000年12月31日,下限是1970年1月1日12:00:00 AM GMT。本地时区则是由TZ环境变量控制的。一个CTimeSpan对象以秒为单位保存时间。由于CTimeSpan对象以带符号的四字节数存储,所以最大的时间跨度近似为$\pm$68年。

当创建一个CTime时,将nDST参数设置为0表示有效的是标准时间,或将其设置为大于0表示有效的白天保留时间,将其设置为小于零的值表示由C运行时库代码来计算有效的是标准时间还是白天保留时间。如果没有设置这个参数,则它的值是不明确的,而从mktime返回的值是不可预知的。如果timeptr指向一个由先前调用asctimegmtime,或localtime返回的tm结构,则tm_isdst域包含了适当的值。

2.构造
CTime( );
CTime( const CTime& timeSrc );
CTime( time_t time );
CTime( int nYear, int nMonth, int nDay, int nHour, int nMin, int nSec, int nDST = -1 );
CTime( WORD wDosDate, WORD wDosTime, int nDST = -1 );
CTime( const SYSTEMTIME& sysTime, int nDST = -1 );
CTime( const FILETIME& fileTime, int nDST = -1 );
参数说明
参数 含义
timeSrc 表示一个已经存在的CTime对象。
time 表示一个时间值。
nYear,nMonth,nDay,nHour,nMin,nSec 表示要拷贝到新的CTime对象中去的日期和时间值。
nDST 表明有效的是否是day light saving time。可以是下列三个值中的某一个:nDST被设置为0表示起作用的是标准时间。nDST被设置为一个大于0的值表示起作用的是day light saving time。nDST被设置为一个小于0的值表示要自动计算起作用的是标准时间还是day light saving time。
wDosDate,wDosTime 要被转换为一个日期/时间值并被拷贝到新的CTime对象中去的MSDOS日期和时间。
sysTime 要被转换为一个日期/时间值并被拷贝到新的CTime对象中去的SYSTEMTIME结构。
fileTime 要被转换为一个日期/时间值并被拷贝到新的CTime对象中去的FILETIME结构。
CTimeSpan( );
CTimeSpan( const CTimeSpan& timeSpanSrc );
CTimeSpan( time_t time );
CTimeSpan( LONG lDays, int nHours, int nMins, int nSecs );
参数说明
参数 含义
timeSpanSrc 一个已存在的CTimeSpan对象。
time 一个time_t时间值。
lDays,nHours,nMins,nSecs 分别代表日、时、分、秒
3.CTime类主要成员函数
static CTime PASCAL GetCurrentTime( );

说明:此成员函数返回一个代表当前时间的CTime对象。

time_t GetTime( ) const;

说明:此成员函数返回一个给定CTime对象的time_t值。

int GetYear( ) const;

说明:此成员函数根据本地时间返回范围在1970年1月1日至2038年1月18日之间的年。这个函数调用GetLocalTm,该函数使用了一个内部的、静态分配的缓冲区。调用其它的CTime成员函数会导致这个缓冲区中的数据被覆盖。

int GetMonth( ) const;

说明:此成员函数根据本地时间返回范围在1至12之间的月(1=一月)。这个函数调用GetLocalTm,该函数使用了一个内部的、静态分配的缓冲区。调用其它的CTime成员函数会导致这个缓冲区中的数据被覆盖。

int GetDay( ) const;

说明:此成员函数根据本地时间返回范围在1-31之间的该月的天。这个函数调用GetLocalTm,该函数使用了一个内部的、静态分配的缓冲区。调用其它的CTime成员函数会导致这个缓冲区中的数据被覆盖。

int GetHour( ) const;

说明:此成员函数根据本地时间返回范围在0至23之间的小时。这个函数调用GetLocalTm,该函数使用了一个内部的、静态分配的缓冲区。调用其它的CTime成员函数会导致这个缓冲区中的数据被覆盖。

int GetMinute( ) const;

说明:此成员函数根据本地时间返回范围在0至59之间的分钟。这个函数调用GetLocalTm,该函数使用了一个内部的、静态分配的缓冲区。调用其它的CTime成员函数会导致这个缓冲区中的数据被覆盖。

int GetSecond( ) const;

说明:此成员函数根据本地时间返回范围在0至59之间的秒。这个函数调用GetLocalTm,该函数使用了一个内部的、静态分配的缓冲区。调用其它的CTime成员函数会导致这个缓冲区中的数据被覆盖。

int GetDayOfWeek( ) const;

说明:此成员函数根据本地时间返回该星期的天;1就是星期日,2就是星期一,…,7就是星期六。这个函数调用GetLocalTm,该函数使用了一个内部的,静态分配的缓冲区。调用其它的CTime成员函数会导致这个缓冲区中的数据被覆盖。

CString Format( LPCTSTR pFormat ) const;
CString Format( UINT nFormatID ) const;

返回值:返回一个包含了格式化时间的CString

参数:

  • pFormat 一个类似于printf格式化字符串的格式化字符串。前面有一个百分号(%)标记的格式化代码,被相应的CTime成分替换。格式化字符串中的其它字符被不作变动地拷贝到返回字符串中。参见运行时函数strftime可以获得详细的信息。Format的格式化代码的值和意义如下所示:
    • %a:周的英文缩写形式。
    • %A:周的英文全名形式。
    • %b:月的英文缩写形式。
    • %B:月的英文全名形式。
    • %c:完整的日期和时间。
    • %d:十进制形式的日期(01-31)。
    • %H:24小时制的小时(00-23)。
    • %I:12小时制的小时(00-11)。
    • %j:十进制表示的一年中的第几天(001-366)。
    • %m:月的十进制表示(01-12)。
    • %M:十进制表示的分钟(00-59)。
    • %p:12小时制的上下午标示(AM/PM)。
    • %S:十进制表示的秒(00-59)。
    • %U:一年中的第几个星期(00-51),星期日是一周的第一天。
    • %W:一年中的第几个星期(00-51),星期一是一周的第一天。
    • %w:十进制表示的星期几(0-6)。
    • %Y:十进制表示的年。
  • nFormatID 用来表示这个格式的字符串的ID。

说明:此成员函数用来创建一个日期/时间值的格式化表达式。如果此CTime对象的状态是空,则返回值是一个空字符串。如果CTime对象的状态是无效,则返回值是一个空字符串。

CString FormatGmt( LPCTSTR pFormat ) const;
CString FormatGmt( UINT nFormatID ) const;

说明:此成员函数用来生成一个对应于这个CTime对象的格式化字符串。这个时间值没有被转换,因此是反映UTC的。

const CTime& operator =( const CTime& timeSrc );
const CTime& operator =( time_t t );

说明:这些重载的赋值操作符将源时间拷贝到此CTime对象中。保存在一个CTime对象中的内部时间与时区无关。在赋值操作中不用进行时区转换。

CTime operator +( CTimeSpan timeSpan ) const;
CTime operator - ( CTimeSpan timeSpan ) const;
CTimeSpan operator - ( CTime time ) const;

说明:CTime对象表示绝对时间。CTimeSpan对象表示相对时间。前两个操作符允许你向或从CTime对象中加上或减去一个CTimeSpan对象。第三个操作符允许你将两个CTime对象相减产生一个CTimeSpan对象。

const CTime& operator +=( CTimeSpan timeSpan );
const CTime& operator -=( CTimeSpan timeSpan );

说明:这些操作符允许你从此CTime对象中加上或减去一个CTimeSpan对象。

BOOL operator ==( CTime time ) const;
BOOL operator !=( CTime time ) const;
BOOL operator <( CTime time ) const;
BOOL operator >( CTime time ) const;
BOOL operator <=( CTime time ) const;
BOOL operator >=( CTime time ) const;

说明:这些操作符比较两个绝对时间,如果测试条件为真则返回非零值;否则返回0。

3.CTimeSpan类主要成员函数
LONG GetDays( ) const;

说明:此成员函数返回完整的天数。如果该时间段是负的,则这个值可能是负的。

int GetHours( ) const;

说明:此成员函数返回当前日的小时数。其范围是-23到+23。

int GetMinutes( ) const;

说明:此成员函数返回当前小时中的分钟数。范围是-59到59。

int GetSeconds( ) const;

说明:此成员函数返回当前分钟中的秒数。范围是-59到59。

LONG GetTotalHours( ) const;

说明:此成员函数返回此CTimeSpan中的完整小时数。

LONG GetTotalMinutes( ) const;

说明:此成员函数返回此CTimeSpan中的完整分钟数。

LONG GetTotalSeconds( ) const;

说明:此成员函数返回此CTimeSpan中的完整秒数。

CString Format( LPCSTR pFormat ) const;
CString Format( LPCTSTR pFormat ) const;
CString Format( UINT nID ) const;

返回值:返回一个包含格式化的时间的CString对象。

参数:

  • pFormat 一个类似于printf格式化字符串的格式化字符串。前面有一个百分号(%)标记的格式化代码,将被相应的CTimeSpan成分替换。格式化字符串中的其它字符被不作变动地拷贝到返回字符串中。
  • Format 格式化代码的值和意义同CTime
  • nID 用来表示这个格式的字符串的ID。

说明:生成一个对应于此CTimeSpan的格式化字符串。库的调试版检查格式化代码,如果代码不在上面的列表中,则将给出断言。

const CTimeSpan& operator =( const CTimeSpan& timeSpanSrc );

说明:这些重载的赋值操作符将源CTimeSpan timeSpanSrc对象拷贝到此CTimeSpan对象中。

CTimeSpan operator +( CTimeSpan timeSpan ) const;
CTimeSpan operator -( CTimeSpan timeSpan ) const;

说明:这两个操作符允许你将两个CTimeSpan对象相加或相减。

const CTimeSpan& operator +=( CTimeSpan timeSpan );
const CTimeSpan& operator -=( CTimeSpan timeSpan );

说明:这些操作符允许你向或从此CTimeSpan中加上或减去一个CTimeSpan对象。

BOOL operator ==( CTimeSpan timeSpan ) const;
BOOL operator !=( CTimeSpan timeSpan ) const;
BOOL operator <( CTimeSpan timeSpan ) const;
BOOL operator >( CTimeSpan timeSpan ) const;
BOOL operator <=( CTimeSpan timeSpan ) const;
BOOL operator >=( CTimeSpan timeSpan ) const;

说明:这些操作符比较两个相对时间值。如果测试条件为真,则返回非零值;否则返回0。

2.计时器

计时器的作用就是用于计时,当到达规定的时间点时,会触发事先设置好的动作。SetTimer函数用于创建一个计时器,KillTimer函数用于销毁一个计时器。计时器属于系统资源,使用完应及时销毁。

1.创建计时器

SetTimer函数的原型如下:

UINT CWnd::SetTimer(UINT nIDEvent, UINT nElapse, void (CALLBACK EXPORT*lpfnTimer) (HWND, UINT, UINT, DWORD) );

返回值:如果函数成功,则返回新定时器的标识符。应用程序可以将这个值传递给KillTimer成员函数以销毁定时器。如果成功,则返回非零值;否则返回0。

参数说明
参数 含义
nIDEvent 指定了不为零的定时器标识符。
nElapse 指定了定时值;以毫秒为单位。
lpfnTimer 指定了应用程序提供的TimerProc回调函数的地址,该函数被用于处理WM_TIMER消息。如果这个参数为NULL,则WM_TIMER消息被放入应用程序的消息队列并由CWnd对象来处理。

说明:这个函数设置一个系统定时器。指定了一个定时值,每当发生超时,则系统就向设置定时器的应用程序的消息队列发送一个WM_TIMER消息,或者将消息传递给应用程序定义的TimerProc回调函数。lpfnTimer回调函数不需要被命名为TimerProc,但是它必须按照如下方式定义:

void CALLBACK EXPORT TimerProc(
   HWND hWnd,      // 调用SetTimer的CWnd的句柄
   UINT nMsg,      // WM_TIMER
   UINT nIDEvent,  // 定时器标识
   DWORD dwTime    // 系统时间
);

定时器是有限的全局资源;因此对于应用程序来说,检查SetTimer返回的值以确定定时器是否可用是很重要的。

2.销毁计时器

KillTimer函数的原型如下:

BOOL CWnd::KillTimer( int nIDEvent );

返回值:指定了函数的结果。如果事件已经被销毁,则返回值为非零值。如果KillTimer成员函数不能找到指定的定时器事件,则返回0。

参数: nIDEvent传递给SetTimer的定时器事件值。

说明:销毁以前调用SetTimer创建的用nIDEvent标识的定时器事件。任何与此定时器有关的未处理的WM_TIMER消息都从消息队列中清除。

3.位图类

CBitmap封装了Windows图形设备接口(GDI)中的位图,并且提供了操纵位图的成员函数。使用CBitmap对象之前要先构造CBitmap对象,调用其中的一个初始化成员函数设置位图对象的句柄。此后就可以调用其它成员函数了。

1.构造
CBitmap();

说明:构造一个CBitmap对象。生成的对象必须用下面的一个成员函数进行初始化。

2.初始化
BOOL LoadBitmap( LPCTSTR lpszRecourceName );
BOOL LoadBitmap( UINT nIDResource );

返回值:调用成功时返回非零值,否则为0。

参数:

  • lpszResourceName指向一个包含了位图资源名字的字符串(该字符串以null结尾)。
  • nIDResource指定位图资源中资源的ID号。

说明:本函数从应用的可执行文件中加载由lpszResourceName指定名字或者由nIDResource指定的ID号标志的位图资源。加载的位图被附在CBitmap对象上。如果由lpszResourceName指定名字的对象不存在,或者没有足够的内存加载位图,函数将返回0。可以调用函数CgdiObject::DeleteObject删除由LoadBitmap加载的位图,否则CBitmap的析构函数将删除该位图对象。

警告:在删除位图对象之前,要保证它没有被选到设备上下文中。在Windows3.1以及以后的版本中,增加了如下的位图:

  • OBM_UPARROWI
  • OBM_DNARROWI
  • OBM_RGARROWI
  • OBM_LFARROWI

在Windows3.0或者更早版本的设备驱动程序中不支持这些位图。

BOOL LoadOEMBitmap( UINT nIDBitmap );

返回值:调用成功返回非零值,否则为0。

参数:nIDBitmap:预定义的Windows位图的ID号。具体定义在头文件WINDOWS.H中。可用的值如下:

  • OBM_BTNCORNERS
  • OBM_OLD_RESTORE
  • OBM_BTSIZE
  • OBM_OLD_RGARROW
  • OBM_CHECK
  • OBM_OLD_UPARROW
  • OBM_CHECKBOXES
  • OBM_OLD_ZOOM
  • OBM_CLOSE
  • OBM_REDUCE
  • OBM_COMBO
  • OBM_REDUCED
  • OBM_DNARROW
  • OBM_RESTORE
  • OBM_DNARROWD
  • OBM_RESTORED
  • OBM_DNARROWI
  • OBM_RGARROW
  • OBM_LFARROW
  • OBM_RGARROWD
  • OBM_LFARROWD
  • OBM_RGARROWI
  • OBM_LFARROWI
  • OBM_SIZE
  • OBM_MNARROW
  • OBM_UPARROW
  • OBM_OLD_CLOSE
  • OBM_UPARROWD
  • OBM_OLD_DNARROW
  • OBM_UPARROWI
  • OBM_OLD_LFARROW
  • OBM_ZOOM
  • OBM_OLD_REDUCE
  • OBM_ZOOMD

说明:本函数用于加载一个Windows预定义的位图。以OBM_OLD开头的位图名表示它们是在Windows3.0之前的版本采用的。使用不是以OBM_OLD开头的常量,需要在包括头文件WINDOW.H之前定义常量OEMRESOURCE

BOOL LoadmappedBitmap(
     UINT nIDBitmap,
     UNIT nFlags = 0,
     LPCOLORMAP lpColorMap = NULL,
     int nMapSize = 0
);

返回值:调用成功时返回非零值,否则为0。

参数说明
参数 含义
nIDBitmap 位图资源的ID号。
nFlags 位图的标记。可以是0或者CMB_MASKED
lpColorMap 指向COLORMAP结构的一个指针。结构中记录了映射位图所需的颜色信息。如果本参数为NULL,函数将使用缺省的颜色映射。
nMapsize lpColorMap指向的颜色映射的数目。

说明:本函数加载一个位图并把它的颜色映射为当前系统颜色。缺省时LoadMapped Bitmap将映射通常在按钮图形中采用的颜色。

BOOL CreateBitmap( int nWidth, int nHeight, UINT nPlanes, UINT nBitcount, const void* lpBits );

返回值:调用成功时返回非零值,否则为0。

参数说明
参数 含义
nWidth 指定位图的宽度(以像素数为单位)。
nHeight 指定位图的高度(以像素数为单位)。
nPlanes 指定位图中的彩色位面数。
nBitCount 指定位图中每个像素颜色的位数。
lpBits 指向一个短整型数组,数组中记录了位图的初始位值。如果为NULL,则新的位图没有被初始化。

说明:本函数用指定的宽度、高度和位模式初始化依赖于设备的内存位图。对彩色位图来说,参数nPlanesnBitcount要有一个被设置为1。如果二者都被设置为1,则建立一个黑白位图。虽然不能为显示设备直接选中一个位图,但可以调用CDC::SelectObject把位图置为内存设备上下文(memory device context)的当前位图,然后调用CDC::BitBlt函数把它拷贝到任何兼容的设备上下文中。终止用CreateBitmap建立的CBitmap对象,先要从设备上下文中移出该位图,然后删除该对象。

BOOL CreateBitmapIndirect( LPBITMAP lpBitmap );

返回值:调用成功时返回非零值,否则为0。

参数:lpBitmap指向包含有关位图信息的BITMAP结构。

说明:本函数用lpBitmap指向的结构中指定的宽度、高度和位模式(可以不指定)初始化位图对象。虽然显示设备不能直接选中一个位图,但可以调用CDC::Select Object把位图置为内存设备上下文(memory device context)的当前位图,然后调用CDC::BitBltCDC::StrechBlt把它拷贝到任何兼容的设备上下文中(CDC::PatBlt函数能把当前画刷的位图直接拷贝到显示设备的上下文中)。如果已经调用函数GetObject填充了lpBitmap指向的结构,则位图的位值没有指定,并且位图未被初始化。要初始化该位图,应用可以调用CDC:BitBlt::SetDIBitsCgdiObject::GetObject函数的第一个参数指定的位图的位值拷贝到CreateBitmapIndirect建立的位图中。终止用CreateBitmapIndirect建立的CBitmap对象,要先从设备上下文中移出该位图,然后删除该对象。

BOOL CreateCompatibleBitmap( CDC* pDC, int nWidth, int nHeight );

返回值:调用成功时返回非零值,否则为0。

参数说明
参数 含义
pDC 指定设备上下文。
nWidth 指定位图的宽度(以像素数为单位)。
nHeight 指定位图的高度(以像素数为单位)。

说明:初始化一个与pDC指定的设备上下文兼容的位图。位图与指定的设备上下文具有相同的颜色位面数或相同的每个像素的位数。任何与pDC指定的设备兼容的内存设备都可以选择它作为当前位图。如果pDC指向的是内存设备上下文,则返回的位图与设备上下文中当前选中的位图具有相同的格式。“内存设备上下文”是一块表示一块显示区域的内存,它可以把图像存储在内存中,以备拷贝到兼容设备的真实显示区域中。建立一个内存设备上下文时,GDI自动地为它选择一个黑白原始位图。既然彩色内存设备上下文的当前位图既可以是彩色的也可以是黑白的,CreateCompatibleBitmap返回的位图就不一定是相同的格式设置。但是,非内存设备上下文的兼容位图的格式总是和设备的格式一致。终止用CreateCompatibleBitmap建立的CBitmap对象,要先从设备上下文中移出位图,然后删除该对象。

BOOL CreateDiscardableBitmap( CDC* pDC, int nWidth, int nHeight );

返回值:调用成功时返回非零值,否则为0。

参数同CreateCompatibleBitmap

说明:本函数初始化一个与pDC指定的设备上下文兼容的可丢弃的位图。位图与指定的设备上下文具有相同的颜色位面数或相同的每个像素的位数。任何与pDC指定的设备兼容的内存设备都可以选择它作为当前位图。应用没有把该函数建立的位图选到某个显示上下文中时,Windows可以丢弃该位图。如果在Windows丢弃了该位图之后,应用又试图选中该位图,函数CDC::SelectObject将返回NULL。终止用CreateDiscardableBitmap建立的CBitmap对象,要先从设备上下文中移出该位图,然后删除该对象。

3.属性
int GetBimap( BITMAP* pBitMap );

返回值:调用成功时返回非零值,否则为0。

参数:pBitMap指向BITMAP结构的一个指针,不能为NULL

说明:本函数用于查看CBitmap对象的信息。返回的信息存放在pBitMap指向的BITMAP结构中。

operator HBITMAP( ) const;

返回值:调用成功时返回一个由CBitmap对象表示的Windows GDI对象的句柄,否则返回NULL

说明:本操作符用于取得CBitmap对象上的Windows GDI对象句柄。这是一个校验性操作符,可直接参考HBITMAP对象。

4.操作
static CBitmap* PASCAL FromHandle( HBITMAP hBitmap );

返回值:调用成功时返回一个指向CBitmap对象的指针,否则返回NULL

参数:hBitmap指定一个Windows GDI 位图的句柄。

说明:本函数在调用时指定一个Windows GDI 位图的句柄,返回一个指向CBitmap对象的指针。如果该句柄上没有相联系的CBitmap对象,则为该句柄建立一个临时CBitmap对象。该临时CBitmap对象保持有效,直到应用在它的事件循环中出现空闲时间,此时Windows会删除所有的临时图形对象。换句话说,临时对象仅在一个Windows消息的处理过程中有效。

DWORD SetBitmapBits( DWORD dwCount, const void* lpBits );

返回值:调用成功时返回设置位图位值的字节数,否则为0。

参数说明
参数 含义
dwCount 指定由lpBits指向的字节数。
lpBits 指向一个BYTE类型的数组,数组中记录了要拷贝到CBitmap对象的位值。

说明:本函数用lpBits指定的位值设置位图的位值。

DWORD GetBitmapBits( DWORD dwCount, LPVOID lpBits ) const;

返回值:调用成功时返回位图的实际字节数,否则为0。

参数说明
参数 含义
dwCount 指定要拷贝的字节数。
lpBits 指向接收位图内容的缓冲。位图用字节数组表示,该数组与一个由很多双字节(16位)组成的结构等价。

说明:本函数把CBitmap对象的位模式拷贝到lpBits指向的缓冲中。参数dwCount指定待拷贝的字节数。可以调用函数CgdiObject::GetObject得到指定位图的正确dwCount值。

CSize SetBitmapDimension (int nWidth, int nHeight);

返回值:返回前一个位图的维数。高度存放在CSize对象的成员cy中,宽度存放在成员cx中。

参数说明
参数 含义
nWidth 指定位图的宽度(以0.1毫米为单位)。
nHeight 指定位图的高度(以0.1毫米为单位)。

说明:本函数用于设置位图的高度和宽度。GDI一般不用这些数字,除非应用调用成员函数GetBitmapDimension来获取它们。

CSize GetBitmapDimension( ) const;

返回值:返回位图的宽度和高度,以0.1毫米为单位。位图高度存放在Csize对象的成员cy中,宽度存放在成员cx中。如果没有调用SetBitmapDimension设置位图的宽度和高度,函数将返回0。

说明:本函数返回位图的宽度和高度。调用之前应已经调用SetBitmapDimension设置位图的宽度和高度。

2.实验目的

掌握MFC的常用类,对MFC类的派生和继承有详细了解以及熟练使用。

3.实验内容

利用OOP创建一个简单时钟如下:
时钟的实现(MFC)_第1张图片

4.代码实现

1.准备工作

首先利用向导创建项目,选择“基于对话框”后点击完成,并删除默认创建的按钮和静态文本控件以及将Caption属性改为Clock。

2.基类CClockBase

由于许多时钟元素存在颜色、外观等共同属性,因此可以创建一个基类用于派生实现,这样大大提高了代码的复用率。

ClockBase.h
#pragma once
class CClockBase
{
public:
	CClockBase();
	virtual ~CClockBase();
protected:
	COLORREF m_mainColor;
	COLORREF m_otherColor;
	CTime m_time;
	CRect m_region;
	int m_radius;
public:
	// 设置绘图区域
	void setRegion(LPRECT lprcRect);
	// 设置当前时间
	void setTime(const CTime& time);
	// 设置颜色
	void setColor(const COLORREF& mainColor, const COLORREF& otherColor);
	virtual void Draw(CDC *pDC) = 0;
};
ClockBase.cpp
#include "stdafx.h"
#include "ClockBase.h"

//#include 
//#define PI 3.14159265358979

CClockBase::CClockBase()
{
	m_radius = 0;
	m_mainColor = RGB(255, 255, 255);
	m_otherColor = RGB(128, 128, 128);
}


CClockBase::~CClockBase()
{
}


// 设置绘图区域
void CClockBase::setRegion(LPRECT lprcRect)
{
	m_region = lprcRect;
	m_radius = (m_region.Width() > m_region.Height() ? m_region.Height() : m_region.Width()) >> 1;
}


// 设置当前时间
void CClockBase::setTime(const CTime& time)
{
	m_time = time;
}


// 设置颜色
void CClockBase::setColor(const COLORREF& mainColor, const COLORREF& otherColor)
{
	m_mainColor = mainColor;
	m_otherColor = otherColor;
}

3.时钟背景类CClockBackground

用于绘制表盘的刻度,以公有方式继承,设置刻度颜色的默认值如下:

CClockBackground::CClockBackground()
{
	m_mainColor = RGB(0, 255, 0);
	m_otherColor = RGB(0, 128, 0);
	//m_otherColor = RGB(255, 255, 255);
}

此外,只需重写Draw函数即可,由于需要用到三角函数和int8_t,首先预处理如下:1

#include 
#define PI 3.14159265358979
#include 

然后实现绘图:

void CClockBackground::Draw(CDC* pDC)
{
	CPen mainPen(PS_SOLID, 1, m_mainColor), otherPen(PS_SOLID, 1, m_otherColor);
	CBrush mainBrush(m_mainColor), otherBrush(m_otherColor);
	CPoint center(m_region.CenterPoint());
	int8_t i(60);
	CPen* oldPen(pDC->SelectObject(&otherPen));
	CBrush* oldBrush(pDC->SelectObject(&mainBrush));
	int r(m_radius - 8);
	do
		if (--i % 5)
		{
			CPoint p(center);
			double theta(PI * i / 30);
			p.Offset(r*sin(theta), r*cos(theta));
			CRect dot(-2, -2, 2, 2);
			dot.OffsetRect(p);
			pDC->Ellipse(dot);
		}
	while (i);
	pDC->SelectObject(&mainPen);
	pDC->SelectObject(&otherBrush);
	do
	{
		CPoint p(center);
		double theta(PI * i / 6);
		p.Offset(r*sin(theta), r*cos(theta));
		CRect dot(-3, -3, 3, 3);
		dot.OffsetRect(p);
		pDC->Rectangle(dot);
	} while (++i < 12);
	pDC->SelectObject(oldPen);
	pDC->SelectObject(oldBrush);
}

4.时钟指针类CClockHourHandCClockMinuteHandCClockSecondHand

接着实现时钟指针类,类似地只需设置颜色初值以及重写绘图函数即可:

CClockHourHand::CClockHourHand()
{
	m_mainColor = RGB(0, 255, 100);
	m_otherColor = RGB(128, 128, 0);
}
void CClockHourHand::Draw(CDC* pDC)
{
	CPen mainPen(PS_SOLID, 1, m_mainColor), otherPen(PS_SOLID, 1, m_otherColor);
	CBrush mainBrush(m_mainColor), otherBrush(m_otherColor);
	CPen* oldPen(pDC->SelectObject(&otherPen));
	CBrush* oldBrush(pDC->SelectObject(&mainBrush));
	int time((m_time.GetHour() % 12) * 3600 + m_time.GetMinute() * 60 + m_time.GetSecond());
	double theta(PI * time / 21600), s_theta(sin(theta)), c_theta(cos(theta));
	/*CPoint p1(m_region.CenterPoint()), p2(p1), p3(p1), p4(p1);
	int r(m_radius >> 1);
	p1.Offset(r*s_theta, -r*c_theta);
	r >>= 2;
	p3.Offset(-r*s_theta, r*c_theta);
	r >>= 1;
	p2.Offset(r*c_theta, r*s_theta);
	p4.Offset(-r*c_theta, -r*s_theta);*/
	CPoint P[4], *p(P + 3), *q(p);
	*p = m_region.CenterPoint();
	do *--q = *p; while (q != P);
	int r(m_radius >> 1);
	q->Offset(r*s_theta, -r*c_theta);
	r >>= 2;
	(--p)->Offset(-r*s_theta, r*c_theta);
	r >>= 1;
	(++q)->Offset(r*c_theta, r*s_theta);
	(++p)->Offset(-r*c_theta, -r*s_theta);
	pDC->Polygon(P, 4);
	pDC->SelectObject(oldPen);
	pDC->SelectObject(oldBrush);
}
CClockMinuteHand::CClockMinuteHand()
{
	m_mainColor = RGB(0, 255, 100);
	m_otherColor = RGB(128, 128, 0);
}
void CClockMinuteHand::Draw(CDC *pDC)
{
	CPen mainPen(PS_SOLID, 1, m_mainColor), otherPen(PS_SOLID, 1, m_otherColor);
	CBrush mainBrush(m_mainColor), otherBrush(m_otherColor);
	CPen* oldPen(pDC->SelectObject(&otherPen));
	CBrush* oldBrush(pDC->SelectObject(&mainBrush));
	int time(m_time.GetMinute() * 60 + m_time.GetSecond());
	double theta(PI * time / 1800), s_theta(sin(theta)), c_theta(cos(theta));
	CPoint P[4], *p(P + 3), *q(p);
	*p = m_region.CenterPoint();
	do *--q = *p; while (q != P);
	int r((m_radius << 1) / 3);
	q->Offset(r*s_theta, -r*c_theta);
	r >>= 2;
	(--p)->Offset(-r*s_theta, r*c_theta);
	r >>= 1;
	(++q)->Offset(r*c_theta, r*s_theta);
	(++p)->Offset(-r*c_theta, -r*s_theta);
	pDC->Polygon(P, 4);
	pDC->SelectObject(oldPen);
	pDC->SelectObject(oldBrush);
}
CClockSecondHand::CClockSecondHand()
{
	m_mainColor = m_otherColor = RGB(0, 200, 200);
}
void CClockSecondHand::Draw(CDC *pDC)
{
	CPen mainPen(PS_SOLID, 1, m_mainColor);
	CPen* oldPen(pDC->SelectObject(&mainPen));
	CPoint P(m_region.CenterPoint());
	pDC->MoveTo(P);
	int r(m_radius - 10);
	double theta(PI * m_time.GetSecond() / 30);
	P.Offset(r*sin(theta), -r*cos(theta));
	pDC->LineTo(P);
	pDC->SelectObject(oldPen);
}

5.封装为控件CClockWidget

使用类向导添加类CClockWidget,并添加上面实现的4个成员变量,声明为私有的。此外,为了绘图方便,额外添加一个CRect类型的成员变量表示用户操作的区域:

#pragma once
#include "afxwin.h"
#include "ClockBackground.h"
#include "ClockHourHand.h"
#include "ClockMinuteHand.h"
#include "ClockSecondHand.h"
class CClockWidget :
	public CStatic
{
public:
	CClockWidget();
	~CClockWidget();
private:
	CClockBackground m_background;
	CClockHourHand m_hourhand;
	CClockMinuteHand m_minutehand;
	CClockSecondHand m_secondhand;
	CRect m_client;
	virtual void PreSubclassWindow();
public:
	DECLARE_MESSAGE_MAP()
	afx_msg void OnSize(UINT nType, int cx, int cy);
	afx_msg void OnTimer(UINT_PTR nIDEvent);
	afx_msg void OnPaint();
	void DrawClock(CDC* pDC);
};

接着在创建控件的时候进行初始化:

void CClockWidget::PreSubclassWindow()
{
	GetClientRect(m_client);
	m_background.setRegion(m_client);
	m_hourhand.setRegion(m_client);
	m_minutehand.setRegion(m_client);
	m_secondhand.setRegion(m_client);
	SetTimer(1u, 1000u, nullptr);

	CStatic::PreSubclassWindow();
}

然后当控件改变大小的时候进行更新:

void CClockWidget::OnSize(UINT nType, int cx, int cy)
{
	CStatic::OnSize(nType, cx, cy);

	GetClientRect(m_client);
	m_background.setRegion(m_client);
	m_hourhand.setRegion(m_client);
	m_minutehand.setRegion(m_client);
	m_secondhand.setRegion(m_client);
}

一旦计时器打点,我们需要刷新出发绘图事件:

void CClockWidget::OnTimer(UINT_PTR nIDEvent)
{
	Invalidate(FALSE);

	CStatic::OnTimer(nIDEvent);
}

最后实现绘图:

void CClockWidget::OnPaint()
{
	CPaintDC dc(this);
	CDC dcMem;
	dcMem.CreateCompatibleDC(&dc);
	CBitmap bmp;
	bmp.CreateCompatibleBitmap(&dc, m_client.Width(), m_client.Height());
	dcMem.SelectObject(&bmp);
	//dcMem.SetBkColor(RGB(255, 255, 255));
	//static CBrush whiteBrush(RGB(255, 255, 255));
	//dcMem.SelectObject(&whiteBrush);
	DrawClock(&dcMem);
	dc.BitBlt(0, 0, m_client.Width(), m_client.Height(), &dcMem, 0, 0, SRCCOPY);
}
void CClockWidget::DrawClock(CDC* pDC)
{
	CTime time(CTime::GetTickCount());
	m_background.setTime(time);
	m_hourhand.setTime(time);
	m_minutehand.setTime(time);
	m_secondhand.setTime(time);
	m_background.Draw(pDC);
	m_secondhand.Draw(pDC);
	m_minutehand.Draw(pDC);
	m_hourhand.Draw(pDC);
}

6.实例化控件

在资源视图的对话框窗口中加入一个CStatic控件和CMonthCalCtrl控件,并调整为合适大小、修改合适的ID。对CStatic控件进行添加变量,变量类型选择CClockWidget即可。

5.运行结果

时钟的实现(MFC)_第2张图片

6.总结

1.实验中遇到的困难

如何实现时钟指针

我一开始是认为时针、分针、秒针都具有某种共性,一开始是选择了类模板来写(未完成):

CClockHandBase.h:

#pragma once
#include "ClockBase.h"
template <double dir1, double dir2, double dir3, double dir4>
class CClockHandBase :
	public CClockBase
{
public:
	CClockHandBase();
	virtual ~CClockHandBase();
	virtual void Draw(CDC *pDC);
};

CClockHandBase.cpp:

#include "stdafx.h"
#include "ClockHandBase.h"


template <double dir1, double dir2, double dir3, double dir4>
CClockHandBase<dir1, dir2, dir3, dir4>::CClockHandBase()
{
	m_mainColor = RGB(0, 255, 100);
	m_otherColor = RGB(128, 128, 0);
}


template <double dir1, double dir2, double dir3, double dir4>
CClockHandBase<dir1, dir2, dir3, dir4>::~CClockHandBase()
{
}


template <double dir1, double dir2, double dir3, double dir4>
void CClockHandBase<dir1, dir2, dir3, dir4>::Draw(CDC *pDC)
{
	CPen mainPen(PS_SOLID, 1, m_mainColor), otherPen(PS_SOLID, 1, m_otherColor);
	CBrush mainBrush(m_mainColor), otherBrush(m_otherColor);
	CPen* oldPen(pDC->SelectObject(&otherPen));
	CBrush* oldBrush(pDC->SelectObject(&mainBrush));
}

本来试图通过typedef或者using来实例化时针、分针和秒针,比如:

typedef CClockHandBase<0.5,0.1,0.05,0.05> CClockHourHand;
typedef CClockHandBase<0.8,0.2,0.1,0.1> CClockHourHand;
typedef CClockHandBase<0.99,0.1,0.05,0.05> CClockHourHand;

但是这样会导致如下问题:

  • 颜色初始化不同的问题:比如时针和秒针就不应使用同一类颜色,否则大大影响美观性。
  • 每次的绘图角度问题:时针、分针、秒针在不同时刻的底层实现它们的角度 θ \theta θ是不同的,比如设当前时刻到当天零时刻距离为 t t t秒,并设
    { t ≡ t 1 ( m o d   3600 ) t ≡ t 2 ( m o d   60 ) \begin{cases} t\equiv t_1({\rm mod}\,3600)\\ t\equiv t_2({\rm mod}\,60) \end{cases} {tt1(mod3600)tt2(mod60)
    则有:
    { θ 时 = π 21600 t θ 分 = π 1800 t 1 θ 秒 = π 30 t 2 \begin{cases} \theta_\text{时}=\dfrac\pi{21600}t\\\\ \theta_\text{分}=\dfrac\pi{1800}t_1\\\\ \theta_\text{秒}=\dfrac\pi{30}t_2 \end{cases} θ=21600πtθ=1800πt1θ=30πt2
    这导致了模板不方便编写绘图函数。一种解决方案是再添加一个枚举类型的模板参数,比如:
enum HandClass{ HourHand, MinuteHand, SecondHand };

实际上这样做就完全没有必要进行前面的模板封装,甚至比单独实现时针、分针、秒针更加麻烦。

2.项目改进

由于时间原因,我没能继续改进程序,我认为它还可以进行如下方面的改进:

  1. CClockBackground类还可以进一步添加数据成员:
COLORREF m_textColor;

用来保存文字输出的颜色,并在合适位置用TextOut绘制文字。
2. CClockWidget类还可以进一步添加数据成员:

COLORREF m_backgroundColor;

从而设置背景板的颜色,而不是普通的黑色。
3. 这次实验没有将日期控件结合到目前时间上,将来可以进一步将CClockWidgetCMonthCalCtrl进行封装。
4. 从CClockWidget的实现方式可以看出,实际处理当前时间的时候还需要额外设置m_time的值,这样不仅运行效率低,而且造成了空间浪费。可以修改绘图函数的实现方式,将m_time改为引用传递:

void CClockBase::Draw(CDC* pDC, const CTime& time)

这样避免了拷贝的时间开销,可以提高程序运行效率。

3.心得体会

通过本次实验,我学会了如何利用MFC的常用类来创建一个简单的时钟控件。使用了类的派生和继承,大大提高了代码的复用率。同时,我也遇到了一些困难,比如如何实现时钟的指针类,但通过自己的思考和努力,最终找到了解决方案。

此外,我还学习到了双缓冲绘图技术。当数据量很大时,绘图可能需要几秒钟甚至更长的时间,而且有时还会出现闪烁现象,为了解决这些问题,可采用双缓冲技术来绘图。双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度。

这次实验让我更加熟悉了MFC的常用类的使用,也对面向对象的编程有了更深刻的理解。

代码地址:https://github.com/zsc118/MFC-exercises


  1. 今后不再一一指出这些预处理部分。 ↩︎

你可能感兴趣的:(MFC实验报告,mfc,c++)