2021/02/14
2021/02/18(「フォームの終了」を改訂)

両矢印線の図形を日程線としてセル上に描画




1.背景

いくら記憶力が良くても、全てのスケジュールが頭の中に入っている方は少ないと思います。多くの方がスケジュール帳やOutlookのような日程管理ソフトに頼っているのではないでしょうか。
また、自分なりの日程枠をExcel上で作り、時間単位や日にち単位で日程線を引いて自己管理をしたり、会社の業務のスケジュールをExcelで作り、部下に配布しながら仕事を管理している方もおられると思います。

Excelを使えば、一般的な手帳やソフトには無い「その人・その職場にピッタリなスケジュール帳」が作れます。しかし「時間単位や日にち単位で日程線」に適している「図形の矢印線」を引くのは、結構面倒です。
今回は簡単に「セル単位の日程線」を引くマクロを紹介します。

2.システム概要

スケジュール帳のフォームは色々でしょうが、日程線を引く枠である「行」や「列」は確保してあると思います。その「行」「列」にマクロを使って図形の矢印線を引いていきます。
今回のシステムをExcelにアドイン登録しておくと、様々なスケジュール帳に日程線が引けます。もちろん、スケジュール帳自体にマクロを組み込んでも構いません。
また、サンプルファイルではSheet2のスケジュール枠の右上に、簡易に試行するためのマクロ起動ボタンを設置しています。

まずアドイン登録のボタン(図2-1の①)をクリックすると線引きダイアログ②が表示されます。このダイアログはモードレスで表示されるので、ダイアログ表示中もワークシートの操作が可能です。
次に日程線を引くセル位置を範囲指定③し、ダイアログの「線引き実行」ボタンをクリック④します。すると範囲指定した部分に図形の両矢印線が表示⑤されます。
日程線を引く(1)
図2-1

最初に引いた日程線が図2-1のように「9:00~10:00」であったとします。その日程が30分ズレた時には、新たに「9:30~10:30」のセル位置を範囲選択⑥し、ダイアログの「線の重複許可③」が「レ点無し」の状態(=線の重複不可)であることを確認してから「線引き実行」ボタンをクリック⑧すると、「9:00~10:00」の線は消えて「9:30~10:30」に置き換わり⑨ます。

これは「最初の線を移動した」ように見えますが、実際には「9:30~10:30」のセルに重なっている線(9:00~10:00の線)を消去してから「9:30~10:30」の線を新たに書き込んでいます。
なお、消去される線は「新たな線に一部でも重なる線、且つ このシステムで引いた日程線」のみですので、例えば⑩のような後からユーザーが書き込んだ図形は削除されません。

なお、システムで引いた日程線に重ならないようにセル範囲指定(例えば⑪のように「10:30~11:30」を指定)して実行⑫すれば、⑬のように併行して日程線が引かれます。
日程線を引く(2)
図2-2

また、図2-3のように横方向に日程線を引く場合もあるかと思いますが、⑭のように横方向にセル範囲を指定し「線引き実行⑮」をクリックすれば、⑯のように横方向に日程線が引かれます。
これはプログラム中で「選択範囲が縦方向に続いている」か「横方向に続いている」かを調べ、その方向に矢印線を引いているためです。しかし1セルだけを範囲指定されると、プログラムからは「縦に引いて良いのか、横に引けば良いのかが分からない」ため、ダイアログ上のオプションボタンの「線の向き」に従って引くようになっています。スケジュール表の向きにより、ダイアログの「線の向き」を選んでおいて下さい。
なお、複数セルを選択している時には「線の向き」のオプションボタンを無視し、指定範囲の続いている方向に日程線を引きます。

また、ダイアログ上の「線の重複許可」にレ点を入れている⑱場合は、⑰のように「最初に引いた日程線に重なるようにセル範囲指定」しても、元の日程線は消去せずに「少しズラして重ね書き⑳」をします。

なお、図2-3の日程線の両端は矢印ではなく「●印」となっていますが、これはダイアログ上の「線の両端」で黒丸を選択することで変更できます。
日程線を引く(3)
図2-3

今回システムでは「不要な日程線」を削除するような機能は持たせていません。1本1本つまんで削除するとか「オブジェクトの選択」でまとめて選択し削除する。または、今回ダイアログの「線の重複許可」のレ点を消した上で「削除するセル範囲を選択し線引き実行」してから「新たに作成された日程線1本を削除」するようにしてください。
(削除機能を付けなかった理由は、いっぱい日程線を引いた後で、一気に全ての日程が無くなるような状況はめったに起こらないだろうと思ったからです。)

また、ダイアログを閉じるには、ダイアログ右上の×印で閉じてください。
なおダイアログ上でユーザーが選択した「線の重複許可」「線の向き」「線の両端」の状態はファイル内に保存されますので、次回起動時には前回の状態で立ち上がります。

3.プログラムの流れ

プログラムの全体的な流れは図3-1のようになっています。
プログラムの流れ
図3-1

まず起動ボタン①をクリックすると、ダイアログ②がモードレスで表示されます。ダイアログのオプション(線の重複許可など)は、データ保存用シートに保存してある値で前回を再現③します。またオプションをユーザーが変更した時点で、その情報をデータ保存用シートに保存③しています。

ユーザーがワークシート上の線引きする位置のセルを選択してから、ダイアログ上の「線引き実行」ボタン④をクリックすると、まず図形描画の位置を計算⑤します。計算をするためには、線同士が重ならないようにズレ量⑦が必要ですが、そのズレ量を得るために別プロシージャ(図3-1の一番右側)を呼び出します。

