アナログ時計

VB SampleVB-Clock
 VBでシンプルなアナログ時計をつくります。概観は右図の通りで、GDI+を使って、フォームに直接描画を行います。筆者の拙いデザインはともかく、GDI+の描画は非常にきれいです。
 なお、コード量はさほど多いわけではありませんが、若干三角関数の知識が必要となります。
 [ INDEX ]
 ・フォームの設計
 ・初期化処理
 ・時計盤の描画
 ・時計針の表示
 ・参考 : HatchStyle 列挙体
 (作成環境 : Visual Basic .net 2002 / Framework SDK 1.0)

●フォームの設計
 MainFormをつくり、タイマーコントロールを一個配置するだけです。フォームとコントロールのプロパティ値で主なものは以下の通りです。

MainForm(Form)
... MaximizeBox : False
... MinimizeBox : False
... Size : 176, 200
... FormBorderStyle : FixedSingle
... Text : VBClock

TIM1(Timer)
... Interval : 500
... Enable : False

 今回作成する関数は、以下の5つです。
関数名種類機能
OnLoadイベントフォームのロード時にバッファとタイマーの設定を行う
OnClosingイベントフォームのアンロード時に終了処理を行う
OnPaintイベント時刻に合わせて時計針を動かす
TIM1_Tickイベント指定した間隔で再描画を行う
SetBackgroundユーザー定義背景と文字盤を描画する
| ▲TOP |

●初期化処理
 時計盤の位置とサイズを定数として定義します。nCenterX/nCenterY は、時計盤の中心座標です。また、nRadius は時計盤の半径を表します。必須ではありませんが、円周率も定数化しています。
 初期化は OnLoad イベント関数で行います。なお、細かい間隔で再描画を行う際には、画面がちらつく可能性があります。ダブルバッファリングを設定しておくと、一旦メモリ上に描画してから一気に画面に描画を行いますので、ある程度ちらつきを抑えることができます。あとは、背景画像(時計盤)を描画して、タイマーを動かす手順となります。
' フォームデザイナのコードは省略しています

' 定数
Const nCenterX As Integer = 84    ' 時計盤の中心のX座標
Const nCenterY As Integer = 84    ' 時計盤の中心のY座標
Const nRadius As Integer = 76       ' 時計盤の半径
Const fPi As Single = 3.1416          ' 円周率の値

' フォームのロードイベント
Protected Overrides Sub OnLoad(ByVal e As EventArgs)
    'ダブルバッファリングの設定
    Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
    Me.SetStyle(ControlStyles.UserPaint, True)
    Me.SetStyle(ControlStyles.DoubleBuffer, True)

    ' 背景画像の設定
    SetBackground()

   ' タイマーの設定
    TIM1.Interval = 500
    TIM1.Enabled = True
End Sub

' フォームのアンロードイベント
Protected Overrides Sub OnClosing(ByVal e As System.ComponentModel.CancelEventArgs)
    TIM1.Enabled = False
    Application.Exit()
End Sub

●時計盤の描画
 文字盤を描く際は三角関数の初歩的な知識が必要となります(右図参照)。この時、用いる単位はラジアン値となります。これは点の円周上での移動距離を元にした単位で、1周、つまり 360度 = 2πラジアン となります。度数をラジアンに直す時は、" 1度 = (π/180) ラジアン " となりますので、「度数 x π / 180」とすれば求められます。逆にラジアンを度数に直す時は、" 1ラジアン = (180/π) 度 " ですので、「ラジアン x 180 / π」となります。(下記 "数字と点の描画" を参照)
 描画に当たっては、まずイメージオブジェクトを作成して描画を行い、描画終了後に、フォームの背景に設定しています。この時、まず、三角関数を用いて30度毎の座標を確定し、続いて、再び三角関数で数字や点の描画サイズから調整値を算出して、これを座標から引いています(for文の箇所)。
 なお、下記のサンプル中、SmoothingMode ではアンチエイリアス処理を指定しています。これがないと線にギザギザが出てしまいます。さらに、文字にアンチエイリアスをかけるには、TextRenderingHint を利用します。
 GDI+の基本的な描画メソッドをあげておきます。
