Visual Basic项目开发案例精粹
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第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 闹铃设置界面