2020/06/18

図形で作るアナログ時計




1.背景

Excel上で日程管理をする際、日付や時刻をデジタルで入力・選択することが多いと思います。
デジタル入力では無い視覚的な方法としては、カレンダーを表示させて日付を選択させたりするものは良く見掛けますが、時刻を視覚的に設定する手段はあまり見かけない と昔から思っていました。
イメージとしては「アナログ時計を表示させ、針をクリックして希望の時刻のところまで回し、離すと時刻が設定される」ようなものを思い浮かべ、色々と調べてみました。
しかし、クリックして動かしている最中のイベント取得の難しさ、また(時計の針の長さを一定に保ったまま)針の動きにマウスを連動させる難しさがあり、未だに実現できていません。

で、とりあえず時計の形を作ったところまでを、今回紹介いたします。
時計が動いているだけですので、もしかしたら何の役にも立たないかもしれません。「アプリのかけら」と思って了承願います。


2.概要

図2-1の「時計」ボタンを押すと、アナログの時計が表示されます。

図2-1

時計は図形で描画しており全ての部品をグループ化していますので、図形枠をクリックすることでシート上を動かす事ができます。(図形サイズを変形させても動き続けますが、表示は崩れます。)
時計を削除する時は、もう一度「時計」ボタンを押します。


3.プログラム

プログラムは、全て標準モジュールに記述しています。

3-1.変数の宣言(標準モジュールの先頭)

標準モジュールの先頭に、モジュール全体で使用する変数を宣言しています。
  1. '========== ⇩① 変数の宣言 ==============
  2. Dim Sp As Shape      '時計円のオブジェクト
  3. Dim Lhand As Shape    '長針のオブジェクト
  4. Dim Shand As Shape    '短針のオブジェクト
  5. Dim SEChand As Shape   '秒針のオブジェクト
  6. Dim Gshape As Shape   '時計の各部をGroup化したオブジェクト
  7. Dim C_Left As Double   '時計の位置(左端)
  8. Dim C_Top As Double   '時計の位置(上端)
  9. Dim Rad As Double    '時計の半径
  10. Dim RadL As Double    '長針の長さ
  11. Dim RadS As Double    '短針の長さ
  12. Dim RadSEC As Double   '秒針の長さ
  13. Dim PosL As Integer    '長針の位置
  14. Dim PosS As Integer    '短針の位置
  15. Dim PosSEC As Integer   '秒針の位置
図3-1

2~6行目は、時計の各部品(それぞれがShapeオブジェクト)の宣言です。どの部分を指すかを図3-2の左図に示しました。尚オブジェクトは名は、各部品のプロパティの設定・取得、部品のグループ化に使用します。
8~13行目は、時計の位置・寸法関係の変数の宣言です。どの部分を指すのかを、図3-2の右図に示しました。

図3-2

オブジェクト関係は以下の通りです。
Sp   : 時計円を指します。モジュール変数としている理由は、時計を移動した時の時計の位置を追いかける為です。
Lhand  : 長針を指します。
Shand  : 短針を指します。
SEChand : 秒針を指します。
Gshape : 時計の各部をGroup化したものを指します。上記オブジェクトに加え、1~12の文字オブジェクトを含みます。

時計の位置・寸法関係は以下の通りです。
C_Left : Excelのワークシート左端から時計円(左端)までの距離です。
C_Top : Excelのワークシート上端から時計円(上端)までの距離です。
Rad  : 時計の半径を指します。
RadL  : 長針の長さを指します。円中心から針先までの距離です。太さは2で固定しています。
RadS  : 短針の長さを指します。円中心から針先までの距離です。太さは4で固定しています。
RadSEC : 秒針の長さを指します。円中心から針先までの距離です。
円の位置は、時計を移動させる際に変化しますので定数では無く変数で持つ必要があります。一方針の長さは定数でも良かったのですが、当初時計サイズの変更も考えていた為に変数にしています。

15~17行目の変数の目的は、処理速度向上です。
各針がどの場所にいるか(図3-3)で針の描画指示内容が異なるのですが、同じ位置であれば省略できる指示内容もあります。よって「針の現在位置を保持する」ために変数に位置を代入しています。詳細は図3-11の部分で説明します。

図3-3


3-2.時計部品の作成