DrawLine直線を描画 DrawRectangle四角形(線)を描画
DrawEllipse楕円(線)を描画 DrawArc円弧(線)を描画
DrawPie扇形(線)を描画 FillRectangle四角形(塗りつぶし)を描画
FillEllipse楕円(塗りつぶし)を描画 FillPie扇形(塗りつぶし)を描画
DrawString文字列の描画
' 背景画像の設定
Private Sub SetBackground()
    Dim m As New Bitmap(Me.ClientSize.Width, Me.ClientSize.Height) ' イメージ
    Dim g As Graphics = Graphics.FromImage(m)           ' グラフィックオブジェクト

    ' 全体の設定
    g.SmoothingMode = Drawing.Drawing2D.SmoothingMode.AntiAlias ' 線のアンチエイリアス
    g.TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias ' 文字のアンチエイリアス
    g.Clear(SystemColors.Control)     ' 全体の背景色

    ' 文字盤の背景の描画
    Dim brsBack As New Drawing2D.HatchBrush( _
        Drawing.Drawing2D.HatchStyle.HorizontalBrick, Color.Wheat, Color.WhiteSmoke)
    g.FillEllipse(brsBack, nCenterX-nRadius, nCenterY-nRadius, nRadius*2, nRadius*2)
    brsBack.Dispose()

    '外周の円の描画
    Dim penRing As New Pen(Color.Peru, 3)
    penRing.Alignment = Drawing.Drawing2D.PenAlignment.Outset  ' 円周の外側に描画
    g.DrawEllipse(penRing, nCenterX-nRadius, nCenterY-nRadius, nRadius*2, nRadius*2)
    penRing.Dispose()

    ' 時を表す数字と点の描画
    Dim oFontFamily As New FontFamily("Times")          ' 数字部分のフォントファミリー
    Dim oFont As New Font(oFontFamily, 24, FontStyle.Bold, GraphicsUnit.Pixel)  ' フォント
    Dim fDescent As Single = CSng(oFont.Size _
            * oFontFamily.GetCellDescent(FontStyle.Bold) _
            / oFontFamily.GetEmHeight(FontStyle.Bold))    ' フォントのディセント部分の高さ
    Dim fDotDiam As Single = nRadius / 10                    ' 時間の点の直径
    Dim i As Integer
    For i = 1 To 12
        Dim fRad As Double = (90 - i * 30) / 180 * fPi         ' ラジアン値(30度毎)
        Dim X As Single = nCenterX + CSng(Math.Cos(fRad) * nRadius)   ' X座標
        Dim Y As Single = nCenterY - CSng(Math.Sin(fRad) * nRadius)    ' Y座標
        fRad = (180 - i * 30) / 180 * fPi                             ' ラジアン値(調整分)
        If i Mod 3 = 0 Then
            ' 3, 6, 9, 12時は数字を描画
            Dim szNum As SizeF = g.MeasureString(i.ToString, oFont)       ' 描画サイズ
            X -= szNum.Width * CSng((Math.Sin(fRad) + 1) / 2)
            Y -= (szNum.Height - fDescent) * CSng((Math.Cos(fRad) + 1) / 2)
            g.DrawString(i.ToString, oFont, Brushes.Navy, X, Y)
        Else
            ' 3, 6, 9, 12時以外は点を描画
            X -= fDotDiam * CSng((Math.Sin(fRad) + 1) / 2)
            Y -= fDotDiam * CSng((Math.Cos(fRad) + 1) / 2)
            g.FillEllipse(Brushes.RoyalBlue, X, Y, fDotDiam, fDotDiam)
         End If
    Next
    oFont.Dispose()
    oFontFamily.Dispose()

    Me.BackgroundImage = m    ' 描画したイメージを背景に設定
    g.Dispose()                         ' リソースの解放
