4.5 小时钟

定时器

定时器的应用非常广泛,游戏中人物的活动、景色的变化等都是依靠定时器来实现的。当应用程序需要每隔一段时间得到通知时就可以申请一个定时器来使用。方法很简单,调用SetTimer函数将定时器安装上,Windows 就会每隔指定的时间间隔向应用程序窗口发送 WM_TIMER 消息,或调用一个编程者指定的函数。

UINT_PTR SetTimer(
    HWND hWnd, //与此定时器相关连的窗口的句柄
    UINT_PTR nIDEvent, //指定一个非0的定时器ID号 Windows用此ID号来表示不同的定时器
    UINT uElapse, //指定一个超时值 单位是毫秒 Windows每隔uElapse毫秒会通知一次你的程序
    TIMERPROC lpTimerFunc //指定一个回调函数 Windows会调用这个函数通知你的应用程序 如果此参数为NULL Windows会向程序的消息队列投递WM_TIMER消息
);

使用定时器的方法有两种。
(1)为定时器关联一个窗口句柄。此时创建定时器的代码如下。

::SetTimer(hWnd, IDT_TIMER1, 250, NULL); 
::SetTimer(hWnd, IDT_TIMER2, 10*1000, NULL); 

定时器ID号分别为 IDT_TIMER1 和 IDT_TIMER2,定时周期 250ms 和10s。每当一个定时周期过后,Windows 会向 hWnd 所代表的窗口投递WM_TIMER消息,其附带参数 wParam 的值是调用 SetTimer 时使用的标识定时器的ID号。

(2)给 lpTimerFunc 参数传递一个自定义函数的地址,在时间到的时候,Windows 会去调用这个函数。具体过程是这样的:DispatchMessage 函数在分发 GetMessage 函数取得的消息时,如果发现是 WM_TIMER 消息,而且消息的 lParam 参数不是 NULL,就会调用这个参数指向的函数 lpTimerFunc,而不再去调用窗口函数。

这里举一个最简单的例子来说明定时器的用法。运行这个例子后,在窗口的客户区单击鼠标左键计数就会开始,而且发出“嘟、嘟”的响声,再单击计数停止,其效果如图4.18所示。
在这里插入图片描述
下面是定时器04TimerDemo的实现代码。

#include <windows.h>

LRESULT __stdcall WndProc(HWND, UINT, WPARAM, LPARAM);

int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
	char szWindowClass[] = "Timer";

	// 注册窗口类
	WNDCLASSEX wcex;

	wcex.cbSize		= sizeof(WNDCLASSEX); 
	wcex.style		= CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc	= (WNDPROC)WndProc;
	wcex.cbClsExtra		= 0;
	wcex.cbWndExtra		= 0;
	wcex.hInstance		= hInstance;
	wcex.hIcon		= 0;
	wcex.hCursor		= LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground	= (HBRUSH)::GetStockObject(WHITE_BRUSH);
	wcex.lpszMenuName	= NULL;
	wcex.lpszClassName	= szWindowClass;
	wcex.hIconSm		= NULL;

	::RegisterClassEx(&wcex);


	// 创建并线程主窗口
	HWND hWnd = ::CreateWindowEx( 
		WS_EX_CLIENTEDGE,	// 扩展样式
		szWindowClass,		// 类名
		"定时器的使用",	// 标题
		WS_OVERLAPPEDWINDOW,	// 窗口样式
		CW_USEDEFAULT,	// 初始 X 坐标
		CW_USEDEFAULT,	// 初始 X 坐标
		CW_USEDEFAULT,	// 宽度
		CW_USEDEFAULT,	// 高度
		NULL,		// 父窗口句柄
		NULL,	        // 菜单句柄
		hInstance,	// 程序实例句柄
		NULL); 	

	::ShowWindow(hWnd, nShowCmd);
	::UpdateWindow(hWnd);

	// 进入消息循环
	MSG msg;
	while(::GetMessage(&msg, NULL, 0, 0))
	{
		::TranslateMessage(&msg);
		::DispatchMessage(&msg); 
	}
	
	return 1;
}


#define IDT_TIMER1 1