図3-4が時計の外観を作る部分です。
  1. '========== ⇩② 時計の作成 ==============
  2. Public Sub start()
  3.  Dim Lb(1 To 12) As Shape
  4.  Dim SpName(1 To 16) As String
  5.  Dim j As Single
  6.  Dim i As Integer
  7.  C_Left = 50
  8.  C_Top = 50
  9.  Rad = 100
  10.  RadL = 85
  11.  RadS = 60
  12.  RadSEC = 80
  13.  If Not Gshape Is Nothing Then
  14.   Gshape.Delete
  15.   Set Gshape = Nothing
  16.   End
  17.  End If
  18.  Widh ActiveSheet.Shapes
  19.    '==========時計円作成==============
  20.   Set Sp = .AddShape(msoShapeOval, C_Left, C_Top, Rad * 2, Rad * 2)
  21.   Sp.Fill.ForeColor.RGB = RGB(178, 63, 201)
  22.   Sp.Fill.Transparency = 0.5
  23.    '==========1~12の文字作成==============
  24.   j = 3.14 / 6
  25.   For i = 1 To 12
  26.    Set Lb(i) = .AddLabel (msoTextOrientationHorizontal, _
  27.           C_Left + Sin(j) * Rad * 0.9 + Rad - 11, C_Top - Cos(j) * Rad * 0.9 + Rad - 11, 30, 30)
  28.    Lb(i).TextFrame.Characters.Text = i
  29.    j = j + 3.14 / 6
  30.   Next i
  31.    '==========長針の作成==============
  32.   Set Lhand = .AddConnector(msoConnectorStraight, C_Left + Rad, C_Top + Rad, C_Left + Rad, C_Top + Rad - RadL)
  33.   Lhand.Line.Weight = 2
  34.    '==========短針の作成==============
  35.   Set Shand = .AddConnector(msoConnectorStraight, C_Left + Rad, C_Top + Rad, C_Left + Rad, C_Top + Rad - RadS)
  36.   Shand.Line.Weight = 6
  37.    '==========秒針の作成==============
  38.   Set SEChand = .AddConnector(msoConnectorStraight, C_Left + Rad, C_Top + Rad, C_Left + Rad, C_Top + Rad - RadSEC)
  39.  End With
  40.   '==========時計の各部をグループ化==============
  41.  For i = 1 To 12
  42.   SpName(i) = Lb(i).Name
  43.  Next i
  44.   SpName(13) = Sp.Name
  45.   SpName(14) = Lhand.Name
  46.   SpName(15) = Shand.Name
  47.   SpName(16) = SEChand.Name
  48.  Set Gshape = Sheet1.Shapes.Range(SpName).Group
  49.   '==========針を稼働==============
  50.  Call myTime
  51. End Sub
図3-4

19行目の「start」プロシージャは、今回はSheet1の「時計」ボタンから呼び出されるようにしています。
もしアドインして自動起動する場合にはWorkbook_Openイベント等に登録して下さい。

20~23行目は、このプロシージャ内で使われる変数・配列の宣言です。
20行目のLb配列(Labelの意味)は、時計の1~12の文字のオブジェクトを入れるものです。
21行目のSpName配列(Shape Nameの意味)は、時計の各部品(Shape)の名前を入れる入れ物です。時計をグループ化する際に使用します。
22行目は、時計の1~12の文字を文字盤に振り分ける為の角度になります。単位はラジアンですので最大360度=2 x 3.14となり、小数点が必要ですのでSingle型にしています。
23行目はカウンタ変数です。

25~30行目は、図3-1で宣言したモジュール変数に値を代入しています。
針の長さ(28~30行目のRadL、RadS、RadSEC )は、時計円(Rad )に対する割合で指定(例:RadL= Rad * 0.85 等)するのも分かり易いかもしれません。

32~36行目は、「時計が表示されている時に、もう一度ボタンを押された時」に実行される部分です。
時計の外観を作成後、図3-4の73行目で各部品をグループ化してオブジェクト名「Gshape」を付けます。したがって「時計が存在している=Gshapeオブジェクトが生きている」ことになりますので、32行目の「 Not Gshape Is Nothing 」がTrueの時に33~35行目が実行されます。
33行目は時計を削除し、34行目でオブジェクトを解放します。
目的が時計の削除ですので、オブジェクト解放後はマクロを終了させるために、35行目で「End」ステートメントを実行します。

38~63行目では、時計を作っています。
Shapeを作るには「ActiveSheet.Shapes.AddShape(種類, 左端からの距離, 上端からの距離, 幅, 高さ) 」等という指定をします。(図形はAddShapeを使いますが、文字はAddLabel、直線はAddConnectrとなります。)
この内「ActiveSheet.Shapes」オブジェクトは共通ですので、38行目ではWithステートメントで括っています。

40~42行目は、時計の円を作成しています。図3-2の右図の寸法でAddShapeの引数を設定しています。
41行目はベースの色を、42行目では透明度を設定しています。(この部分は個人の趣味です)

45~51行目は、文字盤の上に1~12の文字を乗せています。

Excel では、三角関数Sin,Cos,Tanの引数である角度には、degreeではなくπを指定します(360 度 = 2π )。この場合、円周率3.1415・・・が必要になりますが、VBAでの取得方法の主なものは以下になります。
 1)定数として 3.14159265358979 を宣言する。(Single型では 3.141593 )
 2)Application.WorksheetFunction.Pi として関数を呼び出す。
 3)Atn(1) * 4 で45度 x 4 = 180度 = π を計算する。

上記の3)は、アークタンジェント(tan-1)を使う方法です。図3-5の様に逆三角関数を使用すると角度が得られることを使っています。

図3-5

