2022/07/19

設備の稼働状態を入力し、グラフで確認




1.背景

設備の稼働状態を記録・管理することは、利益を上げるためだけでは無く、設備の健康状態を把握する上でも重要です。
その稼働状態を入力・表示するシステムについては、以前「設備の稼働状態記録とデータ集計」でも紹介しましたが、その時は、Excelワークシートを入力画面 兼 稼働状態表示画面とするもので、データも同じブック内へ保存するタイプでした。

今回紹介するのは、ユーザーフォーム上で入力および表示を行うシステムで、且つデータはデータベースファイル(Accessファイル)に保存するタイプとしました。これにより複数の担当者が同時作業できると共に、異なるExcelファイル(例えば、作業日報)を開いた状態でも入力・確認できることになります。

なお、フォーム上に稼働状態が見える形にする必要がありますが、その手法として当初は「稼働状態ごとの色別の時系列グラフ」をシート上に表示し、表示したグラフを「一旦画像ファイルに変換した後、フォーム上に表示」する手法を考えました。
しかし試してみると、まずフォームをモードレスで表示した場合には「Chartの更新がとても遅い(私のPCで約3秒)」ために、表示日付を移動させても「正しいグラフが表示されない」という結果になりました。誤表示率は100%です。
一方、フォームをモーダルで表示させると改善はされ「Chartの更新は速くなる」のですが、追いつかない場合もあり、誤表示率は20~30%というところです(アドイン状態では無く、xlsmの状態で動かすと一応正常に動きそう)。

そこでグラフを仲介するのは今回あきらめ、モードレスでもリアルタイムで反応してくれる「条件付き書式」を使って「時系列グラフ風なもの」を作り、それを「画像ファイルに変換後フォーム上に表示」させるものを今回の仕様としました。また、フォーム上に並べたLabelの背景色を変化させて「時系列グラフ風」に見せるものも併せて紹介します。

なお、AccessDBを使ったものとして、下記も併せて参照下さい。
ExcelからAccessデータベースを作成・操作
Accessデータベースを使用した売上台帳

2.システム概要

今回システムをアドイン登録し、そこから起動すると図2-1のようなフォームがモードレスで立ち上がります。なおサンプルファイルでは、Sheet1とSheet2の上にそれぞれボタンがありますので、それをクリックすると同様に起動します。
左側も右側もほぼ同じに見えますが、フォーム上の稼働状態グラフの作り方が全く異なります。
入力表示ダイアログの起動
図2-1

左側は「フォームへの画像ファイル貼付け方式(Sheet1、UserForm1)」で、フォーム中央のグラフは「Imageコントロール」にグラフの画像を貼り付けています。
一方右側は「フォーム上ラベルの色変更方式(Sheet2、UserForm2)」で、フォーム上に「Labelコントロール」を24個並べてあり、その背景色をそれぞれ変えることでグラフに見えるようにしています。

フォーム上の各機能は図2-2及び図2-3のようになっています。
フォーム各部の機能
図2-2

No.機能詳細
設備名の選択既に登録してある設備名のコンボボックスリストから選択
日付の選択スクロールバーの左右スクロール矢印のクロックで1日ずつ移動。
選択した日付は上部Labelに表示。尚レール部クリックで1週間移動。
稼働状態を入力・変更各時間帯に対応したボタン。今回は1ボタン/時間。
クリックのたび、無印→A→B→C→D→無印→・・・とトグル上に変化
変更した稼働状態を保存稼働状態データを変更した場合にデータをAccessDBに保存。
稼働状態のグラフ設備名×日付に対応した稼働状態をグラフ表示
フォーム終了フォームを閉じる。閉じる際に自動保存はしていない。
図2-3

下図(図2-4)は、データをまだ入力していない状態から、データを入力する場面を表しています。
なお、今回設定した「設備の状態」は、「A=稼働、B=準備、C=故障、D=修理、無印=停止」の5つとしました。フォーム上に記載されている5種の説明Labelは手入力ですが、記号の「A、B、C、D、無印」はマクロ内で設定しており変更可能です。
ボタンクリックで稼働状態を入力
図2-4

「未入力状態=全日が停止状態」を初期状態としています。その初期状態から図2-4の左図のように「5:00~6:00は稼働(A)」というデータを入力するとします。
グラフ枠の下にある「無印のボタン」を1回クリック⑦すると、ボタンの文字は「A(稼働)」に変わります。ボタンはクリックするたびに「無印→A→B→C→D→無印→・・・」とトグル的に変わります。ダブルクリックのように連続クリックしてもOKです。

ボタンの値を変更した後、右上の「登録ボタン⑧」をクリックするとデータがAccessDBに登録され、中央のグラフが「ボタンの値に対応した色」に変わります⑨。
この「対応した色」は、画像ファイル貼付け方式(UserForm1)では「Sheet1の条件付き書式の背景色を手動で設定」していますが、ラベルの色変更方式(UserForm2)では、マクロ内に設定した色配列に従って色付けをしています。
(条件付き書式の色設定もマクロ側から可能ですが、コードが増えてしまうので、今回は手作業で設定しました。)

図2-4では未入力状態からのデータ入力でしたが、既に入力済みのデータを変更する際も、変更時間帯のボタンをクリックすることでトグル的に値が変わり、値変更後に「登録ボタン⑧」をクリックすることでデータが上書きされます。

なお、設備のコンボボックスと日付のスクロールバーの操作は通常の使い方と同じです。設備および日付を選択した時には、それに対応したグラフが表示されるようにしています。
また、今回のフォームは「モードレス」で起動されますので、他のExcelブックを操作しながらでも作業可能です。

3.プログラムの流れ

3-1.入力した稼働状態の記録

グラフの下の「状態ボタン」をクリックすることで、指定の設備×時間の「稼働状態」を設定します。その後でフォーム右上の「登録ボタン」をクリックした時の流れが図3-1になります。
ボタンクリックで稼働状態を入力
図3-1

まずDBテーブルから、登録されている「設備稼働状態」を取得し、そのデータを「記号の配列」に格納します。その際「日付+時刻」のデータは加工し、0,1,2,・・・と「正時」の整数値に直して配列のインデックスに置き換えます。もし対象時刻のデータが無い場合は「配列のその要素は空」になりますし、入力が初めての場合(設備×日付の組み合わせが無い)は、DBデータは空なので「全て空データの配列」となります。

その「記号の配列」とフォーム上の状態ボタンの値を1つ1つ比較し、「異なっていた場合」のみ、DBファイルへの処理を行います。SQL処理としては、元々データが無かった場合はInsertになりますし、元データがあった場合はUpdate、ボタンの値が「""(長さゼロの文字列)」だった場合はDeleteとなります。

なお、時刻を正時の整数にし24行の配列(=24時間分)のインデックスにする理由は、24個のボタンの位置と照合し易くするためであり、また配列値をそのままワークシートに貼り付けることが出来るからです。また図3-3のLabelの番号と照合する場合もコードが単純になります。
寄り道
今回システムでは、ボタンは24個(1時間に1個)あります。処理の方法として、単純に1つ1つのボタンの値に対し、DB内に同じ「設備×日付・時刻」のデータが有るか否か確認し、Insert・Update・Deleteをするというのがまず思い浮かびます。しかしその方法を実際にやってみると、1つのボタンに対し「データ有無確認 → データ処理」という2つのSQL文の実行が必要となりますので、ボタン24個分(合計で約48回)で約6秒(私のPCで)かかってしまう結果となりました。
これではあまりにも遅く、SQL文の発行回数を減らすために「変更した箇所だけ、データを変更(新規・更新・削除)」することと併せ、「元の設備稼働状態」については、設備×日付で一括でデータを取得し配列化しておくことにしました。

その改善の結果、変更した箇所+1回のSQL文の実行で完了できるようになり、遅くても3秒で処理が終わることとなりました。
(更に速くするには、複数のSQL文をまとめて実行するストアドプロシージャなどが考えられますが、今回は未トライです。)

3-2.稼働状態グラフの表示(画像ファイル貼付け方式)

稼働状態グラフを表示させるタイミングは、設備を選択し直した時、日付を変更した時、および登録処理をした後 となります。
まず、DBファイルから「設備稼働状態」を取得し、そのデータを記号の配列に格納します。これは上記の配列と同じもので、24要素の配列となります。その配列をワークシートに値貼り付けます。
貼り付け先には「条件付き書式」が設定してあり、貼り付けた記号(今回だと、A・B・C・D)により彩色するようになっています(貼り付けた記号欄の一つ下の行も含めて、条件付き書式の適用先に設定)。
ボタンクリックで稼働状態を入力
図3-2

条件付き書式で彩色されたセル範囲を画像としてコピーし、新たに作成したChart(グラフ)上に貼付け、Exportメソッドを使って実ファイル(今回はBMPファイル)にします。保存先は、とりあえずExcelファイルと同じ場所です。
その画像ファイルの保存先をフォームのImageコントロールに設定することで、グラフ画像(元は条件付き書式でまだらに彩色されたセル範囲)が表示されます。

3-3.稼働状態グラフの表示(ラベルの色変更方式)

フォーム上のLabelを使用する場合も、上記同様にDBファイルから「設備稼働状態」を取得し、そのデータを記号の24要素の配列に格納します。
ボタンクリックで稼働状態を入力
図3-3

その配列データを使って、フォーム上のLabel(24個)の背景色を変更していきます。設定する背景色の指定は、マクロ内で初期設定されています。
なお、稼働状態グラフの下に並んだ「設定ボタン」の表面文字も同時に、記号の配列のデータを使って書き込みます。この作業は「画像ファイル貼付け方式」でも同じように行います。

4.標準モジュール

4-1.宣言部

標準モジュール先頭の宣言部では、システム内で使用する定数・変数の宣言を行っています。
  1. '========== ⇩(1) 定数・変数の宣言 ============
  2. Public markArray As Variant        '←稼働マーク一覧
  3. Public ColorArray As Variant       '←マーク色一覧
  4. Public MachineArray As Variant      '←設備一覧
  5. Public Const TBL As String = "MC_cond"     '←テーブル名(今回、テーブルは1つ)
  6. Public Const SystemStartDay As Date = #1/1/2022#    '←システム開始日
  7. Private DBpath As String       '←データベースファイルのパス
  8. Private Const DBname As String = "it-084.accdb"    '←データベースファイルのファイル名
  9. Public BMPpath As String       '←画像ファイルのパス
  10. Public Const BMPname As String = "it-084.bmp"     '←画像ファイルのファイル名
図4-1

01行目「Public markArray As Variant」では、稼働マーク(今回は、A・B・C・D)を格納する配列markArrayを宣言しています。マークを使用したのは「データベースに収めるデータとして、例えば『稼働』よりも『A』の方がデータ量が少ない」のもありますが、主は「状態ボタンに表示する文字列として、1文字の方がボタンを小さくできる」ためです。図4-2の22行目でArray関数を使って値を代入しますので、Variant型としています。
02行目「Public ColorArray As Variant」では、稼働マークごとの色番号を格納する配列ColorArrayを宣言しています。図4-2の23行目でArray関数を使って値を代入します。
03行目「Public MachineArray As Variant」では、設備名を格納する配列MachineArrayを宣言しています。図4-2の24行目でArray関数を使って値を代入します。
04行目「Public Const TBL As String = "MC_cond"」では、今回使用するテーブル名「MC_cond」を代用する定数TBLを宣言しています。テーブル名が気に入らない場合は、この定数値を変更するだけで、コード内のSQL文を修正する必要はありません。
05行目「Public Const SystemStartDay As Date = #1/1/2022#」は、システムのスタート日を定数設定しています。但しこの定数は「日付設定のスクロールバーのValue値」で差引しているだけですので、システム運用後に変更しても問題ないはずです。スクロールバーのValue値を無駄に大きくしないために設定しました。

07行目「Private DBpath As String」は、データベースファイルのパスの変数で、図4-2の26行目で値を代入します。
08行目「Private Const DBname As String = "it-084.accdb"」は、データベースファイルのファイル名の設定です。
09行目「Public BMPpath As String」は、画像ファイル貼付け方式で使用する画像ファイル(拡張子.BMP)を置くパス位置の変数です。図4-2の27行目で値を代入します。
10行目「Public Const BMPname As String = "it-084.bmp"」は、画像ファイルのファイル名の設定です。

4-2.初期設定と起動

システム起動のプロシージャ(図4-3、図4-4)から呼び出される「システム変数への値代入」を行うのが図4-2です。
  1. '========== ⇩(2) システム設定 ============
  2. Private Sub SystemIni()
  3.  markArray = Array("", "A", "B", "C", "D")
  4.  ColorArray = Array(RGB(255, 255, 255), RGB(0, 0, 255), RGB(0, 128, 0), RGB(255, 0, 0), RGB(255, 255, 0))
  5.  MachineArray = Array("旋盤", "フライス盤", "裁断機")
  6.  DBpath = ThisWorkbook.Path & "¥"     '←データベースファイルのパス設定
  7.  BMPpath = ThisWorkbook.Path & "¥"    '←画像ファイルのパス設定
  8. End Sub
図4-2

22行目「markArray = Array("", "A", "B", "C", "D")」は、4つの稼働マーク(A・B・C・D )と、停止や未入力を表す無印を配列に代入しています。各マークの意味は、A=稼働、B=準備、C=故障、D=修理 としていますが、その紐付けはフォーム上の表示のみです。
23行目「ColorArray = Array(RGB(255, 255, 255), RGB(0, 0, 255), RGB(0, 128, 0), RGB(255, 0, 0), RGB(255, 255, 0))」は上記マークの色を定めています。今回は、無印=停止・未入力=白色、A=稼働=青色、B=準備=緑色、C=故障=赤色、D=修理=黄色 としました。
24行目「MachineArray = Array("旋盤", "フライス盤", "裁断機")」では、設備として3つを登録しています。この順番でコンボボックスのリストに表示されることになります。
なお通常であれば、設備名は別のテーブルに置き、設備番号などで本体のテーブルと結びつけるのでしょうが、今回は本体テーブルに直接設備名を書き込むタイプとしました。デメリットとしては、運用途中で設備名を変更した、設備を更新したが同じ設備名にした などの時に、古いデータが取り出せなくなったり、新旧設備のデータが混じったりしてしまいますので、注意が必要です。

26行目「DBpath = ThisWorkbook.Path & "¥"」は、DBファイルの場所を指定しています。ここでは、このExcelファイルと同じ場所にしていますが、実際にはファイルサーバー等の皆がアクセス可能な場所を設定する事になります。その場合には26行目のように変数へのパス名代入でも良いのですが、図4-1の07行目を定数宣言にしてパス名を設定すると良いと思います。
27行目「BMPpath = ThisWorkbook.Path & "¥"」は、画像ファイル貼付け方式で使用する画像ファイルを置く場所の設定です。この設定は上記DBpathのようにファイルサーバー上でなくとも、自分だけのパス(アドインした場合には、アドインファイル先=ThisWorkbook.Path)でもOKと思います。

「画像ファイル貼付け方式」でシステムを起動する場合は、図4-3を実行します。アドインに登録した場合のマクロ登録はこのプロシージャを登録します。なお、サンプルファイルではSheet1上のボタンにこのプロシージャが登録されています。
  1. '========== ⇩(3) フォーム1の起動 ============
  2. Public Sub SystemStart1()
  3.  Call SystemIni
  4.  Call CheckDB
  5.  UserForm1.Show 0
  6. End Sub
図4-3

フォームの起動前に、32行目「Call SystemIni」で図4-2を呼出し、システムで使用する変数の設定をします。
33行目「Call CheckDB」では図4-5を呼び出し、DBファイル・テーブルの存在を確認した後、存在していない場合は作成する作業をします。
その後で35行目「UserForm1.Show 0」でフォーム(UserForm1)をモードレスで起動します。なお作業に支障なければモーダルで起動してもOKです。

「ラベルの色変更方式」でシステムを起動する場合は、図4-4を実行します。アドインに登録した場合のマクロ登録はこのプロシージャを登録します。なお、サンプルファイルではSheet2上のボタンにこのプロシージャが登録されています。
  1. '========== ⇩(4) フォーム2の起動 ============
  2. Public Sub SystemStart2()
  3.  Call SystemIni
  4.  Call CheckDB
  5.  UserForm2.Show 0
  6. End Sub
図4-4

42行目「Call SystemIni」で図4-2を呼出し、システムで使用する変数の設定をします。
43行目「Call CheckDB」では図4-5を呼び出し、DBファイル・テーブルの存在を確認し、存在していない場合は作成する作業をします。
その後で45行目「UserForm2.Show 0」でフォーム(UserForm2)をモードレスで起動します。

4-3.DBファイル・テーブルの準備

図4-3の33行目、図4-4の43行目から呼び出される「DBファイル・テーブルの存在を確認し、存在していない場合は作成」するプロシージャが図4-5です。
加えて、指定したパスが違っていたり、ネットワークが繋がっていなかった場合にはコメントを出すようにもしています。
  1. '========== ⇩(5) DBファイル・テーブルの正常化 ============
  2. Private Sub CheckDB()
  3.  Select Case isMissingDB2
  4.   Case 1
  5.    On Error Resume Next
  6.     Call makeDB
  7.     Call makeTable
  8.     If Not Err.Number = 0 Then
  9.      MsgBox "DBファイルの指定フォルダが違っているか、読取専用の可能性があります"
  10.      End
  11.     End If
  12.    On Error GoTo 0
  13.   Case 2
  14.    Call makeTable
  15.   Case 3
  16.    MsgBox "DBファイルの設定場所(サーバー名等)が違っているか、ネットワークが不通です"
  17.    End
  18.  End Select
  19. End Sub
図4-5

54行目「Select Case isMissingDB2」で図4-7を呼び出し、その戻り値によってCase分けをします。関数プロシージャ isMissingDB2の戻り値の内容は「0=テーブル正常、1=DBファイル無し、2=テーブル無し、3=サーバー名異常」となります。

戻り値が1の場合(55行目「Case 1」)は、56~63行目を実行します。
この場合は「DBファイルが無い」ので、DBファイルを作成し、その中にテーブルを作る必要があります。ですので57行目「Call makeDB」で図4-9を呼び出してDBファイルを作成し、58行目「Call makeTable」で図4-10を呼出してテーブルを作成します。

しかしDBファイルが作れない場合があります。その状況と検出方法を下記にまとめます。
No.エラー内容検出方法
1PC内のPathが違うファイルが作れない
2サーバー先のPathが違うDir関数でエラー発生
3フォルダーが読取専用ファイルが作れない
4ネットワークが繋がらないDir関数でエラー発生
図4-6

この表のNo.1・No.3の場合は図4-7内のDir関数では検出できず、57行目でDBファイルを作成する時に初めてエラーが発生します。ですので56行目「On Error Resume Next」でエラー発生時でもスルーさせ、59行目「If Not Err.Number = 0 Then」で検出をし60行目「MsgBox "DBファイルの指定フォルダが違っているか、読取専用の可能性があります"」でコメントを出します。指定の場所にDBファイルが無く、また作ることも出来ませんので、今回システムを使うことが出来ずに61行目「End」でマクロを終了させます。Endステートメントが実行されると、それ以降のコードが実行されませんので、フォームは起動しないことになります。

戻り値が2の場合(64行目「Case 2」)は「DBファイルの存在は確認できたがテーブルが無い」場合です。
ですので65行目で図4-10のmakeTableを呼出してテーブルを作成します。
この際、DBファイルの存在までは確認できているため、指定したDBファイルのパス名+ファイル名は正しく、またネットワークも繋がっている状態と判断できます。残りは図4-6のNo.3のフォルダーが読取専用か否かですが、このシステムで前回少なくともDBファイルが作れたのであれば、(その後、そのフォルダーを読取専用に変えたとは考えにくいため)テーブルも作れるだろうと判断し56行目のような「On Error Resume Next」でのエラー回避は設けませんでした。
その心配がある場合は、エラー回避とコメントの設定をして下さい。

戻り値が3の場合(66行目「Case 3」)は、図4-6のNo.2・No.4の場合です。
この場合は、67行目「MsgBox "DBファイルの設定場所(サーバー名等)が違っているか、ネットワークが不通です"」でコメントを出し、68行目「End」でマクロを終了させます。

図4-5の54行目から呼び出される「DBファイル・テーブルの存在などを調べる」のが図4-7です。
調べた結果は以下のように戻り値として返します。
 戻り値:0=テーブル正常、1=DBファイル無し、2=テーブル無し、3=サーバー名異常
  1. '========== ⇩(6) DBファイル・テーブルのチェック ============
  2. Private Function isMissingDB2() As Integer
  3.  Dim Cat As Object   '←Catalogオブジェクト
  4.  Dim T As Object    '←Tableオブジェクト
  5.  On Error Resume Next
  6.   If Dir(DBpath & DBname) = "" Then
  7.    If Err.Number = 0 Then
  8.     isMissingDB2 = 1   '←DBファイルが無い
  9.    Else
  10.     isMissingDB2 = 3   '←サーバー名が違う。ネットワーク不通
  11.    End If
  12.    Exit Function
  13.   End If
  14.  On Error GoTo 0
  15.  Set Cat = CreateObject("ADOX.Catalog")
  16.  Cat.ActiveConnection = "Provider=Microsoft.Ace.OLEDB.12.0;" & _
  17.               "Data Source=" & DBpath & DBname
  18.  For Each T In Cat.Tables
  19.   If T.Type = "TABLE" Then
  20.    If T.Name = TBL Then
  21.     isMissingDB2 = 0   '←指定のテーブルが存在する
  22.     Exit Function
  23.    End If
  24.   End If
  25.  Next T
  26.  isMissingDB2 = 2   '←テーブルが無い
  27.  Set Cat = Nothing
  28. End Function
図4-7

まず、86行目「If Dir(DBpath & DBname) = "" Then」でDBファイルの存在を確認します。Dir関数で得られる情報を図4-6よりももう少し細かく見てみます(図4-8)。
No.状況Dirでの結果
1存在しないドライブ名を指定""(長さ0文字列)
2実在ドライブ名+存在しないフォルダ名""(長さ0文字列)
3存在しないサーバー名を指定エラー番号=52
4実在サーバー名+存在しないフォルダー名エラー番号=52
5フォルダーが読取専用無関係
6ネットワークが繋がらないエラー番号=52
7Pathは存在するが、ファイルは存在せず""(長さ0文字列)
8Pathは存在し、ファイルも存在するファイル名を戻す
図4-8

図4-8で分かるように、Dir関数での結果は3種類です。
1つ目は、No.8の「調べているファイルが存在する」場合です。86行目のIf文は「 = ""」ですからこのIf文には引っ掛からずに96行目以降(テーブルの存在確認)を実行することになります。
2つ目は、エラーが発生するNo.3~4の「サーバー先が違う」またはNo.6の「ネットワークが繋がっていない」場合です。この場合、まず85行目「On Error Resume Next」でDir関数で発生するエラーを回避させ、87行目「If Err.Number = 0 Then」でエラー時は90行目「isMissingDB2 = 3」で関数戻り値を3(=サーバー名異常)にセットして92行目「Exit Function」で関数プロシージャを抜け出します。
3つ目はNo.7の「ファイルが存在しない」場合です。この場合はエラーは出ませんので、87行目「If Err.Number = 0 Then」で分岐し、88行目「isMissingDB2 = 1」で関数戻り値を1(=DBファイルが無い)にセットして92行目「Exit Function」で関数プロシージャを抜け出します。
但しNo.1~2の「ドライブ名やフォルダー名が違う」場合にも、ファイルが存在しない時と同じ戻り値となってしまいますので、図4-5の56~63行目でファイルを実際に作成する際にエラーが出たら「ドライブ名やフォルダー名が違っていた」と判断するようにしています。

寄り道
なおNo.5のフォルダーが読取専用か否かはDir関数からでは見分けがつきません。ですので、ファイルが無くて図4-5でファイル作成時にエラーが出た時に「ああ、読取専用だった」と分かります。またファイルがあっても読取専用の時には、今回システムではエラー処理をしていませんので実行時エラーが発生する可能性はあります。

それならばと、図4-5の65行目の「Call makeTable」にエラー処理を加えて・・・という対策をしても不十分です。65行目を実行するのは「DBファイルは存在+テーブルが無い」場合ですので、「DBファイルは存在+テーブルも存在」する場合には、SQLによるデータ書込み時になって初めて発覚するのです。

今回は「知らない間に、フォルダーを読取専用に切り替えられてしまうことは稀だろう」と考え、「DBファイルが以前作れたのだから、テーブルも書き込むことが可能」との考え方で進めています。もし「絶対にエラーでは止めない!」という方針であれば、色々な場所に更にエラー処理を追加する必要がありますが、それと併せて「知らない間に変更」されないように、サーバー管理基準と変更情報の展開についてもチェックした方が良いかもしれません。

96行目以降は、DBファイルの存在が確認できた上での処理となります。
96行目「Set Cat = CreateObject("ADOX.Catalog")」では、ADOXのCatalogオブジェクトを生成し、97~98行目「Cat.ActiveConnection = "Provider=Microsoft.Ace.OLEDB.12.0;" & "Data Source=" & DBpath & DBname」でACE系のプロバイダで、DBファイルに接続します。

100行目「For Each T In Cat.Tables」では、接続したDBファイルのテーブルを1つ1つ取り出して行きます。AccessのDBファイル内には、データを入れるテーブル(Type = TABLE)の他にSYSTEM TABLEなどがありますので、102行目「If T.Type = "TABLE" Then」で選り分けます。この時に "TABLE" は全て半角大文字である必要があり、「Table」などと書くと引っ掛かってくれないので注意が必要です。
103行目「If T.Name = TBL Then」は、102行目で選り分けた「データのテーブル」の中に「今回システムのテーブル(図4-1の04行目で設定したMC_condテーブル)」が存在するかを確認しています。そして今回システムのテーブルが見つかった時には104行目「isMissingDB2 = 0」で戻り値をゼロ(=テーブル正常)に設定し、105行目「Exit Function」で関数プロシージャを抜け出します。
なお、今回システムではテーブルを1つしか使用しないため、確認するテーブルも1つですが、もし複数ある場合は「Accessデータベースを使用した売上台帳」のように複数のテーブルを1つ1つ潰し込んでいくような手法が必要になります。

100~109行目のFor Each~Nextでテーブルを全て調べた結果、今回システムのテーブルが見つからなかった時には111行目「isMissingDB2 = 2」が実行され、戻り値として2(=テーブル無し)が設定されます。

4-4.DBファイル作成

図4-5の57行目から呼び出される「DBファイルを作成」するプロシージャが図4-9です。
  1. '========== ⇩(7) DBファイル作成 ============
  2. Private Sub makeDB()
  3.  Dim Cat As Object     '←Catalogオブジェクト
  4.  Set Cat = CreateObject("ADOX.Catalog")
  5.  Cat.Create "Provider=Microsoft.Ace.OLEDB.12.0;" & _
  6.        "Data Source=" & DBpath & DBname
  7.  Set Cat = Nothing
  8. End Sub
図4-9

124行目「Set Cat = CreateObject("ADOX.Catalog")」で、ADOXのCatalogオブジェクトを生成します。
125行目「Cat.Create "Provider=Microsoft.Ace.OLEDB.12.0;" & "Data Source=" & DBpath & DBname」では、Ace系のプロバイダを使い、DBファイルのパス+ファイル名(DBpath & DBname)を指定して、AccessのDBファイルを作成(Create)します。

4-5.テーブルの作成

図4-5の58行目、65行目から呼び出される「テーブルを作成」するプロシージャが図4-10です。
  1. '========== ⇩(8) テーブル作成 ============
  2. Private Sub makeTable()
  3.  Dim sql As String     '←SQL文
  4.  sql = "Create Table " & TBL & " (" & _
  5.     "DT DATETIME," & _
  6.     "MC STRING," & _
  7.     "Cond STRING," & _
  8.     "PRIMARY KEY (DT,MC)" & _
  9.     ")"
  10.  Call SQL_exec6(sql)
  11. End Sub
図4-10

144~149行目がテーブルを作成するSQL文です。
144行目「sql = "Create Table " & TBL & " (" & _」で変数TBL(図4-1の04行目で設定したテーブル名 MC_cond )のテーブルを作成します。今回システムのテーブルは、図4-11のような構造です。
今回テーブルの構造
図4-11

まず、145行目「"DT DATETIME," & _」でDT列をデータ型DATETIME(日付/時刻型)で作成し、146行目「"MC STRING," & _」でMC列をデータ型STRING(文字列型)で作成し、147行目「"Cond STRING," & _」でCond列をデータ型STRING(文字列型)で作成します。
制約は「DT列+MC列のセットでPRIMARY KEY」ですので、148行目「"PRIMARY KEY (DT,MC)" & _」でカッコ内に2つの列名を列記して設定します。2列セットでの制約ですので、DT列の値が同じでもMC列の値が異なればOKとなります。
例えば「7月7日の10時+旋盤」と「7月7日の10時+フライス盤」は、DT列は同じ値ですがMC列が異なるために共存できることになりますし、「7月7日の10時+旋盤」と「7月7日の11時+旋盤」も共存できます。

SQL文が出来たら、150行目「Call SQL_exec6(sql)」で図4-12を呼出し、Create文を実行してテーブルを作成します。

4-6.SQL文の実行

DBファイル・テーブルに接続し、SQL文を実行するのが図4-12です。引数としてSQL文を受け取ります。
  1. '========== ⇩(9) SQL文の実行 ============
  2. Public Function SQL_exec6(sql As String) As Variant
  3.  Dim cn As Object      '←Connectionオブジェクト
  4.  Dim rs As Object      '←Recordsetオブジェクト
  5.  Dim i As Long        '←カウンタ変数(行数)
  6.  Dim j As Integer      '←カウンタ変数(列数)
  7.  Const adOpenStatic = 3   '←カーソル状態の定数値
  8.  Dim buf() As Variant    '←戻り値の配列
  9.  Set cn = CreateObject("ADODB.Connection")
  10.  Set rs = CreateObject("ADODB.Recordset")
  11.  cn.Provider = "Microsoft.ACE.OLEDB.12.0"
  12.  cn.Open DBpath & DBname
  13.   rs.Open sql, cn, adOpenStatic
  14.   If Not rs.State = 0 Then
  15.    ReDim buf(0 To rs.RecordCount, 1 To rs.Fields.Count)
  16.    For j = 1 To rs.Fields.Count
  17.     buf(0, j) = rs.Fields(j - 1).Name
  18.    Next j
  19.    Do Until rs.EOF
  20.     i = i + 1
  21.     For j = 1 To rs.Fields.Count
  22.      buf(i, j) = IIf(IsNull(rs.Fields(j - 1).Value), "", rs.Fields(j - 1).Value)
  23.     Next j
  24.     rs.MoveNext
  25.     DoEvents: DoEvents
  26.    Loop
  27.    SQL_exec6 = buf
  28.    rs.Close
  29.   End If
  30.  cn.Close
  31.  Set rs = Nothing
  32.  Set cn = Nothing
  33. End Function
図4-12

169行目「Set cn = CreateObject("ADODB.Connection")」では、ADOのConnectionオブジェクトを生成します。
170行目「Set rs = CreateObject("ADODB.Recordset")」では、ADOのRecordsetオブジェクトを生成します。

172行目「cn.Provider = "Microsoft.ACE.OLEDB.12.0"」で、Ace系のプロバイダを設定します。
173行目「cn.Open DBpath & DBname」で、DBファイルを開きます。
174行目「rs.Open sql, cn, adOpenStatic」で、引数で得たSQL文を実行します。その時のカーソルとして、Select文で得た行数を取得できる静的カーソル(adOpenStatic)を指定します。どのカーソルを指定すれば良いかについては「Accessデータベースを使用した売上台帳」を参照下さい。
なお今回はCreateObjectを使った実行時バインディングを使用していますので、定数adOpenStaticは有効になっていません。ですので166行目「Const adOpenStatic = 3」で定数宣言をしています。

176行目の、「rs.Open」を実行した後のRecordset(rs)の状態は、実行するSQL文で違います。
Insert文、Update文、Delete文の場合は、rs.Openでテーブルへの操作が完了し、Recordsetは閉じてしまいます。一方Select文の場合は、指定したレコードからデータを取り出すために、rs.Open直後はRecordsetは開いている状態です。

そのRecordsetの状態を176行目「If Not rs.State = 0 Then」で取得し、開いている(=Select文)場合だけ177~193行目の「データを取り出す作業」を行います。RecordsetオブジェクトのStateプロパティは図4-13のようになっています。
Recordset.Stateプロパティ
定数内容
adStateClosed0閉じている
adStateOpen1開いている
adStateConnecting2接続中
adStateExecuting4コマンド実行中
adStateFetching8行を取得中
図4-13

まず177行目「ReDim buf(0 To rs.RecordCount, 1 To rs.Fields.Count)」で、データを格納する配列のサイズを定めます。静的カーソル(adOpenStatic)でRecordsetを開きましたので、行数はRecordCountプロパティで取得が可能ですし、列数はFields.Countで得られます。
なおデータは1行目から格納するのですが、ゼロ行目から設定しています。その理由は「Selectの結果が何行であっても、配列を二次元の状態に保ち、後の処理を楽にする」ためで、詳細は「Accessデータベースを使用した売上台帳」を参照願います。

179~181行目のFor~Nextでは、配列のゼロ行目に「配列の列タイトル」として列名を代入します。なお、今回システムではゼロ行目の列名は利用しないため、実行しなくてもOKです。
179行目「For j = 1 To rs.Fields.Count」でカウンタ変数jを列数分だけ回し、180行目「buf(0, j) = rs.Fields(j - 1).Name」でゼロ行目に列名(.Name)を書き込んでいます。

183~190行目のDo~Loopでは、取得したレコードデータを配列に格納しています。
183行目「Do Until rs.EOF」では、レコードが無くなる(EOF = End of File)までDo~Loopを回します。
184行目「i = i + 1」は、行位置を1つずつ移動していきます。配列の代入前にカウンタ変数iを増やしますので、最初のレコードがi=1となります。
185行目「For j = 1 To rs.Fields.Count」では、列位置のカウンタ変数jを列数分だけ移動していきます。
186行目「buf(i, j) = IIf(IsNull(rs.Fields(j - 1).Value), "", rs.Fields(j - 1).Value)」は、配列の各要素にデータを代入していきます。なお完成した配列内にNullが入っていると、Listに貼り付ける時などにエラーが発生しますので、配列に代入する前にIIFを使って「データがNullの時には、長さゼロの文字列に変換」をしてます。
188行目「rs.MoveNext」は、カーソル位置を次のレコードに移動します。

配列にデータの格納が完了したら、192行目「SQL_exec6 = buf」でこの関数プロシージャの戻り値に設定します。
193行目「rs.Close」ではRecordsetを閉じます。なお、Insert文などではrs.Openの直後に閉じた状態になりますので、更にCloseメソッドを実行しようとするとエラーが出ます。rs.Closeをして良いのはSelect文だけですので注意が必要です。

5.フォームへの画像ファイル貼付け方式

5-1.ワークシート(Sheet1)

Sheet1上に「設備状態グラフの元」を条件付き書式を使って作成します。
図5-1のように、データの貼り付けセルは3行目(C3~Z3セル)、グラフの元となるセル範囲は4行目(C4~Z4セル)としました。
データ貼付け範囲とグラフ取得範囲
図5-1

そしてその両方の範囲(C3~Z4セル)に対して、図5-2のように条件付き書式を設定します。数式は、例えば状態が「稼働(記号=A)」であれば「= C$3 = "A"」と列単位で働く数式とし、数式が成立した時は図4-2の23行目で設定した色と同じ背景色(稼働であれば青色)になるようにします。
条件付き書式の内容
図5-2

今回システムでは、稼働状態は5つ(停止・稼働・準備・故障・修理)ですが、停止(=未入力)の白色は「何も背景色のないセル色」ですので、条件付き書式のどの数式にも当てはまらない場合はセル色(≒白色)としています。
但しユーザーの設定環境によっては、標準のセルに色が付いている場合があります。その場合は条件付き書式のルールを1つ増やし、「= C$3 = ""」の数式で背景色を白色にするなどの対応が必要となります。

5-2.ユーザーフォーム(UserForm1)

5-2-1.フォーム上のコントロール類配置

フォームには、上部にユーザーが選択する「設備名のComboBox」と「日付のScrollBar」を配置し、ScrollBarの上に日付を表示するLabelを置きます。また右上には登録と終了の2つのCommandButtonを置きます。
フォーム上のコントロール類配置
図5-3

フォーム中央にはImageコントロールを置きます。Imageが設備状態グラフになります。Imageの上と下には少しスペースを空けておき、Imageの下には状態ボタンを、Imageの上には時刻のLabelをマクロから生成します。生成するボタンやLabelは、Imageコントロールの位置と幅に合わせて作成しますので、例えば幅を少なくしてしまうとボタンの表示がおかしくなる可能性があります。

また今回は「フォーム自体の大きさ」を調整していません。例えば縦方向が少ないとボタンがフォームからはみ出してしまう可能性もあります。もし自由にボタンを作成・配置するようなアプリを作る場合は、ボタンの大きさ・数からフォームの大きさを計算しサイズを自動調整するような工夫が必要と思います。

5-2-2.フォームモジュール

5-2-2-1.宣言部
フォームモジュール先頭では、フォーム内で使用する変数の宣言、およびマクロで作成する状態ボタンのイベントを使用するための宣言をします。
  1. '========== ⇩(10) モジュールレベル変数の宣言 ============
  2. Private WithEvents myButton As MSForms.CommandButton
  3. Dim Barray() As New UserForm1
  4. Dim EventOFF As Boolean
図5-4

211行目「Private WithEvents myButton As MSForms.CommandButton」は、makeButton(図5-11)で作成する「状態を入力する24個のCommandButton」をクリックした時にClickイベントを取得するためのWithEvents宣言です。
212行目「Dim Barray() As New UserForm1」は、状態ボタンを24個作成しますが、どのボタンが押されたかを把握する目的で、1つ1つのボタンにインデックスを登録するための配列の宣言です。ButtonのArrayという意味で命名しました。

設備名選択のComboBoxや日付選択のScrollBarには、操作した時に反応するためのイベントを仕込んでいます。しかし、ComboBoxやScrollBarの準備段階ではイベントに反応して欲しく無いので、イベントを中断させるための変数として213行目「Dim EventOFF As Boolean」でフラグ変数を宣言しています。

5-2-2-2.カスタムプロパティ
作成したボタンのプロパティ値を取得する時に呼び出すのが図5-5です。
222行目「Set Button = myButton」で、作成したボタン(myButton)のプロパティを呼出し先に戻しています。
  1. '========== ⇩(11) ボタンのプロパティ値を取得 ============
  2. Public Property Get Button() As Control
  3.  Set Button = myButton
  4. End Property
図5-5

ボタンのオブジェクトを設定するのが図5-6です。引数として「オブジェクトへの参照である変数addBTN」を受け取ります。232行目「Set myButton = addBTN」で、受け取ったオブジェクトをボタンに設定します。
  1. '========== ⇩(12) ボタンのオブジェクトを設定 ============
  2. Public Property Set Button(ByVal addBTN As Control)
  3.  Set myButton = addBTN
  4. End Property
図5-6

5-2-2-3.フォームの初期化
フォームの起動時にまず実行されるのが、図5-7のInitializeイベントです。
  1. '========== ⇩(13) Initializeイベント ============
  2. Private Sub UserForm_Initialize()
  3.  Me.CommandButton1.Caption = "登録"
  4.  Me.CommandButton2.Caption = "終了"
  5.  Me.ComboBox1.Style = fmStyleDropDownList
  6.  Me.ScrollBar1.Min = 0
  7.  Me.ScrollBar1.Max = 3660
  8.  Me.ScrollBar1.LargeChange = 7
  9.  Me.Image1.PictureSizeMode = fmPictureSizeModeStretch
  10. End Sub
図5-7

242行目「Me.CommandButton1.Caption = "登録"」と243行目「Me.CommandButton2.Caption = "終了"」では、配置したCommandButtonの表面文字を設定しています。
245行目「Me.ComboBox1.Style = fmStyleDropDownList」では、設備選択用のComboBoxをリストボックスとして使用する設定をします。Styleプロパティに設定可能な値は図5-8の2種です。
ComboBox.Styleプロパティ
定数内容
fmStyleDropDownCombo0編集領域への値入力可 + ドロップダウンリストからの選択可(既定)
fmStyleDropDownList2ドロップダウンリストからのみ選択可
図5-8

今回のように、Styleに「fmStyleDropDownList」を設定することで、編集領域(コンボボックス一番上のテキストボックスの様に入力できる部分)には、「リストに無い値」を入力・保持することが出来なくなります。例えばリスト内に「フライス盤」があったとすると、編集領域に「フライス」や「フライス盤2」などの文字は入力できなくなります。
(実際には、今回システムでStyleプロパティを既定のfmStyleDropDownComboに設定すると、編集領域を変更した時にComboBoxのChangeイベントが発生し状態グラフの再表示を行おうとします。その時、選択中の項目を取得する時に「リストから項目を選んでいない:ListIndex = -1 」ためにエラーが出てしまうことになります。)

247行目「Me.ScrollBar1.Min = 0」は、日付選択のスクロールバーの最小値をゼロ(=図4-1の05行目で設定した、システム開始日)にし、248行目「Me.ScrollBar1.Max = 3660」では最大値を約10年後(≒365日×10年)にしています。
249行目「Me.ScrollBar1.LargeChange = 7」は、スクロールバーのレール部をクリックした時には、1週間(=7日間)まとめて移動するようにします。

251行目「Me.Image1.PictureSizeMode = fmPictureSizeModeStretch」は、図5-9のように「Imageコントロールにピッタリ合うように引き延ばす」指定です。この設定は画像が歪む可能性がありますが、今回はストライプ模様のため、歪んでも問題ありません。
Image.PictureSizeModeプロパティ
定数内容
fmPictureSizeModeClip0はみ出し部分をトリミング(既定)
fmPictureSizeModeStretch1サイズに合わせて引き延ばす
fmPictureSizeModeZoom3歪まない様に引き延ばす
図5-9

Initializeの次に、フォームが表示される時に実行されるのが図5-10のActivateイベントです。ここではフォームの動的な設定をしています。
  1. '========== ⇩(14) Activateイベント ============
  2. Private Sub UserForm_Activate()
  3.  EventOFF = True
  4.   Me.ScrollBar1.Value = Date - SystemStartDay
  5.   Call makeButton
  6.   Call makeTimeLabel
  7.   Call makeCombo
  8.  EventOFF = False
  9.  Call OperateCondDisp
  10. End Sub
図5-10

264行目「Me.ScrollBar1.Value = Date - SystemStartDay」は、日付選択のスクロールバーのスクロールボックスを今日の位置にしています。図4-1の05行目でシステム開始日(スクロールバーの値=ゼロ)を設定していますので、開始日の日付を引いた値がスクロールバーの値になります。
265行目「Call makeButton」では図5-11を呼び出し、設備稼働状態ボタンを作成します。
266行目「Call makeTimeLabel」では図5-14を呼び出し、Imageコントロールの上側に時刻を表示します。
267行目「Call makeCombo」では図5-17を呼び出し、設備選択用のコンボボックスのリストを作成します。

なお今回システムでは、スクロールバーやコンボボックスを操作するのに合わせて設備稼働状態グラフを再表示させますので、264~267行目での設定時にも再表示をしようとします。しかし、全てが整った状態になっていない為にエラーが出ることになります。(makeButtonとmakeTimeLabelではエラーは出ませんが、ひっくるめて「フォーム準備時」としています。)
そこで、263行目「EventOFF = True」でフラグ変数のフラグを立て、実行先のChangeイベント(図5-18、図5-19)でフラグが立っている時にはすぐに抜け出すことにしています。

全ての準備が完了したら、270行目「Call OperateCondDisp」で図5-31を呼出し、起動直後の状態の設備稼働状態グラフを表示させます。

5-2-2-4.ボタンの作成
図5-10の265行目から呼び出されるのが図5-11です。図5-11はボタンを配列Barrayに登録する役目で、実際にボタンを作成する作業は図5-13のaddBTN関数プロシージャに委託しています。
  1. '========== ⇩(15) 稼働状態ボタンの登録 ============
  2. Private Sub makeButton()
  3.  Dim i As Integer   '←ボタンの個数
  4.  ReDim Barray(0 To 23)
  5.  With Me.Image1
  6.   For i = 0 To 23
  7.    Set Barray(i).Button = _
  8.      addBTN(.Top + .Height, .Left + i * .Width / 24, .Width / 24, .Width / 24, "")
  9.   Next i
  10.  End With
  11. End Sub
図5-11

284行目「ReDim Barray(0 To 23)」は、図5-4の212行目で設定した配列にインデックスを付けています。今回は24個のボタンを作りますので、ゼロ~23としています。

288~289行目「Set Barray(i).Button = addBTN(.Top + .Height, .Left + i * .Width / 24, .Width / 24, .Width / 24, "")」では図5-13を呼出し、引数で指定した位置・サイズ、及び表面文字列でボタンを作成します。
引数は5つ有り、第一引数から「上下位置」「左右位置」「高さ」「幅」「表面文字列」となります。フォーム上に配置したImage1を基準(286行目「With Me.Image1」)に、その下に24個並べるために、指定位置は図5-12のようにしました。
作成するボタンの位置
図5-12

その5つの引数を渡してボタンが作成されると、addBTN関数は「作ったボタンのオブジェクト」を戻してきます。そのオブジェクトを図5-6の「ボタンのオブジェクトを設定」するプロパティに渡し、配列Barrayの1つの要素として登録します。
この作業を287行目「For i = 0 To 23」で、カウンタ変数iを配列Barrayのインデックス分(今回24回)だけ回します。

なお、上記コード内で使用している数値の「23」は「Ubound(Barray,1)」、「24」は「Ubound(Barray,1) - Lbound(Barray,1) + 1」などと記載するのが本当なのでしょうが、コードが長くなり流れが掴み難くなると考え、あえて数値としました。御理解下さい。

図5-11の289行目から呼び出される「ボタンを作成し、そのオブジェクトを返す」関数が図5-13です。引数として、作成するボタンの「上下位置」「左右位置」「高さ」「幅」「表面文字列」を受け取ります。なお今回システムでは、ボタンを新規作成する際には「ボタン表面文字列は全て""(長さゼロの文字列)」ですが、汎用性を持たせるために引数の1つにしています。
  1. '========== ⇩(16) ボタンの作成 ============
  2. Private Function addBTN(T As Single, L As Single, _
  3.              H As Single, W As Single, _
  4.              Bcaption As String) As MSForms.CommandButton
  5.  Set addBTN = Me.Controls.Add("Forms.CommandButton.1")
  6.  With addBTN
  7.   .Top = T
  8.   .Left = L
  9.   .Height = H
  10.   .Width = W
  11.   .Caption = Bcaption
  12.  End With
  13. End Function
図5-13

305行目「Set addBTN = Me.Controls.Add("Forms.CommandButton.1")」で、「Forms.CommandButton」のコントロール(=状態ボタン)を作成します。そしてそのオブジェクトを関数プロシージャの戻り値に設定しています。
305行目を実行した時点で、既にボタンは作られているのですが、位置もサイズも指定していないため既定の位置「フォームの左上端(Top=0、Left=0 の場所)」に、既定のサイズ「(私のPCでは)Height=24、Width=72」で作られています。

既定で作られたボタンを307~313行目で位置・サイズを整え、表面文字列(.Caption)を設定します。なお、今回フォントサイズ(.Font.Size)は変更していません。私のPCでは「9ポイント(既定値?)」になっており、ボタンの文字サイズとしては問題なかったため設定を省略しましたが、不具合ある場合にはフォントサイズも設定が必要になります。

5-2-2-5.時刻ラベルの作成
図5-10の266行目から呼び出されるのが図5-14です。図5-14は状態グラフの上部に時刻のラベルの配置位置・サイズ・表示文字を指定する役目で、実際にラベルを作成する作業は図5-16のaddLBL関数プロシージャに委託しています。
  1. '========== ⇩(17) 時刻ラベルの作成指示 ============
  2. Private Sub makeTimeLabel()
  3.  Dim i As Integer   '←ラベルの個数
  4.  With Me.Image1
  5.   For i = 0 To 24
  6.    addLBL .Top - 10, .Left + i * .Width / 24 - 3, 10, .Width / 24, CStr(i)
  7.   Next i
  8.  End With
  9. End Sub
図5-14

稼働状態ボタンとは異なり、時刻ラベルは「クリック等によるイベントを取得しない」また「表示文字などのプロパティを変更しない」ため、単にラベルを作成するだけになります。
326行目「addLBL .Top - 10, .Left + i * .Width / 24 - 3, 10, .Width / 24, CStr(i)」で図5-16を呼び出し、引数で指定した位置・サイズ、及び文字列でラベルを作成します。
引数はボタンの時と同じ5つで、第一引数から「上下位置」「左右位置」「高さ」「幅」「表示文字列」となります。フォーム上に配置したImage1を基準(324行目「With Me.Image1」)に、その上側に25個並べるために、指定位置は図5-15のようにしました。
作成するラベルの位置
図5-15

指定した位置サイズでのラベル作成を、325行目「For i = 0 To 24」で繰り返します。なお、ゼロから24と「全25個」となっているのは、次の日の0時である「24」の文字列を右端に表示しているためです。
また、横位置を「.Left + i * .Width / 24 - 3」と「-3だけ微調整」しているのは、ラべルの数字を時刻の線上に持ってくるためにTry & Errorした結果です。

図5-14の326行目から呼び出される「ラベルを作成し、そのオブジェクトを返す」関数が図5-16です。引数として、ラベルの「上下位置」「左右位置」「高さ」「幅」「表示文字列」を受け取ります。
  1. '========== ⇩(18) ラベルの作成 ============
  2. Private Function addLBL(T As Single, L As Single, _
  3.              H As Single, W As Single, _
  4.              Lcaption As String) As MSForms.Label
  5.  Set addLBL = Me.Controls.Add("Forms.label.1")
  6.  With addLBL
  7.   .Top = T
  8.   .Left = L
  9.   .Height = H
  10.   .Width = W
  11.   .Caption = Lcaption
  12.  End With
  13. End Function
図5-16

345行目「Set addLBL = Me.Controls.Add("Forms.label.1")」で、「Forms.label」のコントロール(=時刻のラベル)を作成し、そのオブジェクトを関数プロシージャの戻り値に設定しています。なお図5-14では、その戻り値は使っていません。

345行目を実行した時点では、作られたラベルは既定の位置「フォームの左上端(Top=0、Left=0 の場所)」に、既定のサイズ「(私のPCでは)Height=18、Width=72」となっています。
既定で作られたボタンを347~353行目で位置・サイズを整え、文字列(.Caption)を設定します。なお、今回フォントサイズ(.Font.Size)は変更していません。私のPCでは「9ポイント(既定値?)」になっており、ラベルの文字サイズとしては問題なかったため設定を省略しましたが、不具合ある場合にはフォントサイズも設定が必要になります。

5-2-2-6.コンボボックスの作成
図5-10の267行目から呼び出される「設備名の選択用コンボボックスのリストを作成」するのが図5-17です。
  1. '========== ⇩(19) コンボボックスの作成 ============
  2. Private Sub makeCombo()
  3.  Dim i As Integer   '←リストの個数
  4.  Me.ComboBox1.Clear
  5.  For i = 0 To UBound(MachineArray, 1)
  6.   Me.ComboBox1.AddItem MachineArray(i)
  7.  Next i
  8.  Me.ComboBox1.ListIndex = 0
  9. End Sub
図5-17

364行目「Me.ComboBox1.Clear」は、一旦コンボボックスのリストをクリアしています。今回システムでは起動時にしかmakeComboプロシージャを実行しませんし、且つフォーム終了もHideでは無くUnloadを使っていますので、Clearは必須ではありません。しかし、フォーム上から設備名を追加できるなどの改造をした場合は、Clearをしないと項目が重複してしまいますので、念の為実行しています。

設備名は、図4-2の24行目で設定済みのMachineArray配列に格納されていますので、366行目「For i = 0 To UBound(MachineArray, 1)」で配列の要素数分だけ回しながら、367行目「Me.ComboBox1.AddItem MachineArray(i)」で1要素ずつリストに追加しています。

リスト完成後、370行目「Me.ComboBox1.ListIndex = 0」で、先頭の設備名を強制的に表示させています。これは、フォーム起動直後に稼働状態グラフを表示(図5-10の270行目)させていますが、その時に「設備名が選択状態で無い」とエラーが出てしまうためです。

5-2-2-7.設備・日付の選択
設備名のコンボボックスを操作した時には、図5-18のChangeイベントが発生します。
  1. '========== ⇩(20) 設備の選択 ============
  2. Private Sub ComboBox1_Change()
  3.  If EventOFF = True Then Exit Sub
  4.  Call OperateCondDisp
  5. End Sub
図5-18

コンボボックスを作成した後、先頭の項目を表示させています(図5-17の370行目)が、その時にも図5-18が呼び出されます。しかし、呼出し元の図5-10の267行目(Call makeCombo)は、その数行前の263行目「EventOff = True」でフラグ変数のフラグを立てている(Trueにしている)ため、382行目「If EventOFF = True Then Exit Sub」でChangeイベントを抜け出すことになります。

ユーザーの手で設備名を変更した場合には、384行目「Call OperateCondDisp」で図5-31が呼び出され、Imageコントロールに状態グラフが表示されることになります。
寄り道
図5-10の267行目「Call makeCombo」を実行する前にEventOffをFalse設定にすれば、384行目「Call OperateCondDisp」により状態グラフを作成してくれます。そうすれば、図5-10の270行目「Call OperateCondDisp」は不要になりますし、また382行目「If EventOFF = True Then Exit Sub」もいらなくなります。

しかし、これでうまく行くのは図5-10の中で「Call makeCombo」が最後に実行されるからに過ぎません。フォームの準備時にやむを得ず発生してしまうイベントを利用するのは、上手なやり方に見えるかもしれませんが、私には危険な手法に思えます。
やはり、準備が完全にできてから「明示的に図5-10の270行目『Call OperateCondDisp』で、初めて状態グラフを表示させる」のが正しい姿だと思います。

日付選択のスクロールバーを動かした時には、図5-19のChangeイベントが発生します。
  1. '========== ⇩(21) 日付の選択 ============
  2. Private Sub ScrollBar1_Change()
  3.  Me.Label1.Caption = SystemStartDay + Me.ScrollBar1.Value
  4.  If EventOFF = True Then Exit Sub
  5.  Call OperateCondDisp
  6. End Sub
図5-19

このイベントが発生する時はスクロールバーのValue値が変更されているので、変更に合わせて393行目「Me.Label1.Caption = SystemStartDay + Me.ScrollBar1.Value」で、スクロールバー上部のLabel1にValue値相当の日付を書き込みます。
SystemStartDayはDate型でScrollBar1.ValueはLong型ですが、それを合わせるとDate型となりますので日付の形でラベルの文字列となります。

395行目「If EventOFF = True Then Exit Sub」は、Activateイベント内でフォームの準備をしている最中は、フラグ変数EventOffのフラグを立てておき、396行目を実行されないようにします。
Activateイベントでの準備が完了し、ユーザーが日付操作した時のみ396行目「Call OperateCondDisp」で図5-31が呼び出され、Imageコントロールに状態グラフが表示されます。

5-2-2-8.稼働状態ボタンの操作
今回システムの「稼働状態ボタン」は、クリックするたびに「状態を切り替え」させることを考えています。
これを実現させるため、まずCommandButtonを操作した時に、どんなイベントが発生するかを見てみます。

初めに、ボタンをゆっくり(ダブルクリックが反応しない様に)と何回もクリックすると、「MouseDown」→「MouseUp」→「Click」という順番でイベントが発生します。図に表すと、図5-20の左側のようになります(図5-20は、4回クリックした時を示しています)。
ボタンをマウス操作した時のイベント順
図5-20

一方、ボタンを素早くクリックすると、PC側は「ダブルクリックをした」と判断して、図5-20の右側のような「ゆっくり時とは異なるイベントの発生」となります。1回目はClickイベント、2回目はDblClickイベント、3回目は戻ってClickイベント という感じです。

また、ボタンはマウスでなくても操作可能です。キーを使い、Tabでコントロール間を移動させ、ボタンにFocusが来たところでスペースキーやEnterキーでボタンをクリックしたことになります。その時のイベントの発生の仕方が図5-21です(図5-21も、4回キーを押した時を示しています)。
ボタンをキー操作した時のイベント順
図5-21

スペースキーだと「KeyDown」→「KeyPress」→「KeyUp」→「Click」という順番、Enterキーだと「KeyDown」→「Click」→「KeyUp」という順番です。速くキーを押してもこのイベント順は同じでした。(キー種によってClickとKeyUpの発生する順番が変わるのは面白いところです)

図5-20と図5-21から、ボタンが何回クリックまたはキー押下されたかを知るためには「Clickイベント 及び DblClickイベント」を取得すれば判断できそうです。
そこで「Clickイベント」および「DblClickイベント」の両方から同じコードを動かそうというのが下記です。

まず「Clickイベント」が図5-22で、発生したら402行目「Call Button_StringChange」で図5-24を呼び出します。
  1. '========== ⇩(22) クリック、またはキー押下時のイベント ============
  2. Private Sub myButton_Click()
  3.  Call Button_StringChange
  4. End Sub
図5-22

また「DblClickイベント」が図5-23で、発生したら412行目「Call Button_StringChange」で同じく図5-24を呼び出します。
  1. '========== ⇩(23) ダブルクリック時のイベント ============
  2. Private Sub myButton_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
  3.  Call Button_StringChange
  4. End Sub
図5-23

図5-22の402行目、図5-23の412行目から呼び出されるのが図5-24です。この中では、ボタンの文字列(=Captionプロパティ)を変更させています。
  1. '========== ⇩(24) ボタンの文字列変更 ============
  2. Private Sub Button_StringChange()
  3.  Dim i As Integer   '←設備の個数
  4.  For i = 0 To UBound(markArray, 1) - 1
  5.   If myButton.Caption = markArray(i) Then
  6.    myButton.Caption = markArray(i + 1)
  7.    Exit Sub
  8.   End If
  9.  Next i
  10.  myButton.Caption = markArray(0)
  11. End Sub
図5-24

まずボタンの文字列は、図4-2の22行目で設定したmarkArray配列です。今回だと図5-25の左側のようにインデックス「0~4」までの各要素に記号が入っています。(インデックス0には、長さゼロの文字列が入っています)
状態記号の発生順序
図5-25

この各記号が、図5-25の右側にょうに「順番に出てくる」ようにする為に、以下の手順で調査・実行などをします。
「現在のボタンの記号を調査」→「その記号が配列の何番目かを調査」→「その次の番号の記号をボタンの記号にする」
但し、ボタンの記号が配列の最後だった場合には、配列の先頭の記号を使用
することになります。これをコードにします。

424行目「For i = 0 To UBound(markArray, 1) - 1」では、カウンタ変数iを「配列の要素数 -1」だけ回します。「-1」しているのは配列の最後のインデックス番号(=次の番号が無い)だからです。
425行目「If myButton.Caption = markArray(i) Then」では、現在のボタンの記号(myButton.Caption)と配列の記号を比較し、合致していれば426行目「myButton.Caption = markArray(i + 1)」で、ボタンの記号を「配列の次のインデックスの記号」に変更します。
記号を変更したら、あとは調べる必要はありませんので427行目「Exit Sub」で抜け出します。

424行目のFor~Next文で回しているのは「配列より1つ少ない数」ですので、もし現在のボタンの記号が配列の最後の記号(図5-25であれば「D」)だった場合には、425行目のIf文では成立せず、For~Nextが終了してしまいます。
抜け出した後は、431行目「myButton.Caption = markArray(0)」で「配列の先頭文字列(図5-25であれば「""(長さゼロの文字列)」)」をボタンの記号とすることになります。

寄り道
図5-22のClickイベントが発生した時点、および図5-23のDblClickイベントが発生した時点では、「クリックまたは押下されたボタンはmyButton」です。myButtonのイベントなのですから当然です。その各イベントプロシージャから呼び出した先の図5-24で、同じmyButtonを対象に処理を行うのは、ちょっと心配です。ボタン文字列表示が完了する前に、次のクリックが発生するかもしれないからです。

試しに、複数ボタンをマウスで乱打してみた結果は「違うボタンの処理をするエラー」は発生しませんでした。私のPCで時間を測ってみると、処理は遅くても0.03秒、操作は速くても0.07秒なので「圧倒的に処理が速い」ので大丈夫そうです。
また「Button_StringChangeが負荷の高い(処理時間がかかる)プロシージャ」を想定し、図5-24の中にDo~Loopで時間をかけてみても大丈夫でした。
但し、Do~Loopの中に「DoEvents」を入れると、処理対象が「グチャグチャ」になりました。

この事は、一度クリックして発生したClickイベントの「一連の動作が終了するまでは割り込みは発生せず」に、次のイベント発生は待機していてくれていることを意味していると思われます。
「永久Loopになったら大変だ」「途中でESCキーで止めたい」という理由で「DoEventsをやたらに入れる」と予期せぬ制御が割り込まれてしまい、思ったのと異なる結果が出てきてしまう可能性があるので注意が必要です。

やはり、ClickイベントとButton_StringChangeプロシージャとの間は「クリックされたボタンオブジェクトを引数としてbyValで渡す」のが一番安心かもしれません。ただし今回システムでは、人間が「ボタン表面の文字列の変化を確認しながら操作」するだろうし、またクリックがデータ保存に直結していない(保存は登録ボタンを押した時のみ)ため、特に気を遣っておりません。

5-2-2-9.データの登録
「登録」ボタンをクリックすると図5-26が呼び出され、状態ボタンでの変更の値がDBファイルに保存されます。
  1. '========== ⇩(25) データの登録 ============
  2. Private Sub CommandButton1_Click()
  3.  Dim mc As String    '←設備名
  4.  Dim d As Date     '←表示日付
  5.  Dim dt As Date     '←日時
  6.  Dim OldMark As Variant   '←DB内に登録されている記号の配列
  7.  Dim NewMark As String   '←ボタンの記号
  8.  Dim sql As String    '←正時
  9.  Dim i As Integer   '←実行するSQL文
  10.  mc = Me.ComboBox1.List(Me.ComboBox1.ListIndex)
  11.  d = CDate(Me.Label1.Caption)
  12.  OldMark = DayMark(d, mc)
  13.  For i = 0 To 23
  14.   dt = DateAdd("h", i, d)
  15.   NewMark = Barray(i).Button.Caption
  16.   If Not OldMark(i) = NewMark Then
  17.    Select Case True
  18.     Case OldMark(i) = ""
  19.      sql = "Insert into " & TBL & " (DT,MC,Cond) " & _
  20.          "values(#" & dt & "#,'" & mc & "','" & NewMark & "')"
  21.     Case NewMark = ""
  22.      sql = "Delete from " & TBL & _
  23.          " where DT= #" & dt & "# and MC= '" & mc & "'"
  24.     Case Else
  25.      sql = "Update " & TBL & " set Cond='" & NewMark & "'" & _
  26.          " where DT= #" & dt & "# and MC= '" & mc & "'"
  27.    End Select
  28.    Call SQL_exec6(sql)
  29.   End If
  30.  Next i
  31.  Call OperateCondDisp
  32. End Sub
図5-26

450行目「mc = Me.ComboBox1.List(Me.ComboBox1.ListIndex)」は、コンボボックスで選択中の設備名を取得します。
451行目「d = CDate(Me.Label1.Caption)」は、選択中の日付のラベルを日付型にして取得します。
452行目「OldMark = DayMark(d, mc)」では、設備名と日付を引数に渡して図5-30を呼び出し、「選択日付×選択設備の元データ」を取得します。このデータは24要素の一次元配列として戻してくるので、変数OldMarkで受け取ります。

処理を行う前に、まず今回のDBテーブルの構造について説明します。テーブルは図5-27のようになっていて、「停止」以外の記号のデータを入れています。つまり「未入力=停止」のような扱いとなります。
DT(日時)MC(設備名)Cond(記号)
2022/07/02 01:00:00旋盤A
2022/07/02 02:00:00旋盤B
2022/07/02 02:00:00裁断機C
:::
図5-27

454~474行目のFor~Next内では、状態ボタン1つ1つについて「表示されている記号」を調査し「DBテーブル内の元の記号」と比較をして、DBファイルに対して処理を行っていきます。その処理の内容は「元の記号」と「現在の記号」の組合せにより異なります。これをまとめたのが図5-28で、この表を元にして処理を分岐させていきます。
状態記号の変更内容による処理の違い
図5-28

図5-28で、まず「新旧の記号が同じ(白色の部分)」場合はデータを書き換える必要がありませんので処理をしません。
それ以外を分岐していきいます。まず、元が「""(長さゼロの文字列)」の場合は、DBテーブルにデータがないので、データ追加(Insert文:緑色)します。
変更後が「""(長さゼロの文字列)」の場合は、DBテーブルからデータを消す必要があるのでDelete文(青色)になります。
その他の組み合わせの場合は、記号を変更する必要があるのでUpdate文(黄色)を使用することになります。

454行目「For i = 0 To 23」では、カウンタ変数iをボタンの数だけ回します。変数iは、正時(0時、1時、2時、・・・)を表すことになります。
455行目「dt = DateAdd("h", i, d)」は、Label1の日付(整数値なので0時相当)に対し正時分だけ時刻を増やした日時データにします。
456行目「NewMark = Barray(i).Button.Caption」では、状態ボタンの表面文字列(=記号)を取得します。

458行目「If Not OldMark(i) = NewMark Then」では、元の記号(OldMark)と新しい記号(NewMark)とが異なる場合のみ459~471行目を実行し、DBテーブルへの処理を行います。つまり、図5-28の白い部分を取り除きます。

色のついた部分は459行目「Select Case True」で分岐させます。
まず緑色部分は460行目「Case OldMark(i) = ""」でTrueとなり、461~462行目「sql = "Insert into " & TBL & " (DT,MC,Cond) " & "values(#" & dt & "#,'" & mc & "','" & NewMark & "')"」で、Insert文を作成します。
挿入するデータは、DT列に日時(dt)、MC列に設備名(mc)、Cond列に記号(Newmark)となります。

次に青色部分は463行目「Case NewMark = ""」でTrueとなり、464~465行目「sql = "Delete from " & TBL & " where DT= #" & dt & "# and MC= '" & mc & "'"」で、Delete文を作成します。
削除するデータは、DT列が日時(dt)で且つMC列が設備名(mc)のレコードです。

最後に黄色部分は466行目「Case Else」に分岐され、467~468行目「sql = "Update " & TBL & " set Cond='" & NewMark & "'" & " where DT= #" & dt & "# and MC= '" & mc & "'"」で、Update文を作成します。
上書きするのは「where DT= #" & dt & "# and MC= '" & mc & "'"」と、DT列が日時(dt)、MC列が設備名(mc)のレコードで、上書きするデータは「" set Cond='" & NewMark & "'"」と「新しい記号」です。

SQL文が完成したら、471行目「Call SQL_exec6(sql)」で図4-12を呼出し、DBテーブルに対して実行します。
この作業をボタンの数だけ行います。
最後に476行目「Call OperateCondDisp」で、状態グラフを更新します。

「終了ボタン」をクリックした時に呼び出されるのが図5-29です。データ登録とは無関係ですが、同じボタン操作系なのでここで説明してしまいます。
  1. '========== ⇩(26) システム終了 ============
  2. Private Sub CommandButton2_Click()
  3.  Unload Me
  4. End Sub
図5-29

482行目「Unload Me」で、フォームを閉じます。
Unloadの代わりに「Me.Hide」で隠すだけでもOKですが、今回のシステムではActivateイベント内で全て初期化(設備名選択も、日付選択も初期値を表示)してしまいますので、初期化部分をInitializeに移動する等の改造をしないと意味がありません。
但しその時に、設備名をフォーム上から修正できるようなシステムにしていたとすると、再度起動した時に 「他の人が変更した設備が反映されない」事になるため、注意が必要です。

5-2-2-10.対象日×選択設備の元のデータ取得
図5-26の452行目、図5-31の520行目から呼び出される「指定された日付・指定された設備の、DBテーブル内のデータ」を配列として戻す関数が図5-30です。引数として日付と設備名を受け取ります。
  1. '========== ⇩(27) DBテーブルの元データ取得 ============
  2. Private Function DayMark(d As Date, mc As String) As String()
  3.  Dim buf1 As Variant    '←DBテーブルから取得したデータ
  4.  Dim buf2(0 To 23) As String    '←関数プロシージャの戻り値
  5.  Dim sql As String    '←SQL文
  6.  Dim i As Integer    '←DBテーブル内のレコード数
  7.  sql = "select val(format(dt,'h')),Cond from " & TBL & _
  8.      " where format(DT,'yyyy/mm/dd')= #" & d & "# " & _
  9.      " and MC= '" & mc & "'"
  10.  buf1 = SQL_exec6(sql)
  11.  For i = 1 To UBound(buf1, 1)
  12.   buf2(buf1(i, 1)) = buf1(i, 2)
  13.  Next i
  14.  DayMark = buf2
  15. End Function
図5-30

まず、変数宣言の内493行目「Dim buf2(0 To 23) As String」は、関数の戻り値となる配列ですが、データがあっても無くても24個の要素となるように静的な一次元配列としています。要素のインデックスは、正時を表す事になります。

DBテーブルから「指定された日付・指定された設備のデータ」を取り出すのが、497~499行目「sql = "select val(format(dt,'h')),Cond from " & TBL & " where format(DT,'yyyy/mm/dd')= #" & d & "# " & " and MC= '" & mc & "'"」です。
まず、レコードの抽出条件部分は「" where format(DT,'yyyy/mm/dd')= #" & d & "# " & " and MC= '" & mc & "'"」で、DT列のデータ(日付+時刻)を「format(DT,'yyyy/mm/dd')」で日付のみにした後、「引数の日付(d)」と比較をします。
またand検索「" and MC= '" & mc & "'"」で、更に設備名が「引数で指定された設備」に絞り込んでいます。

なおSelectするデータは「val(format(dt,'h'))」と「Cond」の2つで、前者は抽出した日付(DT列)の正時部分('h')を取り出し、Val関数で整数にして取得します。後者のCond列は状態記号そのままです。
500行目「buf1 = SQL_exec6(sql)」で図4-12を呼出し、DBテーブルから対象のレコードを取得し、buf1に代入します。buf1はタイトル行も入っていて、二次元配列となります。

502~504行目では、493行目で宣言した24要素の一次元配列に「データが存在するインデックス」の要素に「記号」を入れていきます。
502行目「For i = 1 To UBound(buf1, 1)」では、抽出されたレコード数だけカウンタ変数iを回します。
503行目「buf2(buf1(i, 1)) = buf1(i, 2)」は、記号をbuf2に代入しています。代入する位置は「val(format(dt,'h'))」で取得した「正時のインデックス位置」に代入します。つまり、配列に代入されなかった要素(=時刻)は「停止=未入力」ということになります。

代入が完了したら、「時刻ごとに記号が並んだ配列」を506行目「DayMark = buf2」で関数の戻り値にします。

5-2-2-11.グラフデータ表示
図5-10、図5-18、図5-19、図5-26から呼び出される「稼働状態グラフを表示」するのが図5-31です。
  1. '========== ⇩(28) グラフデータ表示 ============
  2. Private Sub OperateCondDisp()
  3.  Dim mc As String    '←表示する設備名
  4.  Dim d As Date      '←表示する日付
  5.  Dim buf As Variant    '←記号の配列
  6.  Dim R As Range     '←グラフの元となるセル範囲
  7.  Dim i As Integer    '←状態ボタンの数
  8.  mc = Me.ComboBox1.List(Me.ComboBox1.ListIndex)
  9.  d = CDate(Me.Label1.Caption)
  10.  buf = DayMark(d, mc)
  11.  ThisWorkbook.Sheets("Sheet1").Range("C3:Z3") = buf
  12.  Set R = ThisWorkbook.Sheets("Sheet1").Range("C4:Z4")
  13.  Me.Image1.Picture = LoadPicture(Range2File(R))
  14.  For i = 0 To 23
  15.   Barray(i).Button.Caption = buf(i)
  16.  Next i
  17.  Me.Repaint
  18. End Sub
図5-31

518行目「mc = Me.ComboBox1.List(Me.ComboBox1.ListIndex)」で、コンボボックスから選択中の設備名を取得します。
519行目「d = CDate(Me.Label1.Caption)」で、スクロールバー上部のラベルから選択中の日付を取得します。
520行目「buf = DayMark(d, mc)」で図5-30を呼出し、「時刻ごとに記号が並んだ配列」を取得します。

522行目「ThisWorkbook.Sheets("Sheet1").Range("C3:Z3") = buf」では、その24個の記号の配列を、Sheet1の「条件付き書式適用先の上段(C3~Z3セル)」に貼り付けます。貼り付けると、条件付き書式によりセル背景色が変更されます。
524行目「Set R = ThisWorkbook.Sheets("Sheet1").Range("C4:Z4")」で、背景色が変更された範囲の下段(C4~Z4セル)を変数Rに設定します。

526行目「Me.Image1.Picture = LoadPicture(Range2File(R))」の右辺カッコ内の「Range2File」は、図5-32の関数プロシージャです。Range2File関数の引数に渡した「セル範囲」を「画像ファイル」に変換し、その保存先を戻して来ますので、その「画像ファイルのパス+ファイル名」をLoadPictureメソッドの引数にしてImageコントロールのPictureプロパティに設定することでImageコントロールに画像が表示されます。
今回Range2File関数に渡すセル範囲は、524行目で設定したR(条件付き書式範囲の下段=設備状態グラフの元)ですので、設備状態グラフがImageに表示されることになります。

528~530行目は状態ボタンの文字列を「DBテーブルのデータ」に揃えています。
528行目「For i = 0 To 23」でボタンの個数だけカウンタ変数iを回し、529行目「Barray(i).Button.Caption = buf(i)」でボタン文字列をDBテーブルの値(記号)にしています。

なお単に「登録ボタン」をクリックした時には、既にボタン文字列はユーザーが指定した状態になっていますので重複した動作になりますが、コンボボックス(設備名)やスクロールバー(日付)を移動した時にはボタン文字列の更新は必須であるため、一部の重複には目を瞑りました。

Imageコントロールへの画像ファイルの設定、ボタン文字列の設定が終わりましたので、532行目「Me.Repaint」でフォームの画面更新をしています。当初は「Repaintをしなくても大丈夫そうだ」と思っていましたが、極まれに状態グラフが更新されないことに気が付きましたのでRepaintは必要そうです。

5-2-2-12.シート上データを画像ファイル化
図5-31の526行目から呼び出される「指定されたセル範囲を画像ファイルに変換し、その保存先を戻す」関数が図5-32です。引数としてセル範囲を受け取ります。
  1. '========== ⇩(29) 画像ファイル化 ============
  2. Private Function Range2File(R As Range) As String
  3.  Dim fso As Object    '←FileSystemObjectオブジェクト
  4.  Dim Cht As Chart     '←チャートオブジェクト
  5.  R.CopyPicture appearance:=xlScreen, Format:=xlBitmap
  6.  Set Cht = ThisWorkbook.Sheets("Sheet1").ChartObjects.Add(0, 0, R.Width, R.Height).Chart
  7.  Cht.Parent.Select
  8.  Cht.Paste
  9.  Set fso = CreateObject("Scripting.FileSystemObject")
  10.  If Not Dir(BMPpath & BMPname) = "" Then
  11.   fso.getfile(BMPpath & BMPname).Delete
  12.  End If
  13.  Set fso = Nothing
  14.  Cht.Export Filename:=BMPpath & BMPname, filtername:="BMP"
  15.  Cht.Parent.Delete
  16.  Range2File = BMPpath & BMPname
  17. End Function
図5-32

545行目「R.CopyPicture appearance:=xlScreen, Format:=xlBitmap」では、引数で受け取ったセル範囲(今回システムでは、状態グラフの元のセル範囲)を画像コピーします。CopyPictureメソッドの2つの引数については、「図形カレンダーをクリックし日付入力」を参照下さい。
546行目「Set Cht = ThisWorkbook.Sheets("Sheet1").ChartObjects.Add(0, 0, R.Width, R.Height).Chart」は、Sheet1にチャートオブジェクト(ChartObjects)を作成(Add)しています。作成するチャートオブジェクトのサイズは545行目でコピーしたセル範囲(R)と同じ大きさです。実際の動きとしては図5-33のようになります。
Chartを使ったセル範囲の画像ファイル化
図5-33

545行目の画像コピーが図5-33の①、546行目のチャートオブジェクト作成が②の状態です。
546行目の左辺の変数Chtには、作ったチャートオブジェクトのChartを設定しています。
548行目「Cht.Parent.Select」でChartの親(=チャートオブジェクト)を選択した後、549行目「Cht.Paste」でクリップボード上のセルの画像をChartに貼り付けます。貼り付けた状態が、図5-33の③の状態になります。

551行目「Set fso = CreateObject("Scripting.FileSystemObject")」では、FileSystemObjectオブジェクトを生成しています。ここでは554行目で「ファイルを削除」するためにFileSystemObjectを使います。
553行目「If Not Dir(BMPpath & BMPname) = "" Then」で、これから作ろうとしている画像ファイルのパス+ファイル名をDir関数に渡し、存在している場合には554行目「fso.getfile(BMPpath & BMPname).Delete」で画像ファイルを「一旦削除」しています。ですので、図4-1の10行目で設定する画像ファイル名は、よくある名前では無く「他では、まず使われないファイル名」としておく必要があります。

559行目「Cht.Export Filename:=BMPpath & BMPname, filtername:="BMP"」は、セル範囲を貼り付けたChartを画像としてファイル出力(エキスポート)します。ファイル名は「BMPpath & BMPname」、ファイル型は「BMP」です。
ファイル出力が終わりましたらチャートオブジェクトは不要ですので、560行目「Cht.Parent.Delete」で削除します。

562行目「Range2File = BMPpath & BMPname」で、関数の戻り値として「出力した画像ファイルのパス+ファイル名」をセットします。

6.フォーム上ラベルの色変更方式

6-1.ユーザーフォーム(UserForm2)

6-1-1.フォーム上のコントロール類配置

フォーム上には、上部にユーザーが選択する「設備名のComboBox」と「日付のScrollBar」を配置し、ScrollBarの上に日付を表示するLabelを置きます。また右上には登録と終了の2つのCommandButtonを置きます。
フォーム上のコントロール配置
図6-1

なお「画像ファイル貼付け方式」には存在したImageコントロールは、「ラベルの色変更方式」では不要です。Imageの代わりにマクロ側から24個のラベルを作成して並べます。
この方式でも「フォーム自体の大きさ」は調整していません。作るラベルやボタンの大きさ・数からフォームの大きさを計算しサイズを調整するような工夫を必要に応じて実施して下さい。


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

6-1-2-1.宣言部
フォームモジュール先頭では、フォーム内で使用する変数の宣言、およびマクロで作成する「状態ボタン」のイベントを取得するための宣言をします。
  1. '========== ⇩(30) モジュールレベル変数の宣言 ============
  2. Private WithEvents myButton As MSForms.CommandButton
  3. Private myLabel As MSForms.Label
  4. Dim Barray() As New UserForm2
  5. Dim Larray() As New UserForm2
  6. Dim EventOFF As Boolean
図6-2

571行目「Private WithEvents myButton As MSForms.CommandButton」は、図6-9で作成する「状態を入力する24個のCommandButton」をクリックした時にClickイベントを取得するためのWithEvents宣言です。
572行目「Private myLabel As MSForms.Label」も、図6-9で作成する「状態を色で表示する24個のLabel」を操作するための宣言です。

573行目「Dim Barray() As New UserForm2」は、どの状態ボタンが押されたかを把握する目的で、1つ1つのボタンにインデックスを登録するための配列の宣言です。ButtonのArrayという意味で命名しました。
574行目「Dim Larray() As New UserForm2」は、どのラベルを操作すればよいかを把握する目的で、1つ1つのラベルにインデックスを登録するための配列の宣言です。LabelのArrayという意味で命名しました。
設備名選択のComboBoxや日付選択のScrollBarには、ユーザー操作に反応するためのイベントを仕込んでいます。しかし、ComboBoxやScrollBarの準備をするときにはイベントは反応して欲しく無いので、イベントを中断させるためのフラグ変数を575行目「Dim EventOFF As Boolean」で宣言します。

6-1-2-2.カスタムプロパティ
作成したボタンのプロパティ値を取得する時に呼び出すのが図6-3で、ボタンのオブジェクトを設定するのが図6-4です。
図6-3と図6-4は「画像ファイル貼付け方式」と同じなので、詳細説明は「画像ファイル貼付け方式のカスタムプロパティ」を参照願います。
  1. '========== ⇩(31) ボタンのプロパティ値を取得 ============
  2. Public Property Get Button() As Control
  3.  Set Button = myButton
  4. End Property
図6-3

  1. '========== ⇩(32) ボタンのオブジェクトを設定 ============
  2. Public Property Set Button(ByVal addBTN As Control)
  3.  Set myButton = addBTN
  4. End Property
図6-4

作成した「状態グラフ用のラベル」のプロパティ値を取得する時に呼び出すのが図6-5です。
602行目「Set Label = myLabel」で、作成したラベル(myLabel)のプロパティを呼出し先に戻しています。
  1. '========== ⇩(33) ラベルのプロパティ値を取得 ============
  2. Public Property Get Label() As Control
  3.  Set Label = myLabel
  4. End Property
図6-5

「状態グラフ用のラベル」のオブジェクトを設定するのが図6-6です。引数として「オブジェクトへの参照である変数addLBL」を受け取ります。
612行目「Set myLabel = addLBL」で受け取ったオブジェクトをラベルに設定します。
  1. '========== ⇩(34) ラベルのオブジェクトを設定 ============
  2. Public Property Set Label(ByVal addLBL As Control)
  3.  Set myLabel = addLBL
  4. End Property
図6-6

6-1-2-3.フォームの初期化
フォーム起動時に最初に発生するイベントが図6-7です。内容的にはImageコントロールの設定が無いだけで、残りは画像ファイル貼付け方式と同じです。詳細説明は「画像ファイル貼付け方式のフォームの初期化」を参照願います。
  1. '========== ⇩(35) Initializeイベント ============
  2. Private Sub UserForm_Initialize()
  3.  Me.CommandButton1.Caption = "登録"
  4.  Me.CommandButton2.Caption = "終了"
  5.  Me.ComboBox1.Style = fmStyleDropDownList
  6.  Me.ScrollBar1.Min = 0
  7.  Me.ScrollBar1.Max = 3660
  8.  Me.ScrollBar1.LargeChange = 7
  9. End Sub
図6-7

フォームが表示される時に発生するのが図6-8です。
画像ファイル貼付け方式(図5-10)の時には状態ボタンと時刻ラベルを分けて作成していましたが、645行目「Call makeButtonLabel」では「状態グラフ用ラベル+稼働状態ボタン+時刻のラベル」を同時に作成しています。
他は、画像ファイル貼付け方式と同じですので、詳細説明は「画像ファイル貼付け方式のフォームの初期化」を参照願います。
  1. '========== ⇩(36) Activateイベント ============
  2. Private Sub UserForm_Activate()
  3.  EventOFF = True
  4.   Me.ScrollBar1.Value = Date - SystemStartDay
  5.   Call makeButtonLabel
  6.   Call makeCombo
  7.  EventOFF = False
  8.  Call OperateCondDisp
  9. End Sub
図6-8

645行目「Call makeButtonLabel」では図6-9を呼出し、「状態グラフを作成するラベル」+「稼働状態ボタン」+「時刻のラベル」を作成します。

6-1-2-4.ボタンとラベルの作成
図6-8の645行目から呼び出されるのが図6-9です。
  1. '========== ⇩(37) ボタンとラベルの作成 ============
  2. Private Sub makeButtonLabel()
  3.  Const L As Double = 9
  4.  Const T As Double = 72
  5.  Const H As Double = 42
  6.  Const W As Double = 18
  7.  Const fmBoderStyleSingle As Long = 1
  8.  Dim i As Integer   '←ラベル・ボタンの数
  9.  ReDim Larray(0 To 23)
  10.  ReDim Barray(0 To 23)
  11.  For i = 0 To 23
  12.   Set Larray(i).Label = addLBL(T, L + i * W, H, W, "")
  13.   Larray(i).Label.BorderStyle = fmBoderStyleSingle
  14.   Larray(i).Label.BorderColor = RGB(225, 225, 225)
  15.   Set Barray(i).Button = addBTN(T + H, L + i * W, W, W, "")
  16.   addLBL T - 10, L + i * W - 3, 10, W, CStr(i)
  17.  Next i
  18.  addLBL T - 10, L + i * W - 3, 10, W, CStr(i)
  19. End Sub
図6-9

662~665行目では、フォーム(UserForm2)上のどこにラベル等を表示するかを定数宣言しています。今回は、画像ファイル貼付け方式(UserForm1)のImageコントロールと同じ位置に、ラベル(=状態グラフ)を並べて表示することにしました。各定数の示してる位置は、図6-10の通りです。
ラベル・ボタンの位置
図6-10

666行目「Const fmBoderStyleSingle As Long = 1」は、674行目で設定するBorderStyle(外枠の線の有無)の定数値です。当初は「当然登録されている定数」と思っていたのですが、そうでは無さそうなので定数宣言しています。
669行目「ReDim Larray(0 To 23)」は、配列Larray(ラべルの数)の要素数を24個に設定しています。
670行目「ReDim Barray(0 To 23)」は、配列Barray(ボタンの数)の要素数を24個に設定しています。
Larray、Barrayとも「プロパティを取得・設定」するために配列に設定し、指定を可能にしています。
(一方、時刻のラベルは、一度設定した後はプロパティを変更しませんので、配列化は不要です。)

672行目「For i = 0 To 23」は、カウンタ変数iを回しています。
673行目「Set Larray(i).Label = addLBL(T, L + i * W, H, W, "")」で図6-12のaddLBL関数を呼び出し、左からi番目の位置に「状態グラフ用ラベル」を作成し、そのLabelオブジェクトを配列Larray(インデックスi)のLabelに登録します。この操作により、今後もインデックスでラベルの操作が可能になります。
674行目「Larray(i).Label.BorderStyle = fmBoderStyleSingle」は、作成したラベルの外枠を有りにします。プロパティに与えた定数「fmBoderStyleSingle」は、666行目で定数設定したものです。
675行目「Larray(i).Label.BorderColor = RGB(225, 225, 225)」では、その外枠線の色を灰色に設定しています。これは既定だと真っ黒(RGB(0, 0, 0))となり「あまりにもラベル1つ1つが明確になりすぎて、グラフっぽく見えない」と感じたため変更しました。

677行目「Set Barray(i).Button = addBTN(T + H, L + i * W, W, W, "")」は、673行目で作成した状態グラフ用ラベルのすぐ下に、図6-11を呼出して「状態ボタン」を作成し、配列Barrayに登録しています。

679行目「addLBL T - 10, L + i * W - 3, 10, W, CStr(i)」は、再び図6-12のaddLBL関数を呼び出し、状態グラフ用ラベルの上に「時刻表示用ラベル」を作成します。但しこの時刻表示用のラベルは、作成した後は何も変更しないので、673行目のように配列に登録する必要はありません。ですので「単にaddLBL関数を呼び出すだけ」になっています。

24個の「状態グラフ用ラベル」「状態ボタン」「時刻表示用ラベル」の作成が完了した時には、時刻表示ラベルは「23」までしか作成されていません。0時始まりですので24個で23時となるわけです。今回状態グラフの一番右上にも、翌日の0時を表示したいと考え、682行目「addLBL T - 10, L + i * W - 3, 10, W, CStr(i)」で「24」を書き加えています。

ここで、682行目で使っているカウンタ変数iは「24」を指しています。これは672行目「For i = 0 To 23」~680行目「Next i」の間で、カウンタ変数iは「回るたびに1ずつ増えている」訳ですが、i = 23 となっている時に697行目を実行して「時刻ラベルの23を作成」したのち、680行目「Next i」でiは24と増えます。そして今まで通りに672行目のForに戻った時に「iは23までだった」ことに気が付くわけです。そこでFor~Nextを抜け出して682行目を実行するのですから、その時にはカウンタ変数iは24となっている訳です。

「ボタンを作成し、そのオブジェクトを返す」関数が図6-11です。内容は画像ファイル貼付け方式と全く同じです。
詳細説明は「画像ファイル貼付け方式のボタンの作成」を参照願います。
  1. '========== ⇩(38) ボタンの作成 ============
  2. Private Function addBTN(T As Single, L As Single, _
  3.               H As Single, W As Single, _
  4.               Bcaption As String) As MSForms.CommandButton
  5.  Set addBTN = Me.Controls.Add("Forms.CommandButton.1")
  6.  With addBTN
  7.   .Top = T
  8.   .Left = L
  9.   .Height = H
  10.   .Width = W
  11.   .Caption = Bcaption
  12.  End With
  13. End Function
図6-11

「ラベルを作成し、そのオブジェクトを返す」関数が図6-12です。内容は画像ファイル貼付け方式と全く同じです。
詳細説明は「画像ファイル貼付け方式の時刻ラベルの作成」を参照願います。
  1. '========== ⇩(39) ラベルの作成 ============
  2. Private Function addLBL(T As Single, L As Single, _
  3.               H As Single, W As Single, _
  4.               Lcaption As String) As MSForms.Label
  5.  Set addLBL = Me.Controls.Add("Forms.Label.1")
  6.  With addLBL
  7.   .Top = T
  8.   .Left = L
  9.   .Height = H
  10.   .Width = W
  11.   .Caption = Lcaption
  12.  End With
  13. End Function
図6-12

6-1-2-5.コンボボックスの作成
「設備名の選択用コンボボックスを作成」するのが図6-13です。内容は画像ファイル貼付け方式と全く同じです。
詳細説明は「画像ファイル貼付け方式のコンボボックスの作成」を参照願います。
  1. '========== ⇩(40) コンボボックスの作成 ============
  2. Private Sub makeCombo()
  3.  Dim i As Integer
  4.  Me.ComboBox1.Clear
  5.  For i = 0 To UBound(MachineArray, 1)
  6.   Me.ComboBox1.AddItem MachineArray(i)
  7.  Next i
  8.  Me.ComboBox1.ListIndex = 0
  9. End Sub
図6-13

6-1-2-6.設備・日付の選択
設備名のコンボボックスを操作した時に発生するのが図6-14です。内容は画像ファイル貼付け方式と全く同じです。
詳細説明は「画像ファイル貼付け方式の設備・日付の選択」を参照願います。
  1. '========== ⇩(41) 設備の選択 ============
  2. Private Sub ComboBox1_Change()
  3.  If EventOFF = True Then Exit Sub
  4.  Call OperateCondDisp
  5. End Sub
図6-14

日付選択のスクロールバーを動かした時に発生するのが図6-15です。内容は画像ファイル貼付け方式と全く同じです。
詳細説明は「画像ファイル貼付け方式の設備・日付の選択」を参照願います。
  1. '========== ⇩(42) 日付の選択 ============
  2. Private Sub ScrollBar1_Change()
  3.  Me.Label1.Caption = SystemStartDay + Me.ScrollBar1.Value
  4.  If EventOFF = True Then Exit Sub
  5.  Call OperateCondDisp
  6. End Sub
図6-15

6-1-2-7.稼働状態ボタンの操作
稼働状態ボタンをクリックした時に発生する「Clickイベント」が図6-16で、ダブルクリックのように素早くクリックした時に発生する「DblClickイベント」が図6-17です。両方とも同じ処理を行うため、どちらも図6-18が実行されます。
内容は画像ファイル貼付け方式と全く同じです。
詳細説明は「画像ファイル貼付け方式の稼働状態ボタンの操作」を参照願います。
  1. '========== ⇩(43) クリックまたはキー押下時のイベント ============
  2. Private Sub myButton_Click()
  3.  Call Button_StringChange
  4. End Sub
図6-16

  1. '========== ⇩(44) ダブルクリック時のイベント ============
  2. Private Sub myButton_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
  3.  Call Button_StringChange
  4. End Sub
図6-17

  1. '========== ⇩(45) ボタンの文字列変更 ============
  2. Private Sub Button_StringChange()
  3.  Dim i As Integer
  4.  For i = 0 To UBound(markArray, 1) - 1
  5.   If myButton.Caption = markArray(i) Then
  6.    myButton.Caption = markArray(i + 1)
  7.    Exit Sub
  8.   End If
  9.  Next i
  10.  myButton.Caption = markArray(0)
  11. End Sub
図6-18

6-1-2-8.データの登録
「登録」ボタンをクリックしたときに呼び出されるのが図6-19です。内容は画像ファイル貼付け方式と全く同じです。
詳細説明は「画像ファイル貼付け方式のデータの登録」を参照願います。
  1. '========== ⇩(46) データの登録 ============
  2. Private Sub CommandButton1_Click()
  3.  Dim mc As String
  4.  Dim d As Date
  5.  Dim dt As Date
  6.  Dim OldMark As Variant
  7.  Dim NewMark As String
  8.  Dim sql As String
  9.  Dim i As Integer
  10.  mc = Me.ComboBox1.List(Me.ComboBox1.ListIndex)
  11.  d = CDate(Me.Label1.Caption)
  12.  OldMark = DayMark(d, mc)
  13.  For i = 0 To 23
  14.   dt = DateAdd("h", i, d)
  15.   NewMark = Barray(i).Button.Caption
  16.   If Not OldMark(i) = NewMark Then
  17.    Select Case True
  18.     Case OldMark(i) = ""
  19.      sql = "Insert into " & TBL & " (DT,MC,Cond) " & _
  20.          "values(#" & dt & "#,'" & mc & "','" & NewMark & "')"
  21.     Case NewMark = ""
  22.      sql = "Delete from " & TBL & _
  23.          " where DT= #" & dt & "# and MC= '" & mc & "'"
  24.     Case Else
  25.      sql = "Update " & TBL & " set Cond='" & NewMark & "'" & _
  26.          " where DT= #" & dt & "# and MC= '" & mc & "'"
  27.    End Select
  28.    Call SQL_exec6(sql)
  29.   End If
  30.  Next i
  31.  Call OperateCondDisp
  32. End Sub
図6-19

「終了ボタン」をクリックした時に呼び出されるのが図6-20です。内容は画像ファイル貼付け方式と全く同じです。
詳細説明は「画像ファイル貼付け方式のデータの登録」を参照願います。
  1. '========== ⇩(47) システム終了 ============
  2. Private Sub CommandButton2_Click()
  3.  Unload Me
  4. End Sub
図6-20

6-1-2-9.対象日×選択設備の元のデータ取得
「指定された日付・指定された設備の、DBテーブル内のデータ」を配列として戻す関数が図6-21です。内容は画像ファイル貼付け方式と全く同じです。
詳細説明は「画像ファイル貼付け方式の対象日×選択設備の元のデータ取得」を参照願います。
  1. '========== ⇩(48) DBテーブルの元データ取得 ============
  2. Private Function DayMark(d As Date, mc As String) As String()
  3.  Dim buf1 As Variant
  4.  Dim buf2(0 To 23) As String
  5.  Dim sql As String
  6.  Dim i As Integer
  7.  sql = "select val(format(dt,'h')),Cond from " & TBL & _
  8.      " where format(DT,'yyyy/mm/dd')= #" & d & "# " & _
  9.      " and MC= '" & mc & "'"
  10.  buf1 = SQL_exec6(sql)
  11.  For i = 1 To UBound(buf1, 1)
  12.   buf2(buf1(i, 1)) = buf1(i, 2)
  13.  Next i
  14.  DayMark = buf2
  15. End Function
図6-21

6-1-2-10.グラフデータ表示
「稼働状態グラフを表示」させるのが図6-22です。
  1. '========== ⇩(49) グラフデータ表示 ============
  2. Private Sub OperateCondDisp()
  3.  Dim mc As String   '←選択した設備名
  4.  Dim d As Date     '←選択した日付
  5.  Dim buf As Variant   '←記号の配列
  6.  Dim i As Integer   '←状態ラベル・ボタンの数
  7.  Dim j As Integer   '←記号の数
  8.  mc = Me.ComboBox1.List(Me.ComboBox1.ListIndex)
  9.  d = CDate(Me.Label1.Caption)
  10.  buf = DayMark(d, mc)
  11.  For i = 0 To 23
  12.   For j = 0 To UBound(markArray, 1)
  13.    If buf(i) = markArray(j) Then
  14.     Larray(i).Label.BackColor = ColorArray(j)
  15.    End If
  16.   Next j
  17.   Barray(i).Button.Caption = buf(i)
  18.  Next i
  19. ' Me.Repaint 
  20. End Sub
図6-22

888行目「mc = Me.ComboBox1.List(Me.ComboBox1.ListIndex)」で、コンボボックスで選択中の設備名を取得します。
889行目「d = CDate(Me.Label1.Caption)」で、スクロールバー上部のラベルから選択中の日付を取得します。
890行目「buf = DayMark(d, mc)」で図6-21を呼び出し、「時刻ごとに記号が並んだ配列」を取得します。

892行目「For i = 0 To 23」で、カウンタ変数iを「状態グラフ用ラベル(=時刻の数)」の数だけ回します。
893行目「For j = 0 To UBound(markArray, 1)」では、カウンタ変数jを「状態記号」の数だけ回します。
894行目「If buf(i) = markArray(j) Then」で、対象時刻の記号が状態記号と合致した時、895行目「Larray(i).Label.BackColor = ColorArray(j)」で、状態グラフ用ラベルの背景色を「記号の色」に変更します。

899行目「Barray(i).Button.Caption = buf(i)」では、その記号を対象時刻の「状態ボタン」の文字にします。

最後に「状態グラフ用ラベルの背景色」と「状態ボタンの文字」を変更したので、902行目「Me.Repaint」でフォームの画面更新をすると良いのですが、私が試したところ「Repaintを実行しなくても、更新されている」ため、とりあえずコメントアウトしています。
実行しない理由は、「Repaintをすると、フォーム全体がパラパラと動く」ため、見栄え的にはあまり良くないと感じたからです。PCの性能や環境にも寄ると思いますので、「変更が反映されない」場合にはRepaintを実行するようにして下さい。

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

このマクロ付ファイル(サンプルファイル)をExcelのアドインに登録することで、今回の「設備稼働状態入力・確認」を他のブック使用時に呼び出して使うことが出来ます。アドイン方法については「年賀状リスト等の宛名検索と追記 アドイン登録」を参照下さい。
またリボンのボタンには、標準モジュールの「SystemStart1(図4-3)」または「SystemStart2(図4-4)」を登録して下さい。

8.最後に

背景でも書きましたが、当初は設備状態記号を使って1日分の状態グラフ(集合縦棒グラフを使用)を作成し、そのグラフを画像ファイルにしてフォーム上のImageコントロールに表示するものをトライしていました。しかし、記号をセル範囲に貼り付けてからグラフに反映させるのに時間が掛かる(私のPCで約3秒)ことが分かり、今回のような仕様に変更しました。
時間が掛かる理由は「フォームを起動」しているためのようです。色々試し、モードレスで起動するよりはモーダルの方がグラフ反映が速そうな事も分かりましたが、それでも不満足。それならと、一旦標準モジュール側に制御を移動してからRepaintすることもしましたが、グラフに反映されない状況は残りました。

フォームを使わずにワークシートだけを使っていれば、データはグラフにリアルタイム的に反映されるという例題はネット上で見掛けますが、フォーム上にグラフを動的に表示するのは、かなりの難問のようです。しかし、グラフをフォーム上に表示できれば、また色々と使い道が増えそうな気がしますので、またトライしたいと思っています。


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