使用GDI接口在视图上绘制一个时钟。
GDI是Graphics Device Interface的缩写,含义是图形设备接口,它的主要任务是负责系统与绘图程序之间的信息交换,处理所有Windows程序的图形输出。
在Windows操作系统下,绝大多数具备图形界面的应用程序都离不开GDI,我们利用GDI所提供的众多函数就可以方便的在屏幕、打印机及其它输出设备上输出图形,文本等操作。
GDI函数大致可分类为:
设备上下文函数(如GetDC、CreateDC、DeleteDC)、 画线函数(如LineTo、Polyline、Arc)、填充画图函数(如Ellipse、FillRect、Pie)、画图属性函数(如SetBkColor、SetBkMode、SetTextColor)、文本、字体函数(如TextOut、GetFontData)、位图函数(如SetPixel、BitBlt、StretchBlt)、坐标函数(如DPtoLP、LPtoDP、ScreenToClient、ClientToScreen)、映射函数(如SetMapMode、SetWindowExtEx、SetViewportExtEx)、元文件函数(如PlayMetaFile、SetWinMetaFileBits)、区域函数(如FillRgn、FrameRgn、InvertRgn)、路径函数(如BeginPath、EndPath、StrokeAndFillPath)、裁剪函数(如SelectClipRgn、SelectClipPath)等。
——百度百科
最初想法:
建立一个单文档工程,在OnDraw利用
1
2
|
CClientDC dc(this);
dc.Ellipse(CRect(100,100,500,500));
|
画圆,但是打开的窗口太大了。于是,想初始化窗口,在CMainFrame里的PreCreateWindow设置窗口大小:
1
2
3
4
5
6
7
8
9
10
11
|
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if( !CFrameWnd::PreCreateWindow(cs) )
return FALSE;
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
cs.cx = 630;
cs.cy = 680;
return TRUE;
}
|
接下来画钟面上的格子。用MoveTo和LineTo确定线段起始点和终点。我用了一个比较蠢的方法:手算坐标!于是,产生了这样的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
void CClockView::OnDraw(CDC* pDC)
{
CClockDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
CClientDC dc(this);
dc.Ellipse(CRect(100,100,500,500));
//上
dc.MoveTo(300,140);
dc.LineTo(300,100);
//下
dc.MoveTo(300,460);
dc.LineTo(300,500);
//左
dc.MoveTo(140,300);
dc.LineTo(100,300);
//右
dc.MoveTo(460,300);
dc.LineTo(500,300);
//第一区间第二点
dc.MoveTo(380,300-80*1.73);
dc.LineTo(400,300-100*1.73);
//第一区间第三点
dc.MoveTo(300+80*1.73,220);
dc.LineTo(300+100*1.73,200);
//第二区间第二点
dc.MoveTo(300+80*1.73,380);
dc.LineTo(300+100*1.73,400);
//第二区间第三点
dc.MoveTo(380,300+80*1.73);
dc.LineTo(400,300+100*1.73);
//第三区间第二点
dc.MoveTo(220,300+80*1.73);
dc.LineTo(200,300+100*1.73);
//第三区间第三点
dc.MoveTo(300-80*1.73,380);
dc.LineTo(300-100*1.73,400);
//第四区间第二点
dc.MoveTo(300-80*1.73,220);
dc.LineTo(300-100*1.73,200);
//第四区间第三点
dc.MoveTo(220,300-80*1.73);
dc.LineTo(200,300-100*1.73);
}
|
[连三角函数都近似等于的我……等会儿会谈优化问题]
效果是这样:

在视类添加WM_CREATE响应函数,设置一个定时器,1秒发送一次消息。
1
2
3
4
5
6
7
8
9
10
|
int CClockView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
// TODO: Add your specialized creation code here
SetTimer(1, 1000, NULL);
return 0;
}
|
因为指针是在动的,所以要及时刷新页面,在视类添加WM_TIMER响应函数:
1
2
3
4
5
6
7
8
|
void CClockView::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
InvalidateRect(NULL, TRUE);
UpdateWindow();
CView::OnTimer(nIDEvent);
}
|
为了和系统时间一致,就要获取时间,在OnDraw中加入:
1
|
CTime Time = CTime::GetCurrentTime();
|
接下来就是创建三个指针:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
double Radians;
//时针
Radians = Time.GetHour() + Time.GetMinute()/60.0 + Time.GetSecond()/3600.0;
Radians *= 2 * 3.14 / 12.0;
dc.MoveTo(300, 300);
dc.LineTo(300 + (int)((double)100 * sin(Radians)), 300 - (int)((double)100 * cos(Radians))); //取半径的1/2
//分针
Radians = Time.GetMinute() + Time.GetSecond()/60;
Radians *= 2 * 3.14 / 60.0;
dc.MoveTo(300, 300);
dc.LineTo(300 + (int)((double)150 * sin(Radians)), 300 - (int)((double)150 * cos(Radians))); //取半径的3/4
//秒针
Radians = Time.GetSecond();
Radians *= 2 * 3.14 / 60.0;
dc.MoveTo(300, 300);
dc.LineTo(300 + (int)((double)190 * sin(Radians)), 300 - (int)((double)190 * cos(Radians))); //取半径的19/20
|
为了三角函数能用,在函数头部添加#include "math.h"
。