一番右の三角形のように「直角を頂点とする二等辺三角形で、残りの角の角度はそれぞれ45度」になります。Excelの式で書くと「Atn(b/a) 」で、a = b ですから「Atn(1)」となる訳です。その得られる角度は45度で、4倍で180度。2π=360度ですので、Atn(1) x 4 = 180度 = π = 3.1415・・・・ となる仕組みです。

今回、円周率は図形を配置するだけですので、3.14という大雑把な値を使用しています。(45行目・50行目)

文字盤の文字は、1文字ずつ46~51行目のFor~Nextで回して作成します。
回す対象は描画する数字ですが、その位置を示す「描画角度」も必要です。そのため45行目で「360度 ÷ 12時間 」=「 2 x π ÷ 12」=「π ÷ 6」=「3.14 ÷ 6」を設定し、50行目でその角度を増やしていきます。
なお、このj変数の代わりに「( 3.14 / 6 ) * i 」を使ってももちろんOKです。(上記の「π = 3.1415」の説明を分かり易くするために、わざとコードを分けています。)

47~48行目は描画位置を計算してテキスト枠を作成し、作られたテキスト枠に49行目で数字(1~12)を記入します。
描画位置の計算ですが、図3-6で説明します。

図3-6

Addlabelの引数としては、Addlabel(文字の方向, 左端からの位置, 上端からの位置, ラベルの幅, ラベルの高さ)を指定します。
なお、文字を描画するのは、時計円よりも少し内側でないとおかしいので、「Rad x 0.9」の半径の場所にしました。図3-6では一点鎖線で引いてある円です。

まず「文字方向」は今回横書きにしますので「msoTextOrientationHorizontal」を選択します。尚、縦書きにする際には「msoTextOrientationVertical」を選びます。
「左端からの位置」ですが、図3-6で分かる通り「C_Left + Rad 」で時計中心を指すことになりますので、その中心から左右方向に「Sin(文字の角度j) x Rad x 0.9」を足した位置となります。

左端からの位置、及び上端からの位置の最後についている「-11」について説明します。まずは左右方向「C_Left + Sin(j) * Rad * 0.9 + Rad 」、上下方向「C_Top - Cos(j) * Rad * 0.9 + Rad 」が指す位置は、文字を入れるテキスト枠の左上角になります。
時計の針が指すのは「文字の中心」の方が良いですよね。そこで文字位置を針の延長線上に持ってくるために、文字位置の補正を行います。
テキスト枠は、凡そ文字のFontサイズの2倍の大きさがあります。Excelの初期設定ではFontサイズは11ポイント位のため、「11ポイント x 2倍 ÷ 2(中心に持ってくるため) 」=11ポイントをズラす量にしています。
もう少し科学的な方法については後で説明します。

「上端からの位置」についても、「C_Top + Rad」で時計中心にし、その中心から上下方向に「Cos(文字の角度j) x Rad x 0.9」を引いた位置が、テキスト枠の左上角になります。
「ラベルの幅」ですが、30ポイントとしてあります。Fontサイズの2倍位を取っています。この値が小さいと文字が折り返し2段表示になってしまうので注意が必要です。
「ラベルの高さ」も幅と同じく30にしてありますが、どうも自動調整するようなのでどんな値でもOKそうです。

テキスト枠が出来ましたら、作ったテキスト枠のオブジェクトに49行目で値を入れます。
なお、テキスト枠作成の47~48行目をオブジェクトとしてLb()配列に代入していますが、これは67~74行目で時計の各部品をグループ化するためです。
もしグループ化などをする必要が無いのでしたら、47行目でオブジェクト変数に入れずに47~49行目を以下の様に1つの式にする事も出来ます。
「.AddLabel (msoTextOrientationHorizontal, C_Left + Sin(j) * Rad * 0.9 + Rad - 11, C_Top - Cos(j) * Rad * 0.9 + Rad - 11, 30, 30).TextFrame.Characters.Text = i 」

さて、テキスト枠の位置修正について、図3-7で少しだけ科学的に説明します。
(どのサイトにも記載が無いので、本当に合っているのかは不明ですが、値を入れて試してみると凡そ合っている様です。)

図3-7

