第6章 指针式时钟
使用VB很容易就能设计一个具有基本功能的指针式时钟程序。但是,很多指针式时钟程序是使用Label、Shape等基本控件和Line方法来制作最简单的指针式时钟。本章要使用API函数、控件对象等方法设计一个具有圆形窗口、旋转字体、窗口可拖动和始终置前等功能的指针式时钟。
6.1 需求分析
指针式时钟不但要具有显示当前时间的基本功能,还要有时间和闹铃设置等功能,以及圆形窗体界面等风格。本章设计的指针式时钟包括以下功能。
(1)指针式时钟需要有时针、分针和秒针三根时钟指针。
(2)时钟界面为圆形,并且没有任何边框等。
(3)程序启动时,时钟的默认时间是计算机系统时间。
(4)时钟截面上需要有分钟刻度线和小时刻度线。
(5)3、6、9和12点需要有罗马数字表示时间,并且各数字需要旋转到相应的角度。
(6)可以使用鼠标将时钟拖到屏幕的任何位置。
(7)可以使用热键Esc退出时钟程序。
(8)可以设置时钟的时间。
(9)可以设置闹铃时间。
(10)可以设置时钟窗口始终置前,也可以取消时钟窗口置前,系统默认时钟窗口始终置前。图6-1为指针式时钟界面。
图6-1 指针式时钟界面
6.2 技术要点
指针式时钟具有一个圆形窗口,这需要使用API函数实现。CreateEllipticRgn函数用于在指定位置创建一个圆形区域,SetWindowRgn函数将所创建的圆形区域显示出来,而不显示该区域以外的部分。
时钟上的分钟刻度线有60根,可以使用Line方法画出这些刻度线。本章使用Line控件对象设计时钟上的这些分钟刻度线,这就要在程序运行时动态加载Line控件对象,然后设置对象变量的端点坐标即可实现刻度线绘制。
通常,拖动窗体可以通过标题栏实现,但是,本章设计的时钟程序没有标题栏,因此,可以使用Form窗体的MouseDown和MouseUp事件来实现。设计思路是,鼠标在时钟窗口中按下左键,这就确定了鼠标在时钟窗体中位置。然后鼠标移动到一个新的位置,事实上该位置还是在时钟窗体中,只是视觉上在屏幕中而已。因此,就可以计算鼠标在时钟窗体中相对移动的距离。通过设置时钟窗体的Left和Top属性,使其也移动上述的相对距离,即可实现时钟窗口的移动。
需要指出的是,时钟窗口可以移动到屏幕中的任意位置事实上是一个烟雾弹,读者千万别以为这个操作和窗体在屏幕中的位置有关系。使用鼠标拖动无边框窗体的移动,就是通过鼠标在该窗体中的移动实现的。
很多指针式时钟程序使用Label控件实现在3、6、9和12点处放置所对应的罗马数字。但是,这样放置的数字都是不能旋转的。本章使用CreateFontIndirect、SelectObject和TextOut等函数实现罗马数字的字体旋转对应的角度。
窗口始终置前,或者取消窗口始终置前,都可以通过SetWindowPos函数实现。此外,热键扫描退出程序等在前面的章节有过详细介绍。
6.3 系统结构
创建一个新的工程,添加3个Form窗体。根据表6-1所示的工程和控件对象及其属性值,向3 个Form窗体上添加相应控件,并修改相应的属性值。工程的对象及其属性值如表6-1所示。
表6-1 工程的对象及其属性值
此外,还要为主窗体添加一个菜单栏,用于实现打开时间设置和闹铃设置对话框,以及设置时钟窗口是否始终置前操作。时钟窗口的菜单栏列表如表6-2所示。
表6-2 菜单栏列表
因为该菜单主要用做弹出式菜单,所以主菜单项mnuMainMenu设置为不可见,其3 个子菜单项设置为可见。
6.4 实现过程
根据指针式时钟的功能和模块化程序设计方法,可以将程序分为API函数和变量声明、初始化程序、绘制分钟刻度线、绘制时钟指针、设置旋转罗马数字、定时程序、移动时钟窗口、菜单栏设计、时间和闹铃设置等。
6.4.1 函数和变量声明
本章设计的指针式时钟,有些功能必须使用API函数实现。此外,还需要声明当前时间、闹铃时间、时钟界面移动位置等变量。程序代码如程序清单6-1所示。
程序清单6-1函数和变量声明
1. '圆形窗体 2. Private Declare Function CreateEllipticRgn Lib"gdi32"_ 3. (ByVal x1 As Long,ByVal Y1 As Long,_ 4. ByVal x2 As Long,ByVal Y2 As Long)As Long 5. Private Declare Function CombineRgn Lib"gdi32"_ 6. (ByVal hDestRgn As Long,ByVal hSrcRgn1 As Long,_ 7. ByVal hSrcRgn2 As Long,ByVal nCombineMode As Long)As Long 8. Private Declare Function SetWindowRgn Lib"user32"_ 9. (ByVal hwnd As Long,ByVal hRgn As Long,_ 10. ByVal bRedraw As Boolean)As Long 11. Const RGN_AND=0 12. 13. '置前 14. Const SWP_NOMOVE=&H2 15. Const SWP_NOSIZE=&H1 16. Const FLAG=SWP_NOMOVE Or SWP_NOSIZE 17. Const HWND_TOPMOST=-1 18. Const HWND_NOTOPMOST=-2 19. Const HWND_TOP=0 20. Const HWND_BOTTOM=1 21. Private Declare Function SetWindowPos Lib"user32"_ 22. (ByVal hwnd As Long,ByVal hWndInsertAfter As Long,ByVal x As Long,_ 23. ByVal y As Long,ByVal cx As Long,ByVal cy As Long,_ 24. ByVal wFlags As Long)As Long 25. 26. '旋转字体 27. Private Declare Function CreateFontIndirect Lib"gdi32"Alias"CreateFontIndirectA"_ 28. (lpLogFont As LOGFONT)As Long 29. Private Declare Function SelectObject Lib"gdi32"(ByVal hdc As Long,_ 30. ByVal hObject As Long)As Long 31. Private Declare Function TextOut Lib"gdi32"Alias"TextOutA"_ 32. (ByVal hdc As Long,ByVal x As Long,_ 33. ByVal y As Long,ByVal lpString As String,_ 34. ByVal nCount As Long)As Long 35. Private Declare Function DeleteObject Lib"gdi32"(ByVal hObject As Long)As Long 36. Private Declare Function SetBkMode Lib"gdi32"(ByVal hdc As Long,_ 37. ByVal nBkMode As Long)As Long 38. 39. Private Type LOGFONT 40. lfHeight As Long 41. lfWidth As Long 42. lfEscapement As Long
43. lfOrientation As Long 44. lfWeight As Long 45. lfItalic As Byte 46. lfUnderline As Byte 47. lfStrikeOut As Byte 48. lfCharSet As Byte 49. lfOutPrecision As Byte 50. lfClipPrecision As Byte 51. lfQuality As Byte 52. lfPitchAndFamily As Byte 53. lfFaceName As String*50 54. End Type 55. 56. Private RF As LOGFONT 57. Private NewFont As Long 58. Private OldFont As Long 59. Private strHourNums(0 To 3)As String 60. 61. '捕捉热键 62. Private Declare Function GetKeyState Lib"user32"(ByVal nVirtKey As Long)As Integer 63. 64. Private intPixelX As Integer 65. Const pi=3.14159265358979 66. 67. '获得鼠标位置 68. Private lngMoveX As Integer 69. Private lngMoveY As Integer 70. Private blnFormMove As Boolean 71. 72. '当前时间 73. Public intHour As Integer 74. Public intMinute As Integer 75. Public intSecond As Integer 76. 77. '闹铃时间 78. Public intAlarmHour As Integer 79. Public intAlarmMinute As Integer
程序说明:第2~11行声明的API函数和常量主要用于实现圆形窗体。第14~24行声明的常量和API函数用于实现时钟窗口的始终置前与否。第27~59行声明的API函数和结构变量用于实现时钟窗口上4个时间数字的角度旋转。第62行声明的GetKeyState函数用于捕获热键Esc,实现退出时钟程序。第68~70行声明用于移动时钟窗口的变量。第73~75行声明用于存储和设置当前时间的变量。第78~80行声明用于存储和设置闹铃时间的变量。
6.4.2 初始化程序
在程序加载时,需要初始化所声明的变量,同时需要完成时钟窗口的设计。程序代码如程序清单6-2所示。
程序清单6-2初始化程序
1. Private Sub Form_Load() 2. Dim lngBitmap As Long 3. Dim lngElliptic As Long 4. 5. intPixelX=Screen.TwipsPerPixelX 6. lngElliptic=CreateEllipticRgn(1060/intPixelX,1450/intPixelX,_ 7. 5060/intPixelX,5450/intPixelX) 8. CombineRgn lngElliptic,lngElliptic,lngElliptic,RGN_AND 9. SetWindowRgn hwnd,lngElliptic,1 10. 11. intHour=Hour(Time) 12. intMinute=Minute(Time) 13. intSecond=Second(Time) 14. 15. strHourNums(0)="XII" 16. strHourNums(1)="III" 17. strHourNums(2)="VI" 18. strHourNums(3)="IX" 19. 20. SetBkMode Me.hdc,1 21. RF.lfHeight=20 22. RF.lfWidth=10 23. RF.lfWeight=400 24. RF.lfFaceName="Arial"+Chr(0) 25. 26. ClockScale 27. CurrentTime 28. HourFontSet 29. 30. SetWindowPos Me.hwnd,HWND_TOPMOST,0,0,0,0,FLAG 31. End Sub
程序说明:第6~9 行完成圆形窗口设计,第11~13 行获取当前系统时间,第15~18行设置3、6、9和12小时相对应的罗马数字,第20~24行设置罗马数字的字体参数。第26行调用ClockScale过程设计时钟的分针刻度线,第27行调用CurrentTime过程绘制时针、分针和秒针的位置,第28行调用HourFontSet过程设置3、6、9和12小时位置的罗马数字,第30行调用SetWindowPos函数设置默认时钟窗口始终置前。
第6~7 行对位置做了相应的调整。本章设计的圆形指针式时钟的圆心在Form窗体的(3000,3000)位置,读者也许会想,只要将该窗体的BorderStyle属性设置为0不做任何调整即可实现。因为本章需要设计一个弹出式菜单设置时间、闹铃和置前,所以即使设置了BorderStyle属性设置为0,也不能将窗体的标题栏和边框去掉,还需要做一些位置调整。
6.4.3 分钟刻度线
分钟刻度线是使用Line控件对象和For循环语句绘制的,其中对应小时的刻度线长度要稍长些,其余刻度线要稍短些。程序代码如程序清单6-3所示。
程序清单6-3分钟刻度线
1. Private Sub ClockScale() 2. Dim i As Integer 3. Dim lngScaleX1 As Long 4. Dim lngScaleY1 As Long 5. Dim lngScaleX2 As Long 6. Dim lngScaleY2 As Long 7. Dim dblAngle As Double 8. 9. Dim linScales As Line 10. 11. For i=0 To 59 12. dblAngle=pi/2-i/30*pi 13. 14. If(i Mod 5)=0 Then 15. lngScaleX1=3000+1800*Cos(dblAngle) 16. lngScaleY1=3000+1800*Sin(dblAngle) 17. Else 18. lngScaleX1=3000+1950*Cos(dblAngle) 19. lngScaleY1=3000+1950*Sin(dblAngle) 20. End If 21. lngScaleX2=3000+2000*Cos(dblAngle) 22. lngScaleY2=3000+2000*Sin(dblAngle) 23. 24. Load linClockScale(i+1) 25. Set linScales=linClockScale(i+1) 26. With linScales 27. .Visible=True 28. .x1=lngScaleX1 29. .Y1=lngScaleY1 30. .x2=lngScaleX2 31. .Y2=lngScaleY2 32. End With 33. Next 34. 35. With shpClockCenter 36. .Left=3000-50 37. .Top=3000-50 38. End With 39. End Sub
程序说明:第3~7行声明刻度线两个端点位置的变量,第12行计算每个刻度线所对应的角度,第14~20行确定小时刻度线和其余分钟刻度线的第一个端点的坐标,第24行加载Line控件对象,第25行将加载的Line控件对象赋给变量linScales。第26~32行给Line控件对象变量的端点坐标赋值,即可将Line控件设置到对应位置,实现分钟刻度线绘制,第35~38行使用Shape控件绘制时钟的圆心。
6.4.4 时钟指针
时钟指针的绘制方法与分钟刻度线的绘制方法相似。但是,时针和分针的角度设置与秒针是相关联的。事实上,秒针每转一格,分针和时针也同时转动一定的角度。因此,三根时钟指针每时每刻都是运动的。程序代码如程序清单6-4所示。
程序清单6-4时钟指针
1. Public Sub CurrentTime() 2. Dim lngScaleX1 As Long 3. Dim lngScaleY1 As Long 4. Dim lngScaleX2 As Long 5. Dim lngScaleY2 As Long 6. Dim dblAngle As Double 7. 8. intSecond=intSecond+1 9. If intSecond=60 Then 10. intMinute=intMinute+1 11. If intMinute=60 Then 12. intHour=intHour+1 13. If intHour=24 Then 14. intHour=0 15. End If 16. intMinute=0 17. End If 18. intSecond=0 19. End If 20. 21. lngScaleX1=3000 22. lngScaleY1=3000 23. 24. dblAngle=-pi/2+intHour/6*pi+intMinute/30*pi/60_ 25. +intSecond/30*pi/3600 26. lngScaleX2=3000+1000*Cos(dblAngle) 27. lngScaleY2=3000+1000*Sin(dblAngle) 28. With linHour 29. .x1=lngScaleX1 30. .Y1=lngScaleY1 31. .x2=lngScaleX2 32. .Y2=lngScaleY2 33. End With 34. 35. dblAngle=-pi/2+intMinute/30*pi+intSecond/30*pi/60 36. lngScaleX2=3000+1200*Cos(dblAngle) 37. lngScaleY2=3000+1200*Sin(dblAngle) 38. With linMinute 39. .x1=lngScaleX1 40. .Y1=lngScaleY1 41. .x2=lngScaleX2 42. .Y2=lngScaleY2 43. End With
44. 45. dblAngle=-pi/2+intSecond/30*pi 46. lngScaleX2=3000+1400*Cos(dblAngle) 47. lngScaleY2=3000+1400*Sin(dblAngle) 48. With linSecond 49. .DrawMode=13 50. .x1=lngScaleX1 51. .Y1=lngScaleY1 52. .x2=lngScaleX2 53. .Y2=lngScaleY2 54. End With 55. End Sub
程序说明:第8~19行是根据小时、分钟和秒钟的进制更新当前的时分秒数据。第21~22行确定时钟指针的第1个端点就是时钟圆心位置。第24~25行是计算时针所对应的角度,需要使用当前时分秒3个时间数据关联计算。第26~27行计算时钟指针的第2个端点位置。第28~33行给Line控件变量的端点坐标赋值,实现时针的绘制。第35~43和第45~54行分别为分针和秒针的绘制,其绘制方法与时针相似。
细心的读者可能会发现,本段程序是一个Public过程,这是为了便于时间设置窗体中的程序调用。因为在时间设置窗口中,用户设置完时间并确定后,需要立即将时钟指针切换到所设置的时间上,因此可以调用本段代码,简化程序。
6.4.5 罗马数字设置
通常,指针式时钟在3、6、9和12小时处都有罗马数字表示时间,而且6和9小时对应的数字通常是旋转相应的角度,本段程序要实现罗马数字在时钟窗口上的旋转显示。程序代码如程序清单6-5所示。
程序清单6-5罗马数字设置
1. Private Sub HourFontSet() 2. Dim i As Integer 3. Dim dblAngle As Double 4. Dim lngScaleX As Long 5. Dim lngScaleY As Long 6. 7. i=1 8. dblAngle=-pi+i/2*pi 9. RF.lfEscapement=0 10. NewFont=CreateFontIndirect(RF) 11. OldFont=SelectObject(Me.hdc,NewFont) 12. lngScaleX=(3000+1800*Cos(dblAngle-pi/25))/intPixelX 13. lngScaleY=(3000+1800*Sin(dblAngle-pi/25))/intPixelX 14. TextOut Me.hdc,lngScaleX,lngScaleY,strHourNums(i-1),_ 15. Len(strHourNums(i-1)) 16. NewFont=SelectObject(Me.hdc,OldFont) 17. DeleteObject NewFont 18. 19. For i=2 To 4
20. dblAngle=-pi+i/2*pi 21. RF.lfEscapement=(pi/2-dblAngle)/pi*1800 22. NewFont=CreateFontIndirect(RF) 23. OldFont=SelectObject(Me.hdc,NewFont) 24. lngScaleX=(3000+1500*Cos(dblAngle+pi/30))/intPixelX 25. lngScaleY=(3000+1500*Sin(dblAngle+pi/30))/intPixelX 26. TextOut Me.hdc,lngScaleX,lngScaleY,strHourNums(i-1),_ 27. Len(strHourNums(i-1)) 28. NewFont=SelectObject(Me.hdc,OldFont) 29. DeleteObject NewFont 30. Next 31. End Sub
程序说明:第7~17行单独显示12小时处的罗马数字,因为该处的数字通常不旋转角度,第19~30行使用For循环语句旋转显示3、6和9处的罗马数字。本段程序中,RF.lfEscapement变量用于设置字体的角度,TextOut函数用于在窗体上相应位置显示时间数字。
6.4.6 定时程序
定时程序主要完成3项功能。其一,每秒更新当前时间以及时钟指针;其二,判断闹铃时间是否已到,如果到了闹铃时间,就发出闹铃提示声;其三,扫描热键Esc以便退出时钟程序。程序代码如程序清单6-6所示。
程序清单6-6定时程序
1. Private Sub tmrEscape_Timer() 2. Dim intKeyIn As Integer 3. 4. intKeyIn=GetKeyState(vbKeyEscape)And&H8000 5. If intKeyIn=&H8000 Then 6. tmrEscape.Interval=0 7. End 8. End If 9. End Sub 10. 11. Private Sub tmrTime_Timer() 12. CurrentTime 13. 14. If intHour=intAlarmHour And intMinute=intAlarmMinute Then 15. Beep 16. End If 17. End Sub
程序说明:第1~9行扫描热键Esc,扫描的时间间隔是0.1秒,第12行是调用CurrentTime函数更新当前时间和时钟指针,第14~16行判断闹铃时间,到了闹铃时间就发出蜂鸣提示声1分钟。
本段程序将更新指针和判断闹铃使用同一个时钟的Timer事件实现,而将扫描热键使用另一个时钟实现。原因是扫描热键需要较快的扫描频率,而更新指针和判断闹铃只要1秒钟一次就足够了。如果1秒钟扫描一次热键,那么很有可能会遗漏热键被按下的动作,而不能响应用户的操作。
6.4.7 移动时钟窗口
本章设计的指针式时钟可以使用鼠标拖动到屏幕的任何位置。在Form窗体的MouseDown事件中确定当前的鼠标的位置,在MouseUp事件中通过计算鼠标移动的相对位置,来设置窗体的Left和Top属性即可。程序代码如程序清单6-7所示。
程序清单6-7移动时钟窗口
1. Private Sub Form_MouseDown(Button As Integer,Shift As Integer,_ 2. x As Single,y As Single) 3. If Button=1 Then 4. blnFormMove=True 5. lngMoveX=x 6. lngMoveY=y 7. Else 8. PopupMenu mnuMainMenu 9. End If 10. End Sub 11. 12. Private Sub Form_MouseUp(Button As Integer,Shift As Integer,x As Single,y As Single) 13. If blnFormMove=True Then 14. Me.Left=Me.Left-(lngMoveX-x) 15. Me.Top=Me.Top-(lngMoveY-y) 16. blnFormMove=False 17. End If 18. End Sub
程序说明:MouseDown事件中需要完成两个操作。如果单击左键,就要通过第4~6行将鼠标的位置保存下来,以便移动时钟窗口时使用,如果单击右键,就要通过第8行弹出活动菜单。第14~15行计算鼠标的相对移动量,然后通过窗体的Left和Top属性加上这一相对移动量即可实现时钟窗口的移动。
6.4.8 菜单栏
菜单栏的主要功能是实现时间设置、闹铃设置和时钟窗口是否始终置前的设置。程序代码如程序清单6-8所示。
程序清单6-8菜单栏
1. Private Sub mnuTimeSet_Click() 2. frmTimeSet.Show,Me 3. End Sub 4. 5. Private Sub mnuAlarmSet_Click() 6. frmAlarmSet.Show,Me 7. End Sub 8. 9. Private Sub mnuHeadPosition_Click()
10. If mnuHeadPosition.Caption="√置前"Then 11. mnuHeadPosition.Caption=" 置前" 12. SetWindowPos Me.hwnd,HWND_NOTOPMOST,0,0,0,0,FLAG 13. Else 14. mnuHeadPosition.Caption="√置前" 15. SetWindowPos Me.hwnd,HWND_TOPMOST,0,0,0,0,FLAG 16. End If 17. End Sub
程序说明:第1~3和第5~7行分别是时间和闹铃设置,主要通过打开对应的设置窗口进行设置,图6-2为时间设置界面和图6-3为闹铃设置界面。时间和闹铃设置程序比较简单,读者可以参考光盘中的源程序。第12行使用SetWindowPos函数设置窗口不置前,第15行设置窗口置前。
图6-2 时间设置界面
图6-3 闹铃设置界面