まずユーザーが選択しているセル範囲内に存在する図形を抽出⑨し、1つ1つ処理をしていきます。処理には2通りあり、「線の重複が禁止されている場合」には抽出した図形を削除⑩していきます。もう一方の「線の重複が許可されている場合」には図形線が何重になっているかを計算⑪し、その本数を「ズレ量計算」⑦へ戻します。(図形を削除した場合は、重なっている本数はゼロとなります。)

新たなズレ量が確定したら、その値とセル範囲の位置データを使って「新たな図形描画位置」⑧を確定します。その「新たな図形描画位置」を使って線図形を描画⑥⑫します。

4.データ保存用シート(Sheet1)

ユーザーがダイアログ上のチェックボックス・オプションボタンを変更した状態は、図4-1の様にマクロが記載されているブックのワークシート上に保存されます。マクロがアドインとしてExcelに登録されているならば、ユーザーから直接操作できない場所になりますが、もしデータ(スケジュール帳など)と一緒のブック内にマクロを記述する場合には、簡単に操作できないように「シートを非表示」にするなどの処置が必要だと思います。
データ保存用シート
図4-1

今回ダイアログでは、CheckBox1、OptionButton1~4がユーザー操作可能ですが、OptionButton1と2、OptionButtn3と4は「一方が決まれば、もう一方も決まる」関係にあるので、セットとして片方の状態だけを保存することにしています。
ですので「線の重複あり・なし(重複有り=True)」「縦線か横線か(縦線=True)」「先端形状が矢印か黒丸か(矢印=True)」の3種をB1セル、B2セル、B3セルに「True」または「False」で保存します。

なお、もしセルに何も入っていない場合には「False」と判断されるように、読み取り時に細工をします。

5.標準モジュール

標準モジュールには、アドイン登録した場合はExcel上部のボタンから呼び出されるマクロ、一般のブック(.xlsm)としてマクロを保存しておく場合はシート上のボタンへの登録マクロ を、置きます(図5-1)。
  1. '========== ⇩(1) システムの起動プロシージャ ====================
  2. Public Sub StartDrawLine()
  3.  UserForm1.Show 0
  4. End Sub
図5-1

内容としては、3行目でUserForm1をモードレス(ダイアログ表示時もシート操作が可能)で起動します。
Excelで作った日報をPDFでそのままメール送信」では、システム内(フォームを3個使っている)で共通使用する変数・定数を標準モジュール側で宣言・値代入していましたが、今回はフォームが1つですのでUserForm1側で宣言・値代入を行っています。
今回も標準モジュール側で宣言・値代入は可能ですが、変数・定数を使う範囲は「出来るだけ狭い方が良い」のでUserForm1内での宣言・値代入が正しい使い方だと思います。

6.UserForm1(送信ダイアログ)

6ー1.フォームの作成

フォーム上への各コントロールの配置は、図6-1のようにしています。
フォーム1のレイアウト
図6-1

メインは「線引き実行」のボタン(CommandButton1)ですので1番上に大きく置き、その下に「線の重複許可」のCheckBox1、「線の向き」のOptionButton1~2、「線の先端」のOptionButton3~4を配置します。
OptionButton1~2はセットで動かしますのでFrame1で囲みます。OptionButton3~4も同様にFrame2で囲みます。

CommandButton1の表示文字はマクロ側から記入しますが、他のコントロールの文字はレイアウト時にCaption設定しています。
しかし今回システムは、操作の途中でCommandButtonの表示を変更する内容ではないので、表示名は配置時に設定してもOKです。

6ー2.フォームモジュール

6ー2ー1.フォーム起動時の設定

フォーム宣言部では図6-2のように、1つの変数「IniSheet」と2つの定数「Mark」「Dist」を宣言しています。
  1. '========== ⇩(2) フォームレベルの変数・定数宣言 ====================
  2. Dim IniSheet As Worksheet           '条件保存シート
  3. Const Mark As String = "it050"          '代替テキストに書き込むワード(当システムで作成したか否かの判断用)
  4. Const Dist As Single = 10           '線と線の間隔
図6-2

7行目の変数「IniSheet」は、図4-1の「ユーザーのダイアログ設定条件」を保存するワークシートの所在場所を記憶する変数です。
他の方法として、例えば「Const IniSheet As String = "Sheet1" 」のように定数設定する方法もあるとは思いますが、その値を使用する時には「ThisWorkbook.Sheets(IniSheet).Cells(1,2).Value =・・・」のような書き方をしなければならなくなります。
しかしWorksheet型として変数宣言→値代入すれば、「IniSheet.Cells(1,2).Value =・・・」と簡易に表現できますので、今回はWorksheet型として変数宣言しています。

8行目の定数「Mark」は、「このシステムで作成した図形か否か」を見分ける「目印」として使います。この値は図6-6の53行目、図6-15の104行目で、図形の「AlternativeText」プロパティ値として使うものです。AlternativeTextプロパティ値は、図6-3のように「図形の書式設定」の中の「サイズとプロパティ」内の「代替テキスト」の値です。
今回は「自分で作った図形には目印を付け」ておくことで、図2-2の⑩の「ユーザーが自分で作った図形」とは区別して処理するようにしています。

なお、ここで設定している「it050」というMark値は「このシステムのシステム番号(私が勝手に付けた番号)」ですので、システムを実際に使用する場合には変えてもらって構いません。