// 消息处理函数
LRESULT __stdcall WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static int nNum;	// 计数
	static int bSetTimer;	// 指示是否安装了定时器
	char szText[56];
	PAINTSTRUCT ps;
	HDC hdc;
	
	switch(message)
	{
	case WM_CREATE:		// 窗口正在被创建
		bSetTimer = FALSE;
		break;
	case WM_PAINT:		// 窗口客户区需要重画
		hdc = ::BeginPaint(hWnd, &ps);
		wsprintf(szText, "计数:%d", nNum);
		::TextOut(hdc, 10, 10, szText, strlen(szText));
		::EndPaint(hWnd, &ps);
		break;
	case WM_TIMER:		// 定时器时间已到
		if(wParam == IDT_TIMER1)
		{
			hdc = GetDC(hWnd);
			wsprintf(szText, "计数:%d", nNum++);
			::TextOut(hdc, 10, 10, szText, strlen(szText));

			// 发一声“嘟”的声音
			::MessageBeep(MB_OK);
		}
		break;
	case WM_LBUTTONDOWN:	// 用户单击鼠标左键
		if(bSetTimer)
		{
			// 插销一个已经安装的定时器
			::KillTimer(hWnd, IDT_TIMER1);
			bSetTimer = FALSE;
		}
		else
		{
			// 安装一个时间周期为250ms的定时器
			if(::SetTimer(hWnd, IDT_TIMER1, 250, NULL) == 0)
			// SetTimer函数调用成功会返回新的定时器的ID号,失败的话返回0
			{
				::MessageBox(hWnd, "安装定时器失败!", "03Timer", MB_OK);
			}
			else
			{
				bSetTimer = TRUE;
			}
		}
		break;
	case WM_CLOSE:		// 用户要求关闭窗口
		if(bSetTimer)
			::KillTimer(hWnd, IDT_TIMER1);
		break;
	case WM_DESTROY:	// 窗口正在被销毁
		::PostQuitMessage(0);
		break;
	}
	return ::DefWindowProc(hWnd, message, wParam, lParam);
}

当用户单击鼠标左键的时候,程序通过静态变量 bSetTimer 来决定是安装定时器还是撤销定时器。安装定时器以后,Windows会每隔 250ms 向窗口的消息队列投递一个 WM_TIMER 消息,wParam 参数指示了定时器的ID号。如果程序中安装有多个定时器,就要使用 ID 号判断到底是哪一个定时器的时间到了。

别指望 Windows 会非常精确地每隔一定的时间发送 WM_TIMER 消息。Windows 的定时器是基于时钟中断的,其精度只能是55ms 的整数倍。如果指定的值不符合标准的话,Windows 会以和这个间隔最接近的 55ms 的整数倍时间为触发周期。另外,在应用程序忙于处理其他消息的时候,级别较低的 WM_TIMER 消息也会被丢弃。

系统时间

取得系统时间的函数是GetLocalTime,用法如下。

void GetLocalTime(LPSYSTEMTIME lpSystemTime); 

此函数会把返回的时间信息放在SYSTEMTIME结构里。

typedef struct _SYSTEMTIME {
    WORD wYear; //年 
    WORD wMonth; //月
    WORD wDayOfWeek; //星期 0为星期日 1为星期一
    WORD wDay; //日
    WORD wHour; //时
    WORD wMinute; //分
    WORD wSecond; //秒
    WORD wMilliseconds; //毫秒
} SYSTEMTIME, *PSYSTEMTIME, *LPSYSTEMTIME;

04LocalTime当前的系统时间

#include <stdio.h>
#include <windows.h>

int main(int argc, char* argv[])
{
	SYSTEMTIME time;
	::GetLocalTime(&time);
	printf(" 当前时间为:%.2d:%.2d:%.2d \n", time.wHour, time.wMinute, time.wSecond);

	return 0;
}

在这里插入图片描述
使用SetLocalTime函数可以设置系统时间,其参数和GetLocalTime相同。

时钟程序

这个小时钟程序的整体思路很明了,用 GDI 函数画出时针、分针和秒针,创建时间间隔为 1s 的定时器,每次处理 WM_TIMER 消息的时候根据 GetLocalTime 函数返回的当前时间重新设置时钟指针的位置。

之前介绍了设置坐标系的 SetIsotropic 函数和绘制时钟外观的DrawClockFace 函数,它们的具体实现代码在这里就不重复了。下面再编写一个绘制时钟指针的函数 DrawHand。