「文字位置の補正」のところで説明した通り、テキスト枠は左上角が基準点となり、上端からの距離がTop、左端からがLeftになります。
枠の大きさは「Fontサイズの約2倍」と説明しましたが、詳細は図3-7の様になっているようです。
テキスト枠の回りには各Marginがあり、文字がある部分が浮いている様な状態のようです。
テキスト枠全体の高さは、テキスト枠オブジェクトのHeightプロパティで取得でき、また文字列はテキスト枠のほぼ中心に位置しますので、上下方向の補正値としては「Height / 2」とすれば良さそうです。(文字中心=Font.Size中心 では無いようなので、微妙なズレはあります)
一方左右方向については、隙間として「TextFrame.MarginLeft」があってから文字が始まる様です。また文字の中心と言っても「1」と「12」では全体の横幅が異なるので工夫が必要です。
以上を考慮すると、45~51行目の文字を描画するコードは図3-8のように変わります。
  1. '========== ⇩③ より正確に文字盤に文字を描画する方法 ============
  2. With ActiveSheet.Shapes
  3.  j = 3.14 / 6
  4.  For i = 1 To 12
  5.   Set Lb(i) = .AddLabel(msoTextOrientationHorizontal, _
  6.          C_Left + Sin(j) * Rad * 0.9 + Rad, C_Top - Cos(j) * Rad * 0.9 + Rad, 50, 50)
  7.   With Lb(i)
  8.    .TextFrame.Characters.Text = i
  9.    TextFrame.Characters.Font.Size = 20
  10.    .IncrementLeft  -.TextFrame.MarginLeft - .DrawingObject.Font.Size / (4 / Len(.TextFrame.Characters.Text))
  11.    .IncrementTop  -.Height / 2
  12.   End With
  13.   j = j + 3.14 / 6
  14.  Next i
  15. End With
図3-8

まず図3-8の83~84行目は、テキスト枠の左上角に合わせた位置に仮置きします。また、試しでFontサイズを20ポイントにしているため、文字幅・文字高さは20ポイントが入るだけの大きさ(2倍以上が目処)にしてあります。
83~84行目の段階で位置補正をしない理由は、「文字を書き込んでみないと、テキスト枠の各サイズが分からない」為です。

次に86行目で数字を描画し、87行目でFontサイズを定めます。
ここまでの作業でテキスト枠の各サイズが決まりますので、その値(プロパティ)を使い、テキスト枠を移動する処理をします。つまり「IncrementLeft」で左右方向に移動させ、「IncrementTop」で上下方向に移動させます。原点(ワークシートの左上角)から離れていく方向が+になります。

左右方向の修正は、まず「TextFrame.MarginLeft」分だけ原点に近づけ、その後で描画されている文字列の中央部(文字列幅/2)まで更に原点に近づけます。
文字列の幅は、半角文字であれば高さの半分とみれば凡そ合うと思いますので、「1文字であれば、Font.Size/4」「2文字であれば、Font.Size/2」となります。これを式にすると、以下のようになります。
「.DrawingObject.Font.Size / (4 / Len(.TextFrame.Characters.Text))」
分母は、文字数を数えて「1文字なら4」「2文字なら2」になるようにしています。

また上下方向の修正は、「Height」の半分の寸法をに原点に近づけることで、文字列のほぼ中央になります。


次に、54~55行目で長針の作成、58~59行目で短針の作成、62行目で秒針の作成をしています。
単純に時計円の中心を基準にし、針の長さ分だけの直線を引き、あとは太さで各針の役目を強調させています。

図3-9

直線を描画するには、以下のように記述します。
ActiveSheet.Shapes.AddConnector (msoConnectorStraight,始点の水平位置,始点の垂直位置,終点の水平位置,終点の垂直位置)
なお、第一引数には、直線であれば「msoConnectorStraight」を指定しますが、他にも曲線(msoConnectorCurve)、カギ形(msoConnectorElbow)が選べます。
曲線・カギ形ともに、針の先端はちゃんとした時刻を指し示すようなので、試してみると楽しい時計になります。

まず始点ですが、時計円の中心は、水平方向では「C_Left + Rad」、垂直方向は「C_Top + Rad」で、各針で共通しています。
実際の時計は、時計中心の反対側まで針が伸びている時計が多いですが、今回は中心から生えているタイプにしました。
(なお、実際の時計で針が反対側まで延びているのは、針の重さのバランスを取っているのではと推測しています。)

次に終点ですが、各針とも初期は真上に向くように配置しますので、針先の位置は水平方向では「C_Left + Rad」です。
終点の垂直方向だけは各針で異なり「時計中心から針長さ分だけ原点に近づける」ことになります。
針長さは、モジュール変数として設定しており、長針=RadL、短針=RadS、秒針=RadSEC です。

針の太さは、一般的には「短針>長針>秒針」になっていると思いますので、図3-4の55行目では長針の太さを59行目では短針の太さを「Line.Weight」で太さ変更しています。

これで一応時計の形が完成しました。

図3-4の66~73行目では、完成した時計の各部品をまとめてグループ化し、時計を移動できるようにしています。また、時計を削除する際もグループ化することで、容易に削除できるようになります。

図形をグループ化するには、配列に各図形の名前(図形オブジェクトのNameプロパティ)を格納し、「シート名.Shapes.Range(図形の名前の配列).Group」を実行することでグループ化できます。
この配列SpNameは図3-4の21行目で宣言しており、66~72行目でこの配列に全部品(計16個)の名前を格納しています。格納が完了したら、73行目でグループ化しオブジェクト名を「Gshape」としています。

時計外観作りはここまでで、次は針を動かすべく76行目で「myTime」プロシージャへ移動します。

3-3.時刻の取得