本システムで描画した図形の代替テキスト文字列
図6-3

9行目の定数「Dist」は英語のDistanceの略で、「日程線と日程線の距離(単位:ポイント)」になります。今回システムでは、日程線を重複させて表示できるようにもしましたので、その重複する矢印線が見易いように10ポイントに定数設定しています。
なお、セルの幅なり高さなりの中で「何本の日程線が引けるか」は、この定数Distの値で決まります。より多くの日程線を表示させたいのであれば、このDist値を小さ目に設定して下さい。

フォームが初めて起動する際に実行されるのが、図6-4のInitializeイベントプロシージャです。
  1. '========== ⇩(3) フォーム起動時の設定 ====================
  2. Private Sub UserForm_Initialize()
  3.  Set IniSheet = Sheet1
  4.  Me.CommandButton1.Caption = "線引き実行"
  5.  Me.CheckBox1.Value = CBool(IniSheet.Cells(1, 2).Value)
  6.  Me.OptionButton1.Value = CBool(IniSheet.Cells(2, 2).Value)
  7.  Me.OptionButton2.Value = Not Me.OptionButton1.Value
  8.  Me.OptionButton3.Value = CBool(IniSheet.Cells(3, 2).Value)
  9.  Me.OptionButton4.Value = Not Me.OptionButton3.Value
  10. End Sub
図6-4

14行目では、図6-2の7行目で宣言した変数「IniSheet」に、データ保存用シートである「Sheet1」を代入しています。この「Sheet1」はオブジェクト名としてのSheet1です。代わりにサンプルファイルであるならば「ThisWorkbook.Sheets("Sheet1")」または「ThisWorkbook.Worksheets("Sheet1")」でも同じですが、データ保存用シートがユーザーに見えてしまう場合には「シート名を変更されてしまう」とエラーが発生しますので、オブジェクト名を使用した方が安全です。

16行目は、ダイアログのボタン表面に文字を表示させています。
18~22行目は、ダイアログ上の1つの「チェックボックス」と4つの「オプションボタン」の設定です。データ保存用シート(IniSheet)の各セルに保存してある値を各コントロールに設定しています。

例えば18行目では、CheckBox1にデータ保存用シートのCells(1,2)の値を設定しています。
Cells(1,2)の値が「True」であればCheckBox1にはレ点が付き、「False」であればレ点が外れます。これだけで済めば18行目の式の右辺は「IniSheet.Cells(1, 2).Value」だけで良いのですが、もし「Cells(1,2)が空白セル」だった場合には「IniSheet.Cells(1, 2).Value」の計算結果は「Empty」になります。
この値をCheckBoxのValue値に設定してみると、図6-5の様に中途半端な濃さでレ点が付きます。またプロパティ一覧で確認してみると、図6-5の一番右のように「Value値欄が空欄」になっています。他のコントロールでも同じ状態です。
チェックボックス、オプションボタンにEmpty値を設定した時の状態
図6-5

これでは各コントロールを設定したことにはなりませんので「空白セルだった場合には、Falseと見なす」ことに決め、Empty値をFalse値に変換するために「CBool関数」を使います。
なお、この空白セル状態は初回に使う時だけにしか発生せず、一度でもコントロールを切り替えれば「True」か「False」かが、データ保存用シートのセルに入ります。

19行目・21行目は18行目と同じですが、20・22行目は「セットである相手オプションボタンの逆の値」をNotを使って設定することで「どちらかがON」になります。
なお20行目であれば、式の右辺は「Not Me.OptionButton1.Value」の代わりに「Not CBool(IniSheet.Cells(2, 2).Value)」でもOKですが、同じ式を複数回書きたくないので避けました。

6ー2ー2.線引き実行

「線引き実行」ボタンをクリックした時には、図6-6のClickイベントプロシージャが動作します。
  1. '========== ⇩(4) 日程線を引く ====================
  2. Private Sub CommandButton1_Click()
  3.  Dim Arrow As Shape        '描画した図形オブジェクト
  4.  Dim StartX As Single       '始点のX座標
  5.  Dim StartY As Single       '始点のY座標
  6.  Dim EndX As Single       '終点のX座標
  7.  Dim EndY As Single       '終点のY座標
  8.  Dim HeadStyle As Long      '先端の形状
  9.  With ActiveSheet
  10.   If Not .Type = xlWorksheet Or Not TypeName(Selection) = "Range" Then Exit Sub
  11.   If LinePos(Selection, StartX, StartY, EndX, EndY) = False Then
  12.    MsgBox "単列または単行を選択して下さい" & vbCrLf & "または、図形描画の限界を超えています"
  13.    Exit Sub
  14.   End If
  15.   If Me.OptionButton3.Value = True Then
  16.    HeadStyle = msoArrowheadTriangle
  17.   Else
  18.    HeadStyle = msoArrowheadOval
  19.   End If
  20.   Set Arrow = .Shapes.AddConnector(msoConnectorStraight, StartX, StartY, EndX, EndY)
  21.   Arrow.Line.BeginArrowheadStyle = HeadStyle
  22.   Arrow.Line.EndArrowheadStyle = HeadStyle
  23.   Arrow.AlternativeText = Mark
  24.  End With
  25. End Sub
図6-6

実際に図形線を描画しているのは50行目の部分ですが、線を描画するには描画位置(線の始点・終点のX座標・Y座標)を指定する必要があります。今回の場合で説明すると、図6-7のように始点・終点を「StartX, StartY, EndX, EndY」で指定しますので、その4つを図6-6の29~32行目で変数として宣言しています。
線図形を描画するための座標
図6-7