// 指针的长度、宽度、相对于0点偏移的角度、颜色分别由参数nLength、nWidth、nDegrees、clrColor指定
void DrawHand(HDC hdc, int nLength, int nWidth, int nDegrees, COLORREF clrColor)
{
	// 将角度nDegrees转化成弧度 	2*3.1415926/360 == 0.0174533
	double nRadians = (double)nDegrees*0.0174533;

	// 计算坐标
	POINT pt[2];
	pt[0].x = (int)(nLength*sin(nRadians));
	pt[0].y = (int)(nLength*cos(nRadians));
	pt[1].x = -pt[0].x/5;
	pt[1].y = -pt[0].y/5;

	// 创建画笔,并选如DC结构中
	HPEN hPen = ::CreatePen(PS_SOLID, nWidth, clrColor);
	HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);

	// 画线
	::MoveToEx(hdc, pt[0].x, pt[0].y, NULL);
	::LineTo(hdc, pt[1].x, pt[1].y);

	::SelectObject(hdc, hOldPen);
	::DeleteObject(hPen);
}

参数 nDegrees 指定了指针偏移的角度,但是函数 sin 和 cos 参数的单位是弧度,所以要先将角度乘以 0.0174533 转化成弧度。图4.20所示说明了指针两端坐标的计算过程。
在这里插入图片描述
pt[0]坐标的值通过直角三角形各边的关系很容易就求得,pt[1]的值是pt[0]关于原点对称后再缩小5倍后得到的。

下面是具体的消息处理代码。为了维护公共的状态信息,首先定义了一些全局静态变量。窗口函数 WndProc 的基本框架也列到了下面。

//上一次Windows通知时的时间
static int s_nPreHour;		//小时	
static int s_nPreMinute;	//分钟
static int s_nPreSecond;	//秒
//窗口客户区的大小
static int s_cxClient;		
static int s_cyClient;
//是否位于最顶层
static BOOL s_bTopMost;

LRESULT __stdcall WndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
	switch(nMsg)
	{
	case WM_CREATE:
		{	
			// 设置时间
			SYSTEMTIME time;
			::GetLocalTime(&time);
			s_nPreHour = time.wHour%12;
			s_nPreMinute = time.wMinute;
			s_nPreSecond = time.wSecond;
			
			// 创建定时器
			::SetTimer(hWnd, IDT_CLOCK, 1000, NULL);
			return 0;
		}
	case WM_CLOSE:
		{
			::KillTimer(hWnd, IDT_CLOCK);
			::DestroyWindow(hWnd);
			return 0;
		}
	case WM_DESTROY:
		{
			::PostQuitMessage(0);
			return 0;
		}
		return ::DefWindowProc(hWnd, nMsg, wParam, lParam);
}

在窗口刚被创建,即接受到 WM_CREATE 消息的时候,要初始化全局变量的值,并安装定时器。

窗口成功被创建,在调用 ShowWindow 函数显示窗口的时候,窗口会接受到 WM_SIZE 消息通知,说明窗口的大小被改变了。wParam 参数指定了接受到此消息的原因,lParam 参数包含新的大小的值。其实每次窗口大小的改变都会使该窗口收到WM_SIZE 消息,所以可以在响应WM_SIZE消息时更新全局变量s_cxClient和s_cyClient的值。

case WM_SIZE:
		{
			s_cxClient = LOWORD(lParam);
			s_cyClient = HIWORD(lParam);
			return 0;
		}

在接收到 WM_PAINT 消息的时候,就要在窗口的客户区绘制时钟了,下面是 OnPaint 函数中的代码。

case WM_PAINT:
		{
			PAINTSTRUCT ps;
			HDC hdc = ::BeginPaint(hWnd, &ps);
			
			// 设置坐标系
			SetIsotropic(hdc, s_cxClient, s_cyClient);
			
			// 绘制时钟外观
			DrawClockFace(hdc);
			
			// 绘制指针
			
			// 经过1个小时时针走30度(360/12),经过1分钟时针走0.5度(30/60)
			DrawHand(hdc, 200, 8, s_nPreHour*30 + s_nPreMinute/2, RGB(0, 0, 0));
			// 经过1分钟分针走6度(360/60)
			DrawHand(hdc, 400, 6, s_nPreMinute*6, RGB(0, 0, 0));
			// 经过1秒钟秒针走6度(360/60)
			DrawHand(hdc, 400, 1, s_nPreSecond*6, RGB(0, 0, 0));
			
			::EndPaint(hWnd, &ps);
			return 0;
		}

现在运行程序,一个没有走动的时钟就会出现。要想让时钟开始工作,处理定时器消息 WM_TIMER 即可。