美化界面:
1
2
3
4
5
6
7
|
BOOL CClockApp::InitInstance()
{
…
m_pMainWnd->SetWindowText("博靖牌时钟 V1.0");
return TRUE;
}
|
隐藏菜单、状态栏、工具条:
1
2
3
4
5
6
7
8
9
10
|
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
…
SetMenu(NULL); //隐藏菜单
ShowControlBar(&m_wndToolBar,FALSE,FALSE); //隐藏工具条
ShowControlBar(&m_wndStatusBar,FALSE,FALSE); //隐藏状态栏
return 0;
}
|
效果:

详解:
InvalidateRect是一个函数,该函数向指定的窗体更新区域添加一个矩形,然后窗口客户区域的这一部分将被重新绘制。
ShowControlBar:
void ShowControlBar(CControlBar* pBar,BOOL bShow,Bool bDelay);
参数:
pBar 指向要显示或隐含的控件条
bShow 如果为TRUE ,指定控件条将显示;如果为FALSE,则隐藏。
bDelay 如果为TRUE,延迟显示控件条;如果为FALSE,则立即显示
说明:
调用该函数显示或隐藏一个控件条。
缺点:1.不能修改指针和线条样式(例如颜色、粗细等)。
2.窗口拉伸后,钟面不能随窗口拉伸。
改进版:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
void CClock2View::OnDraw(CDC* pDC)
{
CClock2Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
CRect r1;
GetClientRect(&r1);
CTime Time = CTime::GetCurrentTime();
//创建画笔
CPen pen(PS_SOLID, 10, RGB(139, 115, 8));
CPen *pOldPen = pDC->SelectObject(&pen);
//创建钟面
pDC->Ellipse(CRect(30, 30, r1.right-30, r1.bottom-30));
CPoint pt = r1.CenterPoint();
pDC->SetTextColor(RGB(255,0,0));
CString strNumber;
CSize size;
double Radians;
//设置钟面数字
for (int i = 1; i <= 12; i++)
{
strNumber.Format("%d",i);
Radians = (double)i*2*3.14/12.0;
size = pDC->GetTextExtent(strNumber,strNumber.GetLength());
//计算钟点放置位置
//x = CenterX - (size.cx/2) + (int)((double)(CenterX - 20) * sin(Radians));
//y = CenterY - (size.cy/2) - (int)((double)(CenterY - 20) * cos(Radians));
double x = pt.x - (size.cx/2) + (int)((double)(pt.x - 70) * sin(Radians));
double y = pt.y - (size.cy/2) - (int)((double)(pt.y - 70) * cos(Radians));
//double x = pt.x + (double)(r1.right - 50 - pt.x) * sin(Radians);
//double y = pt.y - (double)(r1.bottom - 60 - pt.y) * cos(Radians);
pDC->TextOut(x, y, strNumber);
}
//设置钟面上的格子
for (int j = 1; j <= 12; j++)
{
Radians = (double)j*2*3.14/12.0;
int x = pt.x + (int)((double)(pt.x - 30) * sin(Radians));
int y = pt.y - (int)((double)(pt.y - 30) * cos(Radians));
int m = pt.x + (int)((double)(pt.x - 50) * sin(Radians));
int n = pt.y - (int)((double)(pt.y - 50) * cos(Radians));
pDC->MoveTo(m, n);
pDC->LineTo(x, y);
}
//时针
CPen HourPen(PS_SOLID, 6, RGB(255, 20, 147));
pDC->SelectObject(&HourPen);
Radians = Time.GetHour() + Time.GetMinute()/60.0 + Time.GetSecond()/3600.0;
Radians *= 2 * 3.14 / 12.0;
pDC->MoveTo(pt.x, pt.y);
pDC->LineTo(pt.x + (int)((double)(pt.x/3) * sin(Radians)), pt.y - (int)((double)(pt.y/3) * cos(Radians)));
//分针
CPen MinPen(PS_SOLID, 4, RGB(78, 238, 148));
pDC->SelectObject(&MinPen);
Radians = Time.GetMinute() + Time.GetSecond()/60;
Radians *= 2 * 3.14 / 60.0;
pDC->MoveTo(pt.x, pt.y);
pDC->LineTo(pt.x + (int)((double)(pt.y*1/2) * sin(Radians)), pt.y - (int)((double)(pt.y*1/2) * cos(Radians)));
//秒针
CPen SecPen(PS_SOLID, 2, RGB(0, 0, 205));
pDC->SelectObject(&SecPen);
Radians = Time.GetSecond();
Radians *= 2 * 3.14 / 60.0;
pDC->MoveTo(pt.x, pt.y);
pDC->LineTo(pt.x + (int)((double)(pt.x*2/3) * sin(Radians)), pt.y - (int)((double)(pt.y*2/3) * cos(Radians)));
}
|
详解:
弧长公式:时针:α=l/r=(2πr/12)/r=2π/12 分针、秒针同理。
GetTextExtent
函数功能:使用该函数获得所选字体中指定字符串的高度和宽度
函数原型一:CSize GetTextExtent(LPCTSTR lpszString, int nCount) const;
参数:
lpszString是字符串的指针,也可以用CString 对象
nCount是指字符串的长度
函数原型二: CSize GetTextExtent( const CString& str) const;
参数:
str是一个字符串对象,包含指定的字符。
返回值:
以逻辑单位返回字符串的尺寸,保存在一个CSize对象中。
TextOut,函数名。该函数用当前选择的字体、背景颜色和正文颜色将一个字符串写到指定位置。
效果:

值得改进处:
1.每秒刷新时整个频幕闪动较大。
2.可以突出12、3、6、9的格子长度。
3.调整数字的大小和字体。
4.指针做的更美观,考虑用位图或图片替代。