情報学入門(データ演習) / 第 13 章

ユーザフォーム

イベント駆動 ── 利用者の操作から始まるプログラム

この章のねらい

この章の作りについて。 この教材の処理系は Excel のユーザフォーム(VBA の画面部品)を扱えません。 そこでページ上の HTML の部品を「フォーム」に見立て、 ボタンを押すと対応する VBA の手続きが実行されるようにしました。 プログラムの書き方も、考え方も、本物のユーザフォームと同じです (違うのは部品の作り方だけで、それは Excel の画面上でマウスで並べる作業です)。
下の欄に「どのイベントで、どの手続きが呼ばれたか」が記録されるので、 イベント駆動の動きが目で追えます。

1. ユーザフォームとは

セルに値を入力する時にガイドをするウィンドウを作成することができる。 これをユーザフォームという。(元資料 3 枚目)

作り方は、ツールボックスからコントロールを選び、フォームに貼り付けるだけです (プログラムを書かずに、マウスで並べます)。

元資料 7・8 枚目のコントロール一覧
コントロール英名何をするもの
ラベルLabel説明の文字を出す(入力はできない)
テキストボックスTextBox値を入力してもらう
コンボボックスComboBox一覧から選ぶ(折りたたみ式)
リストボックスListBox一覧から選ぶ(開いたまま)
チェックボックスCheckBoxはい/いいえ(複数選べる)
オプションボタンOptionButtonいくつかから 1 つだけ選ぶ
トグルボタンToggleButton押した状態を保つボタン
フレームFrame部品をまとめる枠
コマンドボタンCommandButton押すと処理が始まる
スクロールバー/スピンボタンScrollBar / SpinButton数値を増減させる
イメージImage画像を置く

最初に覚えるのは 4 つだけで足ります。 Label(説明)、TextBox(入力)、 OptionButton(1 つ選ぶ)、CommandButton(実行)。 この章の例題はすべてこの 4 つで作れます。
なお元資料の一覧にある MultiPage・TabStop・RefEdit は、 いまではほとんど使われません(RefEdit はセル範囲を選ばせる特殊な部品で、 不具合が多いことで知られています)。

2. イベント駆動プログラム

フォーム上でクリック、選択することによりイベントが発生し、 イベント駆動プログラムが実行される。(元資料 9 枚目)

ここがこの章の唯一の、そして重要な新しい考え方です。 第 1〜12 章のプログラムは、すべて「実行ボタンを押したら、上から下へ最後まで走る」ものでした。 イベント駆動はそうではありません。

これまで(第 1〜12 章)イベント駆動(この章)
始まり1 つ(マクロを実行する) いくつもある(ボタンごと、部品ごと)
走る順番書いた順に上から下へ 利用者の操作した順。書いた順とは関係ない
終わり方最後の行まで行ったら終わり 1 回の処理はすぐ終わる。次の操作を待つ
値の持ち方変数に入れておけばよい 手続きが終わると消えるので、残す工夫が必要
利用者がボタンを押す
→
イベントが発生する
Click
→
対応する手続きが呼ばれる
CommandButton1_Click
→
処理して終わる
次の操作を待つ

手続きの名前が「部品の名前 + アンダースコア + イベント名」になっているのが要点です。
Private Sub CommandButton1_Click() ── 「CommandButton1 が Click されたときに、これを実行する」という意味です。 名前でつながっているので、綴りを 1 文字間違えると何も起こりません (エラーも出ません)。イベント駆動でいちばん多いつまずきです。

身のまわりのソフトはほとんどイベント駆動です。 ボタンを押す、文字を入れる、マウスを動かす、時間が来る、通信が届く ── すべてイベントで、それぞれに対応する処理が待っています。 「プログラムが利用者を待っている」という形が、 実用的なソフトウェアの標準的な姿です。 第 1〜12 章で書いてきた「一気に走って終わる」形は、 実はバッチ処理(一括処理)と呼ばれる一部の用途にあたります。

3. 円と球(元資料 12〜15 枚目)

第 3 章と同じ計算を、フォームからやってみます。 違うのは「半径をどこから受け取るか」だけです ── セルではなくテキストボックスから受け取ります。

動かす① 円と球のユーザフォーム ボタンを押す
ユーザフォーム (UserForm1)
TextBox1
イベント駆動プログラム
' TextBox1 の値は A1、結果は B1〜B4 に置く約束にする

Private Sub CommandButton1_Click()
    Dim r As Double
    Dim pi As Double

    r = Cells(1, 1).Value            ' TextBox1 から受け取る
    pi = Application.WorksheetFunction.Pi

    Cells(1, 2) = 2 * pi * r         ' 円の円周
    Cells(2, 2) = pi * r ^ 2         ' 円の面積
    Cells(3, 2) = 4 * pi * r ^ 2     ' 球の表面積
    Cells(4, 2) = 4 / 3 * pi * r ^ 3 ' 球の体積