case WM_TIMER:
		{
			// 如果窗口处于最小化状态就什么也不做
			if(::IsIconic(hWnd))	// IsIconic函数用来判断窗口是否处于最小化状态
				return 0;
			
			// 取得系统时间
			SYSTEMTIME time; 
			::GetLocalTime(&time);
			
			// 建立坐标系
			HDC hdc = ::GetDC(hWnd);
			SetIsotropic(hdc, s_cxClient, s_cyClient);
			
			// 以COLOR_3DFACE为背景色就可以擦除指针了(因为窗口的背景色也是COLOR_3DFACE)
			COLORREF crfColor = ::GetSysColor(COLOR_3DFACE); 
			
			// 如果分钟改变的话就擦除时针和分针
			if(time.wMinute != s_nPreMinute)
			{
				// 擦除时针和分针
				DrawHand(hdc, 200, 8, s_nPreHour*30 + s_nPreMinute/2, crfColor);
				DrawHand(hdc, 400, 6, s_nPreMinute*6, crfColor);
				s_nPreHour = time.wHour;
				s_nPreMinute = time.wMinute;
			}
			
			// 如果秒改变的话就擦除秒针,然后重画所有指针
			if(time.wSecond != s_nPreSecond)
			{
				// 擦除秒针
				DrawHand(hdc, 400, 1, s_nPreSecond*6, crfColor);
				
				// 重画所有指针
				DrawHand(hdc, 400, 1, time.wSecond*6, RGB(0, 0, 0));
				DrawHand(hdc, 200, 8, time.wHour*30 + time.wMinute/2, RGB(0, 0, 0));
				DrawHand(hdc, 400, 6, time.wMinute*6, RGB(0, 0, 0));
				s_nPreSecond = time.wSecond;
			}
			return 0;
		}

移动指针要调用两次DrawHandle函数,第一次是以窗口的背景色(COLOR_3DFACE)为参数调用此函数以擦除这个指针,第二次是在新的位置绘制指针。到此,一个有着基本功能的时钟程序就完成了。

移动窗口

要去掉时钟的标题栏很简单,在创建窗口的时候为窗口样式传递WS_POPUP 和 WS_SYSMENU 的组合即可。

// 创建并显示主窗口
HWND hWnd = ::CreateWindowEx(NULL, szWindowClass, "时钟", 
		WS_POPUP|WS_SYSMENU|WS_SIZEBOX, 100, 100, 300, 300, NULL, NULL, hInstance, NULL);

但是没有标题栏以后的窗口即不能被用户移动,也不能比较方便地关闭。本小节通过欺骗Windows解决移动窗口的问题,下一个小节使用快捷键解决关闭窗口的问题。

在 Windows 下,每一个鼠标消息都是由 WM_NCHITTEST 消息产生的,这个消息的参数包含了鼠标位置的信息。通常情况下,要把这个消息直接交给 DefWindowProc 函数处理,该函数会返回一个值来告诉 Windows 鼠标按下的是窗口的哪一部分。Windows 利用这个返回值来决定要发送的鼠标消息的类型。例如,如果用鼠标左键单击窗口的标题栏,处理 WM_NCHITTEST消息的 DefWindowProc 函数会返回 HTCAPTION,然后Windows 会再向该窗口发送 WM_NCLBUTTONDOWN 消息。如果 DefWindowProc 的返回值是 HTCLIENT,Windows就将鼠标所在位置的坐标从屏幕坐标转化成为客户区坐标,并且通过WM_LBUTTONDOWN 消息通知用户。

为了在客户区拖动鼠标就能够移动窗口,必须欺骗Windows,让它认为用户是在拖动标题栏。可以通过改变处理 WM_NCHITTEST 消息的 DefWindowProc 函数的返回值实现这一点。

case WM_NCHITTEST:
		{
			UINT nHitTest;
			nHitTest = ::DefWindowProc(hWnd, WM_NCHITTEST, wParam, lParam);
			if (nHitTest == HTCLIENT ) 
				nHitTest = HTCAPTION;
			return nHitTest;
		}

有了上面的代码,拖动一个窗口的客户区就会像拖动此窗口的标题栏一样简单。而且即使是一个没有标题栏的窗口也能够被鼠标拖动。完整的代码是这样的:

case WM_NCHITTEST:
		{
			UINT nHitTest;
			nHitTest = ::DefWindowProc(hWnd, WM_NCHITTEST, wParam, lParam);
			if (nHitTest == HTCLIENT &&
				::GetAsyncKeyState(MK_LBUTTON) < 0) // 如果鼠标左键按下,GetAsyncKeyState函数的返回值小于0
				nHitTest = HTCAPTION;
			return nHitTest;
		}