時刻を刻んで、長針・短針・秒針に動く量を指示するのが図3-10のmyTimeプロシージャです。
  1. '========== ⇩④ 時刻の取得と針位置移動指示 ===================
  2. Private Sub myTime()
  3.  Dim T As Single
  4.  Dim Angle As Single
  5.  Do
  6.   T = Timer()
  7.   Do While Int(T + 1) > Timer()
  8.    DoEvents
  9.    DoEvents
  10.   Loop
  11.   T = Timer()
  12.   C_Left = Sp.Left
  13.   C_Top = Sp.Top
  14.   Angle = T Mod 60
  15.   Call myMove(Angle, SEChand, PosSEC, RadSEC)     '秒針
  16.   Angle = (T Mod (CLng(60) * 60 * 12)) / (60 * 60) * 5
  17.   Call myMove(Angle, Shand, PosS, RadS)        '短針
  18.   Angle = (T Mod (60 * 60)) / 60
  19.   Call myMove(Angle, Lhand, PosL, RadL)        '長針
  20.   DoEvents
  21.   DoEvents
  22.  Loop
  23. End Sub
図3-10

このプロシージャでは、Do~Loopが二重になっています。98~120行目の外側のDo~Loopの内側に100~103行目のDo~Loopがある構造になっています。

まず99行目で現在の時刻を取得しています。Timer関数を使用しているため、1秒未満まで取得できます。
例えば、99行目を実行した時のTimer関数の値が「12345.67」秒だったとしましょう。

100~103行目の内側Do~Loopの継続条件は「Int(T + 1) > Timer()」となっており、左辺の「Int(T + 1) 」を計算すると「12345.67 + 1 = 12346.67」でInt関数で小数点切り捨てますので「12346秒」となります。ですので右辺の「Timer()」が「12346.00」になると内側Do~Loopを抜け出し104行目に進みます。

内側Do~Loopの中にある101~102行目のDoEvents関数は「制御をO/Sに渡す」ものであり、ユーザーがこの間に操作・作業をしたり、Do~Loopが終わらなくなった時にキーボード等の操作が出来るようにするものです。
2個DoEventsを並べる理由は良く分かりませんが、「1個ではダメな時がある」と言われており必ず2個入れる様にしています。

104行目では現時刻を再取得しています。104行目時点では切りの良い秒の時刻のはずで、気持ち的には「T = T + 1 」です。しかし内側Do~Loop内でExcel以外の処理が原因で、104行目時点が「切りの良い時刻を大きく過ぎてしまった」こともあり得ますので、時刻の再取得をしています。

106~107行目は、時計円の左右方向・上下方向の位置を再取得しています。ユーザーが時計の位置を動かしてしまったことを想定しています。ユーザーによる移動は「101~102行目のDoEventsの間に行われている」と考えられますので、106~107行目時点では時計円と針は一緒に動いているはずです。
針の位置を修正する工程(109~116行目)の前までに正しい時計位置を取得し、モジュール変数の値を変更するようにしています。

109~110行目は秒針を動かしています。
109行目は現在時刻から秒を割り出しています。104行目時点で「12346.00」秒だった場合、「3時25分46秒00」です。「T Mod 60」の計算結果は「46」となります。(Tの値がもし12346.11などと小数点を持っていても、Modの計算結果は小数点を切り捨ててくれるので、秒針はキッチリと動いているように見えます)
針を動かしているのは図3-11のMyMoveプロシージャで、引数には「360度を60とする角度」「動かす針のオブジェクト名」「針の前回の位置を保持している変数名」「針の長さ」の4つを渡します。

112~113行目は短針を動かします。
112行目の角度Angleとしての式は「(T Mod (CLng(60) * 60 * 12)) / (60 * 60) * 5」であり、先ほどの「12346.00」秒で計算すると、「12346/(60*60)*5 = 17.14722」になります。(電卓での計算結果とは少しズレが出ます)
「妙な数字」と思われるかもしれませんが、時計の17分過ぎの場所に短針があったら「3時半くらい」ですよね。360度を60とする値ですので、このような数字になるのは仕方ありません。

尚、式の中の「Clng(60)」について説明します。Mod関数の割る側は「60 * 60 * 12」です。「60」「60」「12」の各数値はInteger型(-32,768 ~ 32,767)です。しかしその掛け算の結果は「43,200」はInteger型を超えてしまう為、このままではエラーが発生するのです。
Integer型だけの式では「計算結果の枠としてInteger型の枠を用意している」ためです。そのため式中の1つだけでもLong型が混じっていれば「計算結果の枠としてLong型の枠を用意する」ためエラーが出なくなります。

115~116行目は長針を動かします。
115行目の式は「 (T Mod (60 * 60)) / 60」ですので「12346.00」秒で計算すると、「1546/60 = 25.76667」になります。こちらは「25分過ぎでもうすぐ26分」と考えると正しそうな値です。