まず37行目では、「シートの種類」と「Selectしているものがセルか否か」を調べています。
まず開いているシートの種類がワークシートで無ければプロシージャを抜け出し、終了させます。
これは、今回システムでは「セルを範囲指定し、その中に矢印図形を描画」することから、セルが存在するワークシート以外は対象としていないためです。
また「セルを選択している状態」であればTypeName関数は「"Range"」という文字列を返してきますので、「Not TypeName(Selection) = "Range"」で、セル以外(例えば、図形やフォームコントロール類)を選択している時にはプロシージャを抜けます。

39行目の「LinePos」関数プロシージャ(図6-10)は、第一引数に指定セル範囲を渡すことで「そのセル範囲内に描画する矢印線の始点・終点のX座標・Y座標」を第2~第5引数に戻して来るものです。また関数自体の戻り値は、矢印線が引けない場合にはFalseを戻し、引ける状況であり座標の計算結果を第2~第5引数に戻した場合にはTrueを戻します

なお「矢印線が引けない場合」は「複数行x複数列をセル選択している時」及び「図形線のサイズが限界を超えている時」です。
まず複数行x複数列をセル選択されると「縦横どちらの方向に、またどの行・列に矢印線を引いたら良いのか判断できない」ためです。 また、列や行全体を選択して矢印線を引こうとすると、引く線の長さが長すぎてAddConnectorメソッドでエラーが発生してしまいます。
ですので39行目で「LinePos関数」の戻り値がFalseの場合は、40行目でコメントを出し、41行目で作業を中止します。

44~48行目では、線の両端の形状を設定しています。
線の両端形状は、ダイアログ上のOptionButton3~4で「矢印」か「黒丸」かを決めています。OptionButton3~4は「必ずどちらかがTrue」ですので、44行目ではOptionButton3がTrueだった場合(=矢印を選択していた場合)は45行目を実行させ、そうでなかった場合(=黒丸を選択していた場合)は47行目を実行させます。

線の両端の形状は図6-8のような種類が有り、今回は値2か値6の選択としています。
定数内容
msoArrowheadNone1矢印なし  
msoArrowheadTriangle2三角形   
msoArrowheadOpen3矢印    
msoArrowheadStealth4鋭い形   
msoArrowheadDiamond5ひし形   
msoArrowheadOval6楕円形   
図6-8

50行目の「ActiveSheet.Shapes.AddConnector」メソッドで、線(正式にはコネクタ)を作成します。その第一引数には「線の種類」を指定します。
線の種類は図6-9の3種類で、今回は値1の直線コネクタを使用しています。
ただし「カギ線コネクタ」や「曲線コネクタ」は、角度をつけて描画した時に初めて「カギ型」になったり「曲線」になったりするので、今回のように垂直水平状態で描画するのであれば、どれでも「直線」に見えることになります。
定数内容
msoConnectorStraight1直線コネクタ 
msoConnectorElbow2カギ線コネクタ 
msoConnectorCurve3曲線コネクタ 
図6-9

なお、図形を作るだけであれば「.Shapes.AddConnector msoConnectorStraight, StartX, StartY, EndX, EndY」と、AddConnectorメソッドの5つの引数はカッコ無しで与えますが、今回は作った図形を変数「Arrow」に代入する必要がありますので「カッコ内に引数」を並べます。

51~52行目では、50行目で作成した直線図形(Arrow)に対して、両端に矢印か黒丸(変数HeadStyle)を設定しています。
また53行目では、直線図形の「代替テキスト値」に定数Markに設定した文字列(サンプルファイルでは『it050』)を設定しています。前述しましたが、これは「今回システムで作成した図形」であることの目印となります(図6-3参照)。

6ー2ー3.線の描画位置を計算

図6-6の39行目から呼び出される「矢印図形(日程線)を描画する位置」を計算する関数が図6-10です。
  1. '========== ⇩(5) 日程線の位置を計算 ====================
  2. Private Function LinePos(mySelection As Range, StartX As Single, StartY As Single, EndX As Single, EndY As Single) As Boolean
  3.  Dim Direc As Boolean     '線の向き(縦線=True、横線=False)
  4.  Dim Dupl As Boolean     '線の重複許可(許可=True)
  5.  Dim MoveW As Double     '移動量
  6.  If Not (mySelection.Rows.Count - 1) * (mySelection.Columns.Count - 1) = 0 Then Exit Function
  7.  If Not (mySelection.Rows.Count - 1) = 0 Then        '縦線
  8.   Direc = True
  9.  ElseIf Not (mySelection.Columns.Count - 1) = 0 Then     '横線
  10.   Direc = False
  11.  Else
  12.   Direc = Me.OptionButton1.Value
  13.  End If
  14.  Dupl = Me.CheckBox1.Value
  15.  MoveW = Dist * (SearchArrow(Dupl, Direc) + 1)
  16.  If Direc = True Then            '縦線
  17.   StartX = mySelection.Left + MoveW
  18.   EndX = StartX
  19.   StartY = mySelection.Top
  20.   EndY = StartY + mySelection.Height * 0.99
  21.  Else                     '横線
  22.   StartX = mySelection.Left
  23.   EndX = StartX + mySelection.Width * 0.99
  24.   StartY = mySelection.Top + MoveW
  25.   EndY = StartY
  26.  End If
  27.  If (EndX - StartX) <= 169056 And (EndY - StartY) <= 169056 Then
  28.   LinePos = True
  29.  End If
  30. End Function