这个实现代码和上面差不多,惟一不同的是,在改变DefWindowProc 返回值以前还要检查鼠标左键的状态,以免其他的鼠标消息,特别是鼠标右键产生的 WM_CONTEXTMENU 消息也被屏蔽掉。

使用快捷菜单

当用户在窗口上单击右键时,Windows 会发送WM_CONTEXTMENU 消息通知程序。该消息的 wParam 参数是用户单击的窗口的句柄,lParam 参数包含了鼠标的位置信息。一般应用程序都是通过调用 TrackPopupMenu 函数弹出快捷菜单来响应此消息的。

BOOL TrackPopupMenu(
    _In_ HMENU hMenu, //菜单句柄 
    _In_ UINT uFlags, //此参数指定了一些和弹出的菜单的位置相关的选项 
    _In_ int x, //(x, y)是弹出的菜单的基于屏幕的坐标 
    _In_ int y, 
    _Reserved_ int nReserved, //保留
    _In_ HWND hWnd, //此菜单属于哪一个窗口所有(也就是WM_COMMAND消息发到这个窗口)
    _Reserved_ CONST RECT *prcRect //指定此菜单占有的区域。设为NULL的话,Windows会自动调整区域的大小
);

本例中响应WM_CONTEXTMENU消息的代码如下。

case WM_CONTEXTMENU:
		POINT pt; pt.x = LOWORD(lParam); pt.y = HIWORD(lParam);
		{
			// 取得系统菜单的句柄
			HMENU hSysMenu = ::GetSystemMenu(hWnd, FALSE);
			
			// 弹出系统菜单
			int nID = ::TrackPopupMenu(hSysMenu,TPM_LEFTALIGN|TPM_RETURNCMD, 
				pt.x, pt.y, 0, hWnd, NULL);
			if(nID > 0)
				::SendMessage(hWnd, WM_SYSCOMMAND, nID, 0);	
			return 0;
		}

用户从菜单上选择命令的动作会产生 WM_COMMAND 消息,而不是 WM_SYSCOMMAND 消息。为了改变这种默认行为,给TrackPopupMenu 函数传递的 uFlags 参数中包含了TPM_RETURNCMD 标志,从而促使 TrackPopupMenu 函数返回用户所选菜单的ID号。如果 TrackPopupMenu 的返回值大于0,就说明用户从弹出菜单中选择了一个菜单。以返回的ID号为参数wParam的值,程序给自己发送了一个 WM_SYSCOMMAND 消息。

在系统菜单中添加自定义的项也很简单,只需在响应WM_CREATE 消息的时候调用 AppendMenu 函数。Clock程序添加了“总在最前”和“帮助”两个菜单。

// 向系统菜单中添加自定义的项
HMENU hSysMenu;
hSysMenu = ::GetSystemMenu(hWnd, FALSE);
::AppendMenu(hSysMenu, MF_SEPARATOR, 0, NULL);
::AppendMenu(hSysMenu, MF_STRING, IDM_TOPMOST, "总在最前");
::AppendMenu(hSysMenu, MF_STRING, IDM_HELP, "帮助");

在此函数的上面还有 IDM_TOPMOST 和 IDM_HELP 的定义代码。

const int IDM_HELP = 100;
const int IDM_TOPMOST = 101;

响应用户单击系统菜单命令的代码如下。

case WM_SYSCOMMAND:
		int nID = wParam;
		{
			if(nID == IDM_HELP)
			{
				::MessageBox(hWnd, "一个时钟的例子", "时钟", 0);
			}
			else if(nID == IDM_TOPMOST)
			{
				HMENU hSysMenu = ::GetSystemMenu(hWnd, FALSE);
				if(s_bTopMost)
				{
					::CheckMenuItem(hSysMenu, IDM_TOPMOST, MF_UNCHECKED);
					::SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, 
								SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSIZE);
					s_bTopMost = FALSE;
				}
				else
				{
					::CheckMenuItem(hSysMenu, IDM_TOPMOST, MF_CHECKED);
					::SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0,
								SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSIZE);
					s_bTopMost = TRUE;
				}
			}
			return ::DefWindowProc(hWnd, WM_SYSCOMMAND, nID, 0);
		}
	}