End Sub

Private Sub CommandButton2_Click()
    Cells(1, 2) = ""
    Cells(2, 2) = ""
    Cells(3, 2) = ""
    Cells(4, 2) = ""
End Sub

半径を 3 にしてみてください。 球の表面積と球の体積が同じ値(113.10)になります ── 誤りではありません。 4πr2 = (4/3)πr3 となるのは r = 3 のときだけで、 たまたま一致するのです。「同じ値が出たから間違い」と決めつけない ── 検算するときに気をつけたいところです。

ボタンが 2 つあるので、手続きも 2 つあります。 そしてこの 2 つはどちらが先に呼ばれるか分かりません ── 利用者が押した順に呼ばれるだけです。 だから、どの手続きも「いつ呼ばれても正しく動く」ように書く必要があります。 これがイベント駆動プログラムを書くときの心構えです。

なお Private は「このフォームの中だけで使う手続き」という指定で、 イベント駆動プログラムには慣例としてこれを付けます (付けなくても動きますが、他から呼ぶものではないので付けるのが自然です)。

4. 購入費の計算(元資料 18・19 枚目)

果物を購入する際の合計金額を計算せよ。

動かす② 果物の購入費 個数を変えて計算
ユーザフォーム (UserForm2)
× 180 円
× 80 円
× 650 円
イベント駆動プログラム
' A 列に個数、B 列に単価。会員かどうかは A4(True / False)

Private Sub CommandButton1_Click()
    Dim i As Integer
    Dim kingaku As Long
    Dim goukei As Long
    Dim kaiin As Boolean

    goukei = 0
    For i = 1 To 3
        kingaku = Cells(i, 1).Value * Cells(i, 2).Value
        Cells(i, 3) = kingaku          ' 品目ごとの金額
        goukei = goukei + kingaku
    Next i

    Cells(5, 3) = goukei               ' 小計

    kaiin = Cells(4, 1).Value
    If kaiin Then
        Cells(6, 3) = Int(goukei * 0.05)      ' 割引額(1 円未満は切り捨て)
        goukei = goukei - Cells(6, 3).Value
    Else
        Cells(6, 3) = 0
    End If

    Cells(7, 3) = goukei                     ' 割引後
    Cells(8, 3) = Int(goukei * 0.1)          ' 消費税 10 %
    Cells(9, 3) = goukei + Cells(8, 3).Value ' 税込合計
End Sub

Private Sub CommandButton2_Click()
    Dim i As Integer
    For i = 1 To 9
        Cells(i, 3) = ""
    Next i
End Sub

単価をプログラムに書かず、シート(B 列)に置いてあります。 第 2 章の「データはシートに、手順はプログラムに」です。 値段が変わっても、プログラムを直す必要がありません。
また Int(goukei * 0.05) と切り捨てているのは、 1 円未満の端数を出さないためです。 金額を Double のまま扱うと第 4 回・第 12 章の誤差の問題が出るので、 金額は整数で持つのが原則です。

5. 自動販売機(元資料 20 枚目〜)

以下の仕様を満たす自動販売機を作成せよ。
・ソーダ・コーラ・オレンジ・お茶の 4 種類を購入することができる
・「入金」ボタンを押すと入金することができる
・4 種類の飲み物から 1 つを選び、購入ボタンを押すことで購入できる
・購入する前に、入金をしなければいけない

ここで初めて「状態を持つプログラム」が必要になります。 入金ボタンを押して 1000 円入れたあと、購入ボタンを押すときには 「いま 1000 円入っている」ことを覚えていなければなりません。 ところが手続きは終わると局所変数が消えます(第 10 章 §4)。 だから残高をどこかに残す必要があります。

イベントをまたいで値を残す 3 つの方法
方法書き方向き・不向き
モジュール変数 Sub の外で Dim zandaka As Long 手軽。ただしブックを閉じると消える/どこから変わったか追いにくい(第 10 章 §4)
シートに書く Cells(1, 2) = zandaka 保存すれば残る。目で見て確かめられる。元資料の「Record 手続き」がこれ
フォームの部品に持たせる Label1.Caption 表示と兼用できるが、数値を文字列として扱うことになる

元資料 22 枚目に「変数の宣言、記録 Record 手続き」、 23 枚目に「ユーザフォームの初期化」とあります。 つまり元資料はモジュール変数で状態を持ち、Record 手続きでシートに記録する設計です。 この教材では処理系の都合で状態をシートに置く形にしましたが、 考え方は同じ ── 「イベントをまたいで残す場所を決める」ことが設計の第一歩です。