変数Angleのデータ型は97行目でSingle型にしています。もしInteger型にしていたらどうなるでしょうか。
秒針は60分割/1分なのでキッチリと針が動いてくれて問題ありませんし、「T Mod 60」の結果も整数に必ずなります。
短針は60分割/12時間で、12分毎に動くことになりますので、まあ見た目には違和感は無いだろうと思います。
しかし長針は違います。60分割/1時間ですから「1分で1コマ」つまり秒針が1周し終わらないと1コマ進んでくれず、動きがデジタル的になってしまいます。これではアナログ時計とは言えません。
ですので、変数Angleのデータ型は、長針の為にSingle型にしています。

118~119行目のDoEventsは、101~102行目のDoEventsと同じ意味で置いていますが、すぐに98行目のDoに進むはずですので、あまり意味は無いかもしれません。

3-4.針の動作(図形描画位置の修正)

針を実際に動かしている(=直線の位置を修正している)のが図3-11です。短針・長針・秒針とも同じプロシージャを使えるようにするため、針の角度(S As Single)・オブジェクト名(Sp As Shape)・位置の変数(Pos As Integer)・針の長さ(L As Single)を引数として受け取ります。
  1. '========== ⇩⑤ 針の描画位置の修正 ==============
  2. Private Sub myMove(S As Single, Sp As Shape, Pos As Integer, L As Single)
  3.   'S :1周を60とする角度
  4.   'Sp:各針のShapeオブジェクト
  5.   'Pos:針の位置を示す変数
  6.   'L:針の長さ
  7.  Select Case Int(S / 15 )
  8.   Case 0
  9.    If Not Pos = 0 Then
  10.     Pos = 0
  11.     If Not Sp.HorizontalFlip = False Then Sp.Flip msoFlipHorizontal
  12.     If Not Sp.VerticalFlip = True Then Sp.Flip msoFlipVertical
  13.    End If
  14.    Sp.Left = C_Left + Rad
  15.    Sp.Top = C_Top + Rad - L * Cos(3.14 / 30 * S)
  16.    Sp.Width = L * Sin(3.14 / 30 * S)
  17.    Sp.Height = L * Cos(3.14 / 30 * S)
  18.   Case 1
  19.    If Not Pos = 1 Then
  20.     Pos = 1
  21.     If Not Sp.HorizontalFlip = False Then Sp.Flip msoFlipHorizontal
  22.     If Not Sp.VerticalFlip = False Then Sp.Flip msoFlipVertical
  23.    End If
  24.    Sp.Left = C_Left + Rad
  25.    Sp.Top = C_Top + Rad
  26.    Sp.Width = L * Cos(3.14 / 30 * (S - 15))
  27.    Sp.Height = L * Sin(3.14 / 30 * (S - 15))
  28.   Case 2
  29.    If Not Pos = 2 Then
  30.     Pos = 2
  31.     If Not Sp.HorizontalFlip = False Then Sp.Flip msoFlipHorizontal
  32.     If Not Sp.VerticalFlip = True Then Sp.Flip msoFlipVertical
  33.    End If
  34.    Sp.Left = C_Left + Rad - L * Sin(3.14 / 30 * (S - 30))
  35.    Sp.Top = C_Top + Rad
  36.    Sp.Width = L * Sin(3.14 / 30 * (S - 30))
  37.    Sp.Height = L * Cos(3.14 / 30 * (S - 30))
  38.   Case 3
  39.    If Not Pos = 3 Then
  40.     Pos = 3
  41.     If Not Sp.HorizontalFlip = True Then Sp.Flip msoFlipHorizontal
  42.     If Not Sp.VerticalFlip = True Then Sp.Flip msoFlipVertical
  43.    End If
  44.    Sp.Left = C_Left + Rad - L * Cos(3.14 / 30 * (S - 45))
  45.    Sp.Top = C_Top + Rad - L * Sin(3.14 / 30 * (S - 45))
  46.    Sp.Width = L * Cos(3.14 / 30 * (S - 45))
  47.    Sp.Height = L * Sin(3.14 / 30 * (S - 45))
  48.  End Select
  49. End Sub
図3-11

まず、このプロシージャを説明する前に、直線図形の正転・反転について説明します。

図3-4で、長針・短針・秒針を作成するのに以下の式を使用しました。
ActiveSheet.Shapes.AddConnector (msoConnectorStraight,始点の水平位置,始点の垂直位置,終点の水平位置,終点の垂直位置)
この式は、作成した「直線には始点と終点がある」ことを示しています。今回の時計では「時計円の中心(=時計の軸)が始点」「針の先が終点」にしています。
作成直後は針は真上を向いていますが、時刻により針先が「⓪右上方向」「①右下方向」「②左下方向」「③左上方向」に変わります。直線として見れば、図3-12のようになります。


図3-12