动态的管理菜单就要先取得菜单句柄,GetSystemMenu 函数返回系统菜单的句柄。AppendMenu 函数追增一个新项到指定菜单条的结尾,而 CheckMenuItem 函数可以设置指定菜单项的状态。这些函数的用法都很简单,这里就不专门讨论了。

SetWindowPos 函数用来为窗口指定一个新的位置和状态,它也可改变窗口在内部窗口列表中的位置,原型如下。

BOOL SetWindowPos(
    _In_ HWND hWnd, //欲定位的窗口
    _In_opt_ HWND hWndInsertAfter, //一个窗口句柄,在Z轴上将位于这个窗口之后
    _In_ int X, //窗口新的x坐标。如果hWnd是一个子窗口,则X用父窗口的客户区坐标表示
    _In_ int Y, //窗口新的y坐标。如果hWnd是一个子窗口,则Y用父窗口的客户区坐标表示 
    _In_ int cx, //指定新的窗口宽度 
    _In_ int cy, //指定新的窗口高度 
    _In_ UINT uFlags //包含了标志的一个整数 
);

hWndInsertAfter 参数指定了欲定位窗口的位置。在窗口列表中,窗口 hWnd 会被置于这个句柄所代表窗口的后面。也可以选用下述值之一:
● HWND_BOTTOM 将窗口置于窗口列表底部
● HWND_TOP 将置于本线程窗口的顶部
● HWND_TOPMOST 将窗口置于列表顶部,并位于任何最顶部窗口的前面
● HWND_NOTOPMOST 将窗口置于列表顶部,并位于任何最顶部窗口的后面 uFlags参数可取以下的值:
● SWP_DRAWFRAME 围绕窗口画一个框
● SWP_HIDEWINDOW 隐藏窗口
● SWP_NOACTIVATE 不激活窗口
● SWP_NOMOVE 保持当前位置(x和y设定将被忽略)
● SWP_NOREDRAW 窗口不自动重画
● SWP_NOSIZE 保持当前大小(cx和cy会被忽略)
● SWP_NOZORDER 保持窗口在列表的当前位置(hWndInsertAfter将被忽略)
● SWP_SHOWWINDOW 显示窗口
● SWP_FRAMECHANGED 强迫一条WM_NCCALCSIZE消息进入窗口,即使窗口的大小没有改变

本程序为这个参数传递的值是SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSIZE,即只改变窗口在Z轴序列列表中的位置。

整个小时钟程序的全部功能就全部实现了04Clock工程

// Clock.cpp文件

#include <windows.h>


#include "resource.h"
#include <math.h>

LRESULT __stdcall WndProc(HWND, UINT, WPARAM, LPARAM);
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
	char szWindowClass[] = "Clock";

	// 注册窗口类
	WNDCLASSEX wcex;

	wcex.cbSize		= sizeof(WNDCLASSEX); 
	wcex.style		= CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc	= (WNDPROC)WndProc;
	wcex.cbClsExtra		= 0;
	wcex.cbWndExtra		= 0;
	wcex.hInstance		= hInstance;
	wcex.hIcon		= ::LoadIcon(hInstance, (LPCTSTR)IDI_MAIN);
	wcex.hCursor		= LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground	= (HBRUSH)(COLOR_3DFACE + 1); // 此域可以是一个系统定义的颜色值
	wcex.lpszMenuName	= NULL;
	wcex.lpszClassName	= szWindowClass;
	wcex.hIconSm		= NULL;

	::RegisterClassEx(&wcex);


	// 创建并显示主窗口
	HWND hWnd = ::CreateWindowEx(NULL, szWindowClass, "时钟", 
		WS_POPUP|WS_SYSMENU|WS_SIZEBOX, 100, 100, 300, 300, NULL, NULL, hInstance, NULL); 	
	::ShowWindow(hWnd, nShowCmd);
	::UpdateWindow(hWnd);

	// 进入消息循环
	MSG msg;
	while(::GetMessage(&msg, NULL, 0, 0))
	{
		::TranslateMessage(&msg);
		::DispatchMessage(&msg); 
	}

	return 1;
}



//
// 消息处理代码

#define IDT_CLOCK 1
const int IDM_HELP = 100;
const int IDM_TOPMOST = 101;

// 实现函数
void SetIsotropic(HDC hdc,  int cxClient, int cyClient);
void DrawClockFace(HDC hdc);
void DrawHand(HDC hdc, int nLength, int nWidth, int nDegrees, COLORREF clrColor);