図6-10

65行目では「複数行x複数列」をセル選択している時は、そのまま関数プロシージャを抜けます。
行と列の両方が複数であることを確認するには「行数>1 AND 列数>1」という条件式でも良いのですが、今回(65行目)は「行・列のどちらか、又は両方が1 の場合はOK」であることから「(行 - 1)x(列 - 1)<>0」としてみました。

67~73行目は「縦線・横線のどちらを引くか」を決めています。なお、この段階では既に「複数行x複数列」の状態は取り除かれています。
まず67行目の条件式「Not (mySelection.Rows.Count - 1) = 0」は、「行数は1 では無い」という意味ですので「行方向に延びたセル範囲」となり、縦線を引くことになります。ですので変数DirecにはTrue(ここでは縦線の意味にしている)を代入します。

69行目の条件式「Not (mySelection.Columns.Count - 1) = 0」は67行目と逆ですので「列方向に延びたセル範囲」となり、横線を引くことになります。ですので変数DirecにはFalse(横の意味)を代入します。

最後に残っているのは「単一行x単一列(=1セルのみ)」のセル範囲の場合です。1セルのみですと「縦横どちらの方向に線を引けばよいのか」が判断できませんので、この場合はダイアログ上の「線の向き」で「ユーザーが指定した方向」に従うことにします。
ですので「縦線であるなら OptionButton1がON(=True)」、「横線であるなら OptionButton2がON(=OptionButton1がOFF=False)」とします。
ですので72行目は「Direc = Me.OptionButton1.Value」となります。

75行目は、ダイアログ上の「線の重複許可」であるCheckBox1の値を変数「Dupl」に代入しています。
そして77行目では「SearchArrow」関数プロシージャを呼び出します。そのSearchArrowに渡す引数の内、第一引数が75行目で代入された変数Dupl(線の重複可=True、不可=False)、第二引数が67~73行目で代入された変数Direc(縦線=True、横線=False)です。

この「SearchArrow」関数プロシージャの役目は2つあります。
1つは「重なっている線を削除するか否か」、もう1つは「線の重複を許可する場合は、線が重ならないようにする位置の計算」です。

まず「Dupl=False」である「線の重複を許可しない(=重なっている線を削除する)」場合は、「SearchArrow」関数プロシージャの中で重なっている線を削除し、関数の戻り値としては「ゼロ」を返してきます。「ゼロ」は「これから描画する位置には、ゼロ本の線がある」という意味になります。
次に「Dupl=True」である「線の重複を許可する」場合は、図6-11のように重なっている線を調べ「従来の線は、何重になっているか」を返してきます。

線の重なりとSearchArrow関数が戻す値
図6-11

なお図6-11の③は「線が1重」ですが、1行目の線は数えず「一番外側の線は何番目か」のような数え方をしています。

77行目は、その戻り値「SearchArrow(Dupl, Direc)」に1を足してから「Dist」を掛け、変数MoveWに代入します。
この中で「Dist」は図6-11で示しているように「線と線の距離」ですので、変数MoveWは「新しく描画する線の、セル範囲端からの距離」ということになります。

79~89行目は、始点・終点のX座標・Y座標の計算をしています。
まず「これから描画する線が、縦線なのか横線なのか」で場合分けをしています。縦線の場合は80~83行目を実行し、横線の場合は85~88行目を実行します。分岐に使用する値は、67~73行目で計算した変数Direcの値(縦線=True、横線=False)です。

「縦線」の場合は図6-12の左側のようになります。まず始点(Start)のX座標(StartX)は、セル範囲の左上角(Selection.Left)に対してMoveW分だけズレた場所になります。図形線は縦に垂直ですので、終点(End)のX座標(EndX)もStartXと同じということになります。
次に始点(Start)のY座標(StartY)は、セル範囲の左上角(Selection.Top)です。また終点(End)のY座標(EndY)は、そこからセル範囲の高さ(Selection.Height)分だけ下に下がったところになります。
矢印の始点・終点の位置
図6-12

ただし、83行目の式の最後に「x 0.99」が付いており、セル範囲の高さ(Height)に対して、ちょっとだけ小さくしています。これは描画した図形の線が「隣のセルに入ってしまわないように」するためです。
図6-13で、B2~B3セルにそのまま線を引いた場合(一番左側)には、終点は「ギリギリでB4セルに入っている」ことになります。
一方左から2番目は、C2~C3セルに「高さx0.99」で線を引いた場合ですが、ちょっとだけ線が短くなったおかげで「ギリギリC3セルにとどまっている」ことになります。

このように「少しだけ短くしている」理由は、図6-15のSearchArrow関数プロシージャ内で「従来の図形線が、新しく線を引くためのセル範囲に含まれているか否か」を判断する際、図6-13の一番右側のように「重なっていないはずなのに、計算上は重なっていることになる」のを防ぐためです。
例えば「9:00~10:00」に既に予定が入っているところに、「10:00~11:00」の日程を加えられないのでは困りますので、見掛けはほとんど変わらず、且つ計算上は重ならないようにするため「0.99を掛ける」ことにしました。
終点のセル位置
図6-13

別な方法として「ちょっとだけ(例えば0.1ポイント)長さをマイナスする」という方法もあります。「x0.99」と1%短くしてしまうと、長さが長い場合には差が大きくなってしまいますが、「少しだけ引く」のであれば目立ちにくくなります。
掛けても引いても、どちらでも良いので「ちょっとだけ短くする」のがポイントです。