End Sub
  上記中、MesureString は、文字を描画した時のサイズを取得します。また、数字の描画で fDescent は、数字では使われないフォントの下部(g や j などの文字で下にはみ出す部分)の高さで、12時位置以外の描画では、これが下の余白となってしまうので、計算して取り除いています。この時、使われている単位はデザイン単位の em となりますので、割合を利用して適当な単位に変換する必要があります。
 なお、HatchStyle の模様については、本ページ末のサンプルも参考にしてください。また、色の名称に関しては 「カラーキーワード」(名前付きカラー) のページも参考にしてください。 Color.FromArgb を使用するとより細かな色の設定ができます。
 ちなみに、上記のサンプルで、点の代わりにすべて数字を描画することも可能です(右図)。また、数字や点の位置を特定する方法は一通りではありませんので、上記の式にこだわる必要はありません。もっとすっきりと算出できるかもしれません。

| ▲TOP |

●時計針の表示
 OnPaint イベントは、タイマーが有効になると、指定間隔ごとに Tick イベントが発生し、Tick 関数の中から呼び出されて描画を実行します。ここでは、短針(時間)、長針(分)、秒針の順で描画をしていますが、それぞれ、円周上の位置を特定させるために、三角関数を使用しています。
' フォームの描画イベント
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
    Dim g As Graphics = e.Graphics                ' グラフィックオブジェクト
    Dim penHour As New Pen(Color.Blue, 7)    ' 短針用のペン
    Dim penMin As New Pen(Color.Blue, 5)      ' 長針用のペン
    Dim penSec As New Pen(Color.Red, 3)      ' 秒針用のペン
    Dim fRad As Double                   ' ラジアンの値
    Dim nHand As Single                 ' 針の長さ
    Dim X, Y As Single                    ' X座標、Y座標

    ' アンチエイリアス処理(線のギザギザをなくす)を指定
    g.SmoothingMode = Drawing.Drawing2D.SmoothingMode.AntiAlias

    ' 短針の描画
    nHand = nRadius * 0.6                ' 長さ=半径の60%
    fRad = (90 - (Now.Hour * 30 + Now.Minute * 30 / 60)) / 180 * fPi
    X = CSng(Math.Cos(fRad) * nHand)
    Y = CSng(Math.Sin(fRad) * nHand)
    penHour.StartCap = Drawing.Drawing2D.LineCap.Round    ' 針先を丸くする
    penHour.EndCap = Drawing.Drawing2D.LineCap.Round
    g.DrawLine(penHour, nCenterX, nCenterY, nCenterX + X, nCenterY - Y)

    ' 長針の描画
    nHand = nRadius * 0.8                ' 長さ=半径の80%
    fRad = (90 - Now.Minute * 6) / 180 * fPi
    X = CSng(Math.Cos(fRad) * nHand)
    Y = CSng(Math.Sin(fRad) * nHand)
    penMin.StartCap = Drawing.Drawing2D.LineCap.Round    ' 針先を丸くする
    penMin.EndCap = Drawing.Drawing2D.LineCap.Round
    g.DrawLine(penMin, nCenterX, nCenterY, nCenterX + X, nCenterY - Y)

    ' 秒針の描画
    nHand = nRadius * 0.9                ' 長さ=半径の90%
    fRad = (90 - Now.Second * 6) / 180 * fPi
    X = CSng(Math.Cos(fRad) * nHand)
    Y = CSng(Math.Sin(fRad) * nHand)
    g.DrawLine(penSec, nCenterX, nCenterY, nCenterX + X, nCenterY - Y)

    ' リソースの解放
    penSec.Dispose()
    penMin.Dispose()
    penHour.Dispose()
End Sub

' タイマー・イベント Private Sub TIM1_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles TIM1.Tick ' 再描画 Me.Invalidate() End Sub
| ▲TOP |

* 参考 : HatchStyle 列挙体

(ForeColor = Color.Black / BackColor = Color.White での描画結果)

| ■HOME | ◆プログラムTop | ▲ページの先頭 |