// 上一次Windows通知时的时间
static int s_nPreHour;		// 小时	
static int s_nPreMinute;	// 分钟
static int s_nPreSecond;	// 秒
// 窗口客户区的大小
static int s_cxClient;		
static int s_cyClient;
// 是否位于最顶层
static BOOL s_bTopMost;

LRESULT __stdcall WndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
	switch(nMsg)
	{
	case WM_CREATE:
		{	
			// 向系统菜单中添加自定义的项
			HMENU hSysMenu;
			hSysMenu = ::GetSystemMenu(hWnd, FALSE);
			::AppendMenu(hSysMenu, MF_SEPARATOR, 0, NULL);
			::AppendMenu(hSysMenu, MF_STRING, IDM_TOPMOST, "总在最前");
			::AppendMenu(hSysMenu, MF_STRING, IDM_HELP, "帮助");
			
			// 设置时间
			SYSTEMTIME time;
			::GetLocalTime(&time);
			s_nPreHour = time.wHour%12;
			s_nPreMinute = time.wMinute;
			s_nPreSecond = time.wSecond;
			
			// 创建定时器
			::SetTimer(hWnd, IDT_CLOCK, 1000, NULL);
			return 0;
		}
	case WM_SIZE:
		{
			s_cxClient = LOWORD(lParam);
			s_cyClient = HIWORD(lParam);
			return 0;
		}
	case WM_PAINT:
		{
			PAINTSTRUCT ps;
			HDC hdc = ::BeginPaint(hWnd, &ps);
			
			// 设置坐标系
			SetIsotropic(hdc, s_cxClient, s_cyClient);
			
			// 绘制时钟外观
			DrawClockFace(hdc);
			
			// 绘制指针
			
			// 经过1个小时时针走30度(360/12),经过1分钟时针走0.5度(30/60)
			DrawHand(hdc, 200, 8, s_nPreHour*30 + s_nPreMinute/2, RGB(0, 0, 0));
			// 经过1分钟分针走6度(360/60)
			DrawHand(hdc, 400, 6, s_nPreMinute*6, RGB(0, 0, 0));
			// 经过1秒钟秒针走6度(360/60)
			DrawHand(hdc, 400, 1, s_nPreSecond*6, RGB(0, 0, 0));
			
			::EndPaint(hWnd, &ps);
			return 0;
		}
	case WM_TIMER:
		{
			// 如果窗口处于最小化状态就什么也不做
			if(::IsIconic(hWnd))	// IsIconic函数用来判断窗口是否处于最小化状态
				return 0;
			
			// 取得系统时间
			SYSTEMTIME time; 
			::GetLocalTime(&time);
			
			// 建立坐标系
			HDC hdc = ::GetDC(hWnd);
			SetIsotropic(hdc, s_cxClient, s_cyClient);
			
			// 以COLOR_3DFACE为背景色就可以擦除指针了(因为窗口的背景色也是COLOR_3DFACE)
			COLORREF crfColor = ::GetSysColor(COLOR_3DFACE); 
			
			// 如果分钟改变的话就擦除时针和分针
			if(time.wMinute != s_nPreMinute)
			{
				// 擦除时针和分针
				DrawHand(hdc, 200, 8, s_nPreHour*30 + s_nPreMinute/2, crfColor);
				DrawHand(hdc, 400, 6, s_nPreMinute*6, crfColor);
				s_nPreHour = time.wHour;
				s_nPreMinute = time.wMinute;
			}
			
			// 如果秒改变的话就擦除秒针,然后重画所有指针
			if(time.wSecond != s_nPreSecond)
			{
				// 擦除秒针
				DrawHand(hdc, 400, 1, s_nPreSecond*6, crfColor);
				
				// 重画所有指针
				DrawHand(hdc, 400, 1, time.wSecond*6, RGB(0, 0, 0));
				DrawHand(hdc, 200, 8, time.wHour*30 + time.wMinute/2, RGB(0, 0, 0));
				DrawHand(hdc, 400, 6, time.wMinute*6, RGB(0, 0, 0));
				s_nPreSecond = time.wSecond;
			}
			return 0;
		}
	case WM_CONTEXTMENU:
		POINT pt; pt.x = LOWORD(lParam); pt.y = HIWORD(lParam);
		{
			// 取得系统菜单的句柄
			HMENU hSysMenu = ::GetSystemMenu(hWnd, FALSE);
			
			// 弹出系统菜单
			int nID = ::TrackPopupMenu(hSysMenu,TPM_LEFTALIGN|TPM_RETURNCMD, 
				pt.x, pt.y, 0, hWnd, NULL);
			if(nID > 0)
				::SendMessage(hWnd, WM_SYSCOMMAND, nID, 0);	
			return 0;
		}
	case WM_CLOSE:
		{
			::KillTimer(hWnd, IDT_CLOCK);
			::DestroyWindow(hWnd);
			return 0;
		}
	case WM_DESTROY:
		{
			::PostQuitMessage(0);
			return 0;
		}
	case WM_NCHITTEST:
		{
			UINT nHitTest;
			nHitTest = ::DefWindowProc(hWnd, WM_NCHITTEST, wParam, lParam);
			if (nHitTest == HTCLIENT &&
				::GetAsyncKeyState(MK_LBUTTON) < 0) // 如果鼠标左键按下,GetAsyncKeyState函数的返回值小于0
				nHitTest = HTCAPTION;
			return nHitTest;
		}
	case WM_SYSCOMMAND:
		int nID = wParam;
		{
			if(nID == IDM_HELP)
			{
				::MessageBox(hWnd, "一个时钟的例子", "时钟", 0);
			}
			else if(nID == IDM_TOPMOST)
			{
				HMENU hSysMenu = ::GetSystemMenu(hWnd, FALSE);
				if(s_bTopMost)
				{
					::CheckMenuItem(hSysMenu, IDM_TOPMOST, MF_UNCHECKED);
					::SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, 
								SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSIZE);
					s_bTopMost = FALSE;
				}
				else
				{
					::CheckMenuItem(hSysMenu, IDM_TOPMOST, MF_CHECKED);
					::SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0,
								SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSIZE);
					s_bTopMost = TRUE;
				}
			}
			return ::DefWindowProc(hWnd, WM_SYSCOMMAND, nID, 0);
		}
	}

	return ::DefWindowProc(hWnd, nMsg, wParam, lParam);
}