図3-12では始点を●、終点を〇と表現していますが、この始点・終点の位置関係によって、直線オブジェクトの「HorizontalFlip」及び「VerticalFlip」プロパティの値が変わってきます。尚「Flip」とは反転の意味です。
「HorizontalFlip」は横方向での位置関係、「VerticalFlip」は縦方向での位置関係を示し、ワークシートの原点(左上角)から見て「始点より終点の方が遠い場合=正」「始点より終点の方が近い場合=逆」となります。図3-12では、セルの行位置・列位置で、直線図形の始点・終点を位置関係を比較しています。
「逆」になっている状態の時に「HorizontalFlip」「VerticalFlip」はTrue値、「正」の時にはFalse値になります。
(HorizontalFlip=True ならば「横方向の逆転(HorizontalFlip)は正しい(True)」という感じです。)

直線図形を時計の針、始点●を針の軸とした場合を考えてみます。図3-12の関係を時計の針に置き換えると、図3-13のように「0時~3時までが⓪」「3時~6時までが①」「6時~9時までが②」「9時~12時までが③」となります。
尚、ちょうど0時(針が真上を向いている時)は⓪、ちょうど3時(針が真右)は①という具合に分類されるようです。


図3-13

ですので針の位置が、短針で見た場合「⓪0時~3時まで」「①3時~6時まで」「②6時~9時まで」「③9時~12時まで」の4つに分類して処理しなければならない事になります。

図3-11のプロシージャは、針の角度を引数Sで受け取ります。このSは360度を60とする値ですので4分割し、Sの値が「⓪0~15まで」「①15~30まで」「②30~45まで」「③45~60まで」で処理を分けていきます。
この4つに分けるため、図3-11の129行目で「Int ( S / 15 )」を計算しています。つまり「Sを15で割り、その商を取り出」しSelect Caseで仕訳けることで、130行目の「Case 0」、141行目の「Case 1」、153行目の「Case 2」、165行目の「Case 3」に処理を4分割しています。

①:まず「Sの値が0~15まで(短針で言うと0時~3時まで)」の処理が131~140行目になります。
その中で131~135行目は、図3-12で説明した「HorizontalFlip」「VerticalFlip」のプロパティを設定(133~134行目)する部分です。針の位置としては、図3-12の一番左側の図になりますので、プロパティ値は「HorizontalFlip=False」「VerticalFlip=True」になります。

133行目は「HorizontalFlip=False」で無かったら、直線図形のオブジェクトのFlipプロパティに「msoFlipHorizontal」を設定して「HorizontalFlip=False」の状態にします。
また134行目は「VerticalFlip = True」で無かったら、Flipプロパティに「msoFlipVertical」を設定して「VerticalFlip = True」の状態にします。

このmyMoveプロシージャは約1秒に1回呼び出されますが、次に呼び出された時も「針の位置は、短針で言うと0時~3時までの間にある」可能性があり、「HorizontalFlip」と「VerticalFlip」のプロパティ値は保持されている為、再設定する必要は有りません(もちろん毎回再設定しても問題ありませんが、処理時間が結構増えます)。
ですので「現在の位置をPos変数(=モジュール変数のPosL、PosS、PosSEC)に保持し、同じ位置だったら「HorizontalFlip」と「VerticalFlip」プロパティの再設定を省く」ようにしています。

137~140行目は、針の描画位置を再設定しています。
針を「新規に描画した時には始点・終点を指示」したのですが、再設定する時には少し異なります。
針の始点・終点を角とする仮想の四角形で囲み、その四角形の「Left」「Top」「Width」「Height」の4つのプロパティに値を入れることで画像描画位置を再設定するのです(図3-14 )。

図3-14

「Case 0 」の短針で見て0時~3時は、針の位置でいうと図3-15に該当します。ですので、この針を囲んだ四角形枠の位置を、プロシージャに渡された引数(針の角度(S)・オブジェクト名(Sp)・位置の変数(Pos)・針の長さ(L))を使って設定していきます。

図3-15

まず、針を囲んだ四角形中で、角度の分かっている側の三角形に注目します。(図3-16の左図の黄色い部分)

図3-16

この三角形の中で分かっている寸法は、直線(針)の長さLと針の角度θです。ここから三角関数を使って、分かっていない辺の長さを求めていきます。図3-16の右側には、図3-5の左側の三角形を並べて見ました。
裏っ返しに見るような感じになりますが、分かっている角度θと最も長い辺を基準にして見比べてみると、Widthに相当する部分が右三角形のb、Heightに相当する部分が右三角形のa であることが分かります。

bをもとめるには、図3-16の一番右の一番上の式「Sin(θ)=b/c」を使います。求めたいb以外は全て値が分かっていますので、bを中心に式を動かし「c*Sin(θ)=b」とします。cは左図ではLに当たりますので、Widthは「L*Sin(θ)」となるのです。
Heightでは「Cos(θ)=a/c」の式を改造していくと「L*Cos(θ)」になります。θの部分をどう記述するかは、この後で説明します。

先に「Left」と「Top」の寸法を求めます。(図3-17)

図3-17

針を含む四角形の左端は、時計円の中心(針の軸心)ですので、「Left」は時計円中心である「C_Left + Rad」になります。
また、針を含む四角形の下端も時計円の中心(針の軸心)で、四角形の高さ(Height)は先程計算してありますので、「Top」は「C_Left + Rad」ー「Height」になります。

あとは角度です。今まで角度はθとしてきましたが、実際に引数として渡されているのはSという「360度を60とする角度」です。しかし、計算式で使用しているSin、Cos関数の引数には「360度を2πとする角度」を与える必要があります。
そこでSin・Cosに使用するθを、図3-18の様に換算をします。尚、πは「3.14」としています。

図3-18

以上の計算により、針が⓪の範囲にある時の「Left」「Top」「Width」「Height」は以下の計算式になりました。これが図3-11の137~140行目の式になります。

【Sの値が0~15まで(短針で言うと0時~3時まで】
 「Left 」= C_Left + Rad
 「Top  」= C_Top + Rad - L * Cos(3.14 / 30 * S)
 「Width 」= L * Sin(3.14 / 30 * S)
 「Height」= L * Cos(3.14 / 30 * S)

①:次に「Sの値が15~30まで(短針で言うと3時~6時まで)」の処理が142~151行目になります。
この針の位置は図3-12の①に相当するため、142~146行目で「HorizontalFlip = False」と「VerticalFlip = False」を設定しています。
また148~151行目は、針の描画位置の再設定を行う部分であり、上記と同様の手法で図3-19に従って設定します。

図3-19

【Sの値が15~30まで(短針で言うと3時~6時まで】
 「Left 」 = C_Left + Rad
 「Top  」 = C_Top + Rad
 「Width 」 = L * Cos(3.14 / 30 * (S - 15))
 「Height」 = L * Sin(3.14 / 30 * (S - 15))

針の直線図形を含む四角形の位置は、左右方向のLeftも上下方向のTopも時計円中心とイコールです。またWidth・Heightも上記同様に求められるのですが、この中の「Sin・Cosの中で使っている角度」が「s - 15」になっています。
図3-19での針の角度は、真上の12時方向から数えて90度+アルファが①の位置になるのですが、三角形を使ってWidth・Heightを計算する際には90度を差し引いて考えた方が簡単です
そのため、角度Sから90度相当の15を引いて計算しているのです。(360度が60ですので、1/4の90度は15に相当します)

②:「Sの値が30~45まで(短針で言うと6時~9時まで)」の処理が154~163行目になります。
この位置は、図3-12の②に相当しますので、「HorizontalFlip = False」「VerticalFlip = True」を設定します。
また再描画位置についても針の角度は180度+アルファの位置ですので、180度に相当する30を引き「S-30」で角度計算をし、図3-20の左図②の様に計算をします。

図3-20

【Sの値が30~45まで(短針で言うと6時~9時まで】
 「Left 」= C_Left + Rad - L * Sin(3.14 / 30 * (S - 30))
 「Top  」= C_Top + Rad
 「Width 」= L * Sin(3.14 / 30 * (S - 30))
 「Height」= L * Cos(3.14 / 30 * (S - 30))

③:「Sの値が45~60まで(短針で言うと9時~12時まで)」の処理が166~175行目になります。
この位置は、図3-12の③に相当しますので、「HorizontalFlip = True」「VerticalFlip = True」を設定します。
再描画時の針の角度は270度+アルファの位置ですので、270度に相当する45を引き「S-45」で角度計算をし、図3-20の右図③の様に計算をします。

【Sの値が45~60まで(短針で言うと9時~12時まで】
 「Left 」= C_Left + Rad - L * Cos(3.14 / 30 * (S - 45))
 「Top  」= C_Top + Rad - L * Sin(3.14 / 30 * (S - 45))
 「Width 」= L * Cos(3.14 / 30 * (S - 45))
 「Height」= L * Sin(3.14 / 30 * (S - 45))

なお、図3-4の54~62行目で長針・短針・秒針を作成した時は、針は真上を向いているので各直線オブジェクトは「HorizontalFlip = False」「VerticalFlip = True」のプロパティを持っています。
また、針の位置を保存しておく変数PosL・PosS・PosSECは図3-1の15~17行目で宣言をしただけですので「変数の値はゼロ」になっています。従って「針を作成した時点で、針の位置情報値と既に合っている」ことになります。
そのため時計を初めて動かす際に、ある針が⓪の位置に動こうとした場合は、図3-11の131行目の「If Not Pos = 0 Then」が成立しないために132~134行目が実行されません。

逆に言うと、変数が初期値(数値型であればゼロ、Boolean型であればFalse、文字列型であれば""(空文字))のままプログラムが動き出しても大丈夫かをチェックする必要があります。
もし心配であるなら、明示的に初期値を代入する方が良いと思います。


4.最後に

ワークシートの上に図形を描くのはExcelの本来の使い方では無いかもしれません。しかしExcelのデータを使って、その図形を動かしたりする事は、情報の見える化になり有用なことだと思います。
あまり複雑な図形だとExcelには荷が重すぎると思いますが、今回のような簡単な図形であればトライする価値はあるかもしれません。


図形で作るアナログ時計(it-031.xlsm)

セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始)
解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。