「横線」の場合は図6-12の右側のようになります。縦線とほぼ一緒ですが、まず始点(Start)のX座標(StartX)は、セル範囲の左上角(Selection.Left)になります。終点(End)のX座標(EndX)はそこからセル範囲の幅(Selection.Width)分だけズレた位置になりますが、その幅に0.99を掛けています。これは縦軸の場合と同じ理由です。
次に始点(Start)のY座標(StartY)は、セル範囲の左上角(Selection.Top)に対して、下にMoveWだけズレた位置になります。また終点(End)のY座標(EndY)は、図形線が水平ですので同じ値ということになります。

91~93行目ですが、線図形を図6-6の50行目の「AddConnectorメソッド」で作成する場合、線のX方向Y方向に対して「169,056ポイント(2348インチ)」を超えた長さを設定すると、図6-14の左側ようなエラーが発生します。
図形長さの限界を超えた時のエラー表示
図6-14

図6-14の右側のように、線の長さという訳では無いようで、X方向・Y方向に分割した長さが「169,056ポイント」を超えないようにする必要があります。AddConnectorメソッドの座標位置の引数はSingle型なので小数点以下もあります。試してみると「169,056.0078125」まではOKのようですが、理論的な意味は不明です。

どのような場面でこのエラーが発生するかというと、「列全体、または行全体を選択状態にした上で、日程線を引こうとする」場合などが考えられます。標準状態でも横方向は865,075ポイント、縦方向は18,874,368ポイントありますので、完全にエラーです。

とにかくエラーを避けるために、91~93行目で「X方向、Y方向の両方が169056ポイント以下ならばOK(=LinePos関数としてTrueを戻す)」となるようにしました。
なお、X方向・Y方向のどちらかが超えていた時には92行目が実行されませんので、戻り値のLinePosにはBoolean型の既定値のFalseが入る(LinePos関数としてFalseを戻す)ことになり、戻った先の図6-6の40行目でエラーコメントが出ることになります。

なお、Ctrlキー等を使って「Areaを分けて選択」している場合は、Areas(1)の範囲で計算されます。ですので「最初に選択したAreaのみが有効」になり、2番目以降の選択Areaは無視されることになります。

6ー2ー4.選択範囲に重なる線の取得と処理

図6-10の77行目から呼び出される「重複する日程線の、削除処理or重なり量計算」プロシージャが図6-15です。
  1. '========== ⇩(6) 選択範囲に重なる日程線の取得と処理 ====================
  2. Private Function SearchArrow(Dupl As Boolean, Direc As Boolean) As Integer
  3.  Dim SP As Shape      'シート上の図形の1つ1つ(Shapeの略)
  4.  Dim SPM As Integer      '線図形の重なり数(Shape Movementの略のつもり)
  5.  For Each SP In ActiveSheet.Shapes
  6.   If SP.Type = msoConnectorStraight _
  7.     And Not Intersect(Selection, Range(SP.TopLeftCell, SP.BottomRightCell)) Is Nothing _
  8.     And SP.AlternativeText = Mark Then
  9.    If Dupl = False Then        '重複不可(重なっている線は削除)
  10.     SP.Delete
  11.     SPM = 0
  12.    Else                 '重複許可(何本目の線なのか数える)
  13.     If Direc = True Then       '縦線
  14.      If SPM < (SP.Left - Selection.Left) / Dist And (SP.Left - Selection.Left) < Selection.Width Then
  15.       SPM = (SP.Left - Selection.Left) / Dist
  16.      End If
  17.     Else                '横線
  18.      If SPM < (SP.Top - Selection.Top) / Dist And (SP.Top - Selection.Top) < Selection.Height Then
  19.       SPM = (SP.Top - Selection.Top) / Dist
  20.      End If
  21.     End If
  22.    End If
  23.   End If
  24.  Next SP
  25.  SearchArrow = SPM
  26. End Function
図6-15

101行目のFor Eachでは、アクティブシート上の図形を1つ1つ取り上げます。
102~104行目は、For Eachで取り込んだ図形を1つずつ以下の3つの条件式で調べていきます。ANDで結んでいますので、3つの条件式が全て満足する場合に、106~120行目を実行します。
 ①「SP.Type = msoConnectorStraight」
 ②「Not Intersect(Selection, Range(SP.TopLeftCell, SP.BottomRightCell)) Is Nothing」
 ③「SP.AlternativeText = Mark」

①の条件式は「図形がコネクタ線」であることです。今回システムではコネクタ線図形のみを描画していますので、それ以外の図形は対象外にするためです。

②の条件式は「選択セル範囲と重なっている図形」を選別しています。Intersectメソッドは、引数のセル範囲同士が「重なっているセル範囲」を戻すものです。
1つ目の範囲は「Selection(選択セル範囲)」で、2つ目の範囲は「Range(SP.TopLeftCell, SP.BottomRightCell)」です。2つ目の範囲を単に「SP.TopLeftCell(始点のあるセル位置)」や「SP.BottomRightCell(終点のあるセル位置)」にしなかったのは、図6-16のように「始点(SP.TopLeftCell)や終点(SP.BottomRightCell)は選択範囲(Selection)と重なっていない」が「線の軸は選択範囲(Selection)と重なっている」時にも「重なっている」と判断させるためです。
図形と重なりの判断
図6-16

③の条件式は「今回システムで作成した図形」を選別しています。
今回システムでは、図6-6の53行目で、変数Mark(図6-2の8行目で定数宣言)の値を図形の「代替テキスト」に設定していますので、変数Markの値(サンプルファイルでは『it050』)が入っていれば「今回システムで作成した図形」ということになります。
なお、この③の条件式は、図2-2の⑩のような「ユーザーが追加した図形」は消さずに残した方が良いと考えたからです。
(但し、Markの値を「担当者の名前」とか「数値」みたいな一般的なワードにしてしまいますと、ユーザーが使ってしまう可能性があり、システムが誤認する可能性が出てきますので注意が必要です。)

以上①~③の条件を全て満たした図形を「削除」または「何重に重なっているか計算」していきます。
なお、今回システムではコネクタ線図形しか作っていないので、②+③のみの条件式でも成立する可能性はあります。

106行目では、第一引数のDuplの値を調べて分岐させてます。Duplは「線の重複を許可しない(=False)」か「線の重複を許可(=True)」です。

「線の重複を許可しない(=False)」場合は、107~108行目を実行します。
107行目では①~③の条件を満足した図形を削除し、108行目で変数SPMにゼロ値を代入しています。
なお、この関数プロシージャを実行している最中は「Duplの値は変化しない」ので、Dupl=Falseの場合は「102~104行目の条件式を満足した図形は、全て107~108行目を実行する」ことになります。ですので変数SPMの既定値=0のままですので、特に108行目で「SPM = 0」とする必要はありませんが、今回は明示的にゼロを代入しています。

「線の重複を許可(=True)」の場合は110~120行目を実行します。この中では、引数Direcの値で分岐をさせています。
第二引数のDirecは、「縦線(True)」か「横線(False)」で、縦線の場合は111~113行目を実行し、横線の場合は115~117行目を実行します。

111行目の条件式は、以下の2つの条件式をANDで結んでいます。
 ①「SPM < (SP.Left - Selection.Left) / Dist」
 ②「(SP.Left - Selection.Left) < Selection.Width」

①の条件式の右辺は「何本の日程線が引かれているか」を計算しています。たとえば図6-17の左側は3本の日程線が引かれていますが、3本目(図形SP)の位置は「セル範囲の左端」から数えると「SP.Left - Selection.Left」となります。また、セル範囲の端から1本目の線までの距離、および線と線の間隔は「定数Dist」です。
ですので「(SP.Left - Selection.Left) / Dist」は「3」という値が得られるはずです。これは「3本の日程線が重なっている」ことを意味します。

線の重なり量を計算
図6-17

①の不等号では、その「重なっている本数」を「それまでに調べた線の本数(SPM)」と比較し、今回の方が多かった場合は112行目で「重なっている本数」を置き換えます。つまり「今までで、一番多かった本数」が変数SPMに入っていることになります。

なおコンピュータ内の計算は、計算過程で「桁落ち」が発生する可能性があるため、人間が考えたのとは異なる値が出ることが考えられます。
例えば「(SP.Left - Selection.Left) / Dist」の計算結果が「3」ではなく「2.99999」や「3.00001」のような値になってしまった場合です。
しかし今回の場合は、その結果を受取っている変数SPM(112行目)は「Integer型」で宣言されていますので、いわば「CInt(2.99999)」や「CInt(3.00001)」の計算をしている様なものなので、四捨五入(正確には、最も近い偶数に値を丸める。例えばCInt(2.5)=2 となる)されて「3」が得られます。

次に②の式ですが、図6-17の右側のように「多くの日程線を重複(SP.Left - Selection.Left)」して引いた場合に「日程線の枠であるセル選択範囲(Selection.Width)」を超えてしまったものについては無視をする、というものです。
つまり、セル選択範囲内での最大の本数をSearchArrow関数としては返すことになります。

ただし、SearchArrow関数としてはセル範囲内の本数を返しているのに、線を引く位置を計算している図6-10の77行目では、「+1」してしまっているため、「セル範囲を超えて日程線を引いてしまう」ことになります。超えてしまう前にユーザーに対してコメントを出すのも1つの手だとは思いますが、今回は「枠を超えて日程線が引かれた」ことでユーザーに気が付かせ、枠を広げるなりしてもらう方法としました。

以上は縦線についての処理でしたが、横線の場合には115~116行目の処理になります。縦線とは位置を測る方向が異なり、「Left → Top」「Width → Height」での計算式になります。

124行目は、変数SPMの値をSearchArrow関数の戻り値にしています。
繰り返しますが、線の重複不可の場合は「既定値のゼロ」を、重複許可の場合は「最大の重なり本数」になります。

6ー2ー5.オプション項目を操作した時のデータ保存

ダイアログの「線の重複許可」「線の向き」「線の両端」の選択を変更した時には、対応したCheckBox・OptionButtonのClickイベント(図6-18)が発生しますので、変更された値をデータ保存用シート(IniSheet)に書き込んでいます。
  1. '========== ⇩(7) オプション項目の保存(CheckBox1) ====================
  2. Private Sub CheckBox1_Click()
  3.  IniSheet.Cells(1, 2).Value = Me.CheckBox1.Value
  4. End Sub
  5. '========== ⇩(8) オプション項目の保存(OptionButton1) ====================
  6. Private Sub OptionButton1_Click()
  7.  IniSheet.Cells(2, 2).Value = True
  8. End Sub
  9. '========== ⇩(9) オプション項目の保存(OptionButton2) ====================
  10. Private Sub OptionButton2_Click()
  11.  IniSheet.Cells(2, 2).Value = False
  12. End Sub
  13. '========== ⇩(10) オプション項目の保存(OptionButton3) ====================
  14. Private Sub OptionButton3_Click()
  15.  IniSheet.Cells(3, 2).Value = True
  16. End Sub
  17. '========== ⇩(11) オプション項目の保存(OptionButton4) ====================
  18. Private Sub OptionButton4_Click()
  19.  IniSheet.Cells(3, 2).Value = False
  20. End Sub
図6-18

選択されたダイアログ上の状態を、図4-1の基準に従ってデータ保存用シートに保存しています。
OptionButton1と2は正逆の関係にありますので、書き込む値もTrueとFalseの正逆となります。OptionButton3と4も同様です。

6ー2ー6.フォームの終了

今回のダイアログ上には「閉じる」ボタンを設けていません。その代わりにダイアログ右上の×印で「閉じる」ボタンの代わりをさせることにしました。ダイアログ右上の×印をクリックした時に発生するQueryCloseイベントが図6-19です。
  1. '========== ⇩(12) フォームの終了 ====================
  2. Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
  3.  Cancel = True
  4.  Me.Hide
  5.  End
  6. End Sub
図6-19

通常、右上の×印でダイアログを閉じてしまうと、ダイアログのインスタンスがメモリー上から削除されてしまいます。再び起動させた時には、初期起動の設定(Initializeイベント)から整えていくことになります。
フォームをHideで閉じる(=隠す)時にはメモリー上に残っていますので、再び起動させた時には表示時の設定(Activateイベント)からの動作になります。

「改訂」
当初はQueryCloseイベントプロシージャ内で「155行目で引数のCancelにTrueを設定し『終了を中止』させ、156行目でダイアログを隠す(Hide)処理をする」としていました。
しかしHide後の状態とは「ユーザーフォームが隠れているだけ」であり、また隠れている場所は「初めて起動したワークブック上」となります。当然、再起動した場合も「初めて起動したワークブック上で表示される」ことになります。

このことが影響してくる場面としては、図6-20のように複数のブックを立ち上げて作業する場合です。
ダイアログのインスタンス(実体)は起動した時アクティブであったブックに支配されているようなので、「初めにダイアログを起動したブックとは異なるブックで作業しようとしても、ダイアログが下に隠れてしまう」ことになります。
もちろん「アクティブなシート」は認識しているので、線を引くセル範囲を選択したのち「裏に隠れているダイアログのボタンをクリック」すれば日程線は引けます。ただし作業性はとても悪いです。
複数のブック上で1つのダイアログを使う時のデメリット
図6-20

これだけならまだ良いのですが「複数のブックが立ち上がっている」状態で、「ダイアログを起動したブックを閉じてしまう」と「ダイアログの実体はまだ存在している事になっているのに、その親のExcelブックが無い状態」になってしまうので、「もう一度ダイアログをShowしようとしても起動しない」ことになるのです。
(この場合は、イミディエイトウィンドウで「End」を実行する必要があります。)

そこで今回は、「ダイアログを起動したブックとは異なるブックで作業しようとしても、ダイアログが隠れてしまう」ので、使い勝手が悪いので「ユーザーは、一旦はダイアログを閉じる」だろうとユーザー行動を勝手に限定しました。
そして「ダイアログを閉じる時には、一旦フォームをUnloadまたはプログラムを終了」させ、「異なるブックで再起動した時にフォームを再起動する」ことが、現在のところの最良策だと考えました。

ですので、図6-19の158行目で「Endステートメント」を実行することにしました。もちろん「Unload Me」でもOKです。なお、この処置は「QueryCloseイベントでは何もしない」ことと一緒ですので、「図6-19のQueryCloseイベントプロシージャ自体を無くす」でもOKです。

また「閉じる」ボタンを追加し、そのClickイベントにプログラムの実行を停止させる「End」ステートメント、または「Unload Me」を記述してもOKです(ダイアログ上のスペースは余計に取ってしまいますが)。

なお「複数ブックを開いて作業する」場面は、他の項でも出てきています。まだチェックが追いついていませんが、同様のことが発生するはずですので、もし発生していたら「Hideでは無く、とりあえずフォームをUnloadする」ように改造して下さい。

7.アドインとしてExcelにマクロを登録

このマクロをExcelの機能の1つに登録し、Excel上部のリボンのボタンを押せばシステムを起動できるようにできます。
その方法については「年賀状リスト等の宛名検索と追記 アドイン登録」を参照下さい。

8.最後に

スケジュール表のフォーマットは職場によって様々です。1日を分割する単位も5分~1時間の間でバラバラ、同じ職場なのにテーブルが隣になっただけで微妙に違ったりもします。
以前いくつかの職場に対して「線引きソフト」を作ったことがありますが、職場によって「日程線はもうちょっと上に表示してくれ」「両端は黒丸にしてくれ」などと細かい要望も。あまり考えずに作り始めたために各職場Onlyの汎用性の乏しいソフトになってしまいました。どんなソフトでも、まずは市場調査が大切だと今更ながら反省しています。

昔のExcelでは、シート上に「図形描画のボタンセット」がフロート表示してくれたので図形を描き易かった気がします。現在のバージョンのExcelの図形ボタンはリボンに固定ですので、今回の「日程線引き」システムを改造して「フロートする図形ボタンセット」を作れば結構便利かもしれません。


両矢印線の図形を日程線としてセル上に描画(it-050.xlsm)

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