void SetIsotropic(HDC hdc, int cx, int cy)
{
	::SetMapMode(hdc, MM_ISOTROPIC);
	::SetWindowExtEx(hdc, 1000, 1000, NULL);
	::SetViewportExtEx(hdc, cx, -cy, NULL);
	::SetViewportOrgEx(hdc, cx/2, cy/2, NULL);
}

// 绘制时钟的外观
void DrawClockFace(HDC hdc)
{
	const int SQUARESIZE = 20;
	static POINT pt[] =
	{
		0, 450,		// 12点
		225, 390,	// 1点
		390, 225,	// 2点
		450, 0,		// 3点
		390, -225,	//... 下面的坐标是上面的点的对称点(以X轴、Y轴或原点对称)
		225, -390,
		0, -450,
		-225, -390,
		-390, -225,
		-450, 0,
		-390, 225,
		-225, 390
	};

	// 选择一个黑色的画刷
	::SelectObject(hdc, ::GetStockObject(BLACK_BRUSH));

	// 画12个圆
	for(int i=0; i<12; i++)
	{
		::Ellipse(hdc, pt[i].x - SQUARESIZE, pt[i].y + SQUARESIZE,
			pt[i].x + SQUARESIZE, pt[i].y - SQUARESIZE);
	}
}



// 指针的长度、宽度、相对于0点偏移的角度、颜色分别由参数nLength、nWidth、nDegrees、clrColor指定
void DrawHand(HDC hdc, int nLength, int nWidth, int nDegrees, COLORREF clrColor)
{
	// 将角度nDegrees转化成弧度 .	2*3.1415926/360 == 0.0174533
	double nRadians = (double)nDegrees*0.0174533;

	// 计算坐标
	POINT pt[2];
	pt[0].x = (int)(nLength*sin(nRadians));
	pt[0].y = (int)(nLength*cos(nRadians));
	pt[1].x = -pt[0].x/5;
	pt[1].y = -pt[0].y/5;

	// 创建画笔,并选如DC结构中
	HPEN hPen = ::CreatePen(PS_SOLID, nWidth, clrColor);
	HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);

	// 画线
	::MoveToEx(hdc, pt[0].x, pt[0].y, NULL);
	::LineTo(hdc, pt[1].x, pt[1].y);

	::SelectObject(hdc, hOldPen);
	::DeleteObject(hPen);
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳光开朗男孩

你的鼓励是我最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值