動かす③ 自動販売機 入金 → 選ぶ → 購入
ユーザフォーム (VendingMachine)
残高
0 円
飲み物を 1 つ選ぶ(OptionButton)
記録(Record 手続きがシートに書いたもの)
呼ばれた手続き
イベント駆動プログラム
' シートの使い方(=状態を残す場所)
'   A1: 残高    B1: 売上金額   C1: 売れた本数
'   A3:A6 品名  B3:B6 値段     C3:C6 在庫    D3:D6 売れた本数
'   A8: 選ばれた番号(1〜4)   B8: 入金額
'   A10 以下: 記録(Record 手続き)

Private Sub UserForm_Initialize()
    ' フォームを開いたときに 1 回だけ実行される
    Cells(1, 1) = 0                      ' 残高
    Cells(1, 2) = 0                      ' 売上
    Cells(1, 3) = 0                      ' 売れた本数

    Cells(3, 1) = "ソーダ":   Cells(3, 2) = 120: Cells(3, 3) = 3
    Cells(4, 1) = "コーラ":   Cells(4, 2) = 130: Cells(4, 3) = 3
    Cells(5, 1) = "オレンジ": Cells(5, 2) = 150: Cells(5, 3) = 2
    Cells(6, 1) = "お茶":     Cells(6, 2) = 110: Cells(6, 3) = 3

    Dim i As Integer
    For i = 3 To 6
        Cells(i, 4) = 0
    Next i
    For i = 10 To 40                     ' 記録も消す
        Cells(i, 1) = ""
    Next i
    Call Record("初期化しました")
End Sub

Private Sub Nyuukin_Click()
    Dim kingaku As Long
    kingaku = Cells(8, 2).Value                  ' 入れた金額
    Cells(1, 1) = Cells(1, 1).Value + kingaku    ' 残高に足す
    Call Record(kingaku & " 円 入金 残高 " & Cells(1, 1).Value & " 円")
End Sub

Private Sub Kounyuu_Click()
    Dim n As Integer
    Dim nedan As Long
    Dim zaiko As Integer

    n = Cells(8, 1).Value                        ' 選ばれた番号(1〜4)
    If n = 0 Then
        Call Record("飲み物が選ばれていません")
        Exit Sub
    End If

    nedan = Cells(n + 2, 2).Value
    zaiko = Cells(n + 2, 3).Value

    ' 買えない場合を先にはじく(第 6 章 §2.1)
    If zaiko <= 0 Then
        Call Record(Cells(n + 2, 1).Value & " は売り切れです")
        Exit Sub
    End If
    If Cells(1, 1).Value < nedan Then
        Call Record("残高が足りません(あと " & nedan - Cells(1, 1).Value & " 円)")
        Exit Sub
    End If

    ' ここまで来たら購入できる
    Cells(1, 1) = Cells(1, 1).Value - nedan      ' 残高を減らす
    Cells(1, 2) = Cells(1, 2).Value + nedan      ' 売上を増やす
    Cells(1, 3) = Cells(1, 3).Value + 1
    Cells(n + 2, 3) = zaiko - 1                  ' 在庫を減らす
    Cells(n + 2, 4) = Cells(n + 2, 4).Value + 1  ' その商品の売れた本数

    Call Record(Cells(n + 2, 1).Value & " を購入(" & nedan _
        & " 円) 残高 " & Cells(1, 1).Value & " 円")
End Sub

Private Sub Otsuri_Click()
    Dim z As Long
    z = Cells(1, 1).Value
    If z = 0 Then
        Call Record("残高がありません")
        Exit Sub
    End If
    Cells(1, 1) = 0
    Call Record("おつり " & z & " 円 返却しました")
End Sub

' 記録する手続き(元資料の Record 手続き)
Sub Record(ByVal s As String)
    Dim r As Integer
    r = 10
    Do While Cells(r, 1).Value <> ""            ' 空いている行を探す
        r = r + 1
        If r > 40 Then Exit Do
    Loop
    Cells(r, 1) = s
End Sub

試してみてほしい順番。 ① いきなり「購入」を押す(→ 選ばれていない) ② 飲み物を選んで「購入」(→ 残高が足りません) ③ 100 円だけ入金して「購入」(→ まだ足りない) ④ 1000 円入金して次々に購入(→ 在庫が減っていく) ⑤ オレンジを 3 本買おうとする(→ 売り切れ) ⑥「おつり」を押す(→ 残高が 0 に戻る)
どの順番で押しても、おかしなことにならないように書かれていることを確かめてください。 それがイベント駆動プログラムの品質です。

このプログラムのどこが「よい設計」か。

実物の自動販売機なら、さらに考えることがあります。 釣り銭の在庫(お釣りが出せないときは高額紙幣を受け付けない)、 2 人が同時に操作したらどうなるか、 停電したときに入金した額をどう扱うか ── 「状態を持つ」プログラムは、状態の数だけ考えることが増えます。 第 12 章までの「入力から出力を計算する」プログラムとは、難しさの質が違うのです。

6. 本物の Excel で作るときの手順

操作
①VBA の画面(Alt+F11)で「挿入」→「ユーザーフォーム」
②ツールボックスから部品を選び、フォームの上にドラッグして置く
③部品を選んでプロパティで名前(Name)と表示文字(Caption)を決める。 名前は意味のあるものに変えておく(CommandButton1 → Nyuukin など)
④部品をダブルクリックすると、 Private Sub 名前_Click() の枠が自動で作られる。中身を書く
⑤実行タブから「Sub/ユーザー フォームの実行」(または F5)で動かす (元資料 11 枚目)
⑥シートのボタンから開きたいときは、標準モジュールに UserForm1.Show と書いたマクロを作る

いちばん多いつまずき。 「ボタンを押しても何も起こらない」── たいていは手続きの名前が部品の名前と一致していないのが原因です。 部品の名前をあとから変えると、手続きの名前は自動では変わりません。 部品をダブルクリックして枠を作らせるのが確実です(綴りを間違えません)。

7. まとめ

  1. ユーザフォームは、値を入力してもらうためのウィンドウ。部品はマウスで並べる
  2. 覚えるのは Label・TextBox・OptionButton・CommandButton の 4 つでよい
  3. イベント駆動=利用者の操作が起点。始まりが 1 つではない
  4. 手続きの名前は 部品名 + _ + イベント名。綴り違いは無反応になる
  5. どの順番で操作されても正しく動くように書く
  6. 手続きが終わると局所変数は消える。イベントをまたいで残す場所を決めるのが設計の第一歩
  7. 状態を持つプログラムでは、買えない場合を先にはじく/ お金を減らす前に在庫を確かめるなど、順序が品質を左右する

章末問題

付録:元資料からの修正一覧

もとにしたのは「情報学入門(VBA)170-2019」全 35 枚です。

元資料この教材種別
イベント駆動が、これまでのプログラムとどう違うかが 「イベントが発生し、イベント駆動プログラムが実行される」の 1 文だけ この章の唯一の新しい概念なので §2 として立て、 始まりが 1 つでない/走る順は利用者が決める/手続きが終わると値が消えるという 3 点を表で対比した不足
手続きの名前が部品の名前とつながっていることの説明がない 部品名_イベント名 という規則を明示。 綴りが違うと無反応(エラーも出ない)という、いちばん多いつまずきを書いた不足
自動販売機で「変数の宣言、記録 Record 手続き」「ユーザフォームの初期化」と 見出しだけがあり、なぜ必要かが説明されていない 「イベントをまたいで状態を残す」という問題として立て、 3 つの方法(モジュール変数/シート/部品)を比較した不足
自動販売機の異常な操作(未選択・売り切れ・残高不足)への言及がない 3 つとも先にはじく形で実装し、お金を減らす前に在庫を確かめるという 順序の重要性を明記。試す順番も示した不足
コントロール一覧に MultiPage・TabStop・RefEdit が並列で載っている 現在ほとんど使われない。最初は 4 つで足りると絞った古い
「円と球」「標準体重」は第 3 章・第 7 章と同じ計算 円と球だけを残し、「受け取り先がセルからテキストボックスに変わるだけ」という 点に絞った(標準体重は第 7 章を参照)重複
35 枚のうち大半が Excel 画面のスクリーンショットで、 プログラムが画像の中にしかない 3 つの実際に動くフォームにした。 本物の Excel での作成手順は 6 段の表にまとめた(画面が変わっても読める形に)体裁
球の表面積・体積の式(12 枚目) 元資料では上付き文字つきで正しく書かれている (4πr2、(4/3)πr3)。第 3 章と同じ─

元資料で正しかったこと。 ユーザフォームの定義(値を入力する時にガイドをするウィンドウ)、 コントロール 15 種類の名称、 「クリック・選択によりイベントが発生し、イベント駆動プログラムが実行される」という説明、 実行方法(実行タブ →「Sub/ユーザー フォームの実行」)、 そして自動販売機という題材の選び方── いずれも適切です。 とくに自動販売機は「購入する前に入金をしなければいけない」という仕様が 状態を持つ必要を自然に生む点で、イベント駆動を学ぶ題材として的確です。 「入金 → 購入 → 残高不足 → 追加入金 → 購入」という 操作の流れを画面つきで 6 枚使って示しているのも、動きが伝わる良い構成です。