フォームのUnload時に値を戻せないデータ型
- 1.概要
- 1ー1.値が戻せないデータ型の確認(標準)
- 1ー2.対策1:値を戻すための改善
- 1ー3.対策2:フォームをUnloadして終了
- 2.システム補助
- 2ー1.ワークシート
- 2ー2.標準モジュール
- 3.標準状態のフォーム
- 3ー1.標準モジュール
- 3ー2.ユーザーフォーム
- 3ー2ー1.フォームレイアウト
- 3ー2ー2.フォームモジュール
- 3ー2ー2ー1.フォームレベル変数宣言
- 3ー2ー2ー2.フォーム起動関数
- 3ー2ー2ー3.フォームの終了
- 4.対策1(全てのデータ型を戻す仕様)
- 4ー1.標準モジュール
- 4ー2.ユーザーフォーム
- 4ー2ー1.フォームレイアウト
- 4ー2ー2.フォームモジュール
- 4ー2ー2ー1.フォームレベル変数宣言
- 4ー2ー2ー2.フォーム起動関数
- 4ー2ー2ー3.フォームの終了
- 4ー2ー2ー4.イベント時刻記入
- 5.対策2(フォームを必ずUnloadする仕様)
- 5ー1.標準モジュール
- よりみち(メモリー上にあるユーザーフォームを探す方法)
- 6.まとめ
- アプリ実例
- サンプルファイル
「ユーザーフォームへの値の渡し方・戻し方」では、シート側からフォームモジュール内の関数プロシージャを呼び出す事で、フォーム側と値の受け渡しができる事を紹介しました。しかしユーザーフォームを「Unload で閉じる」と、ユーザーフォーム側から値を戻す際、データ型によっては「戻す値が空」になります。
今回はこの「値を戻せないデータ型」と、その対策について紹介します。
また、Unloadでは無く「Hideで閉じる(=ユーザーフォームを隠す)」のであれば、どんなデータ型でも戻るのですが、Hideではユーザーフォームがメモリ上に残ってしまい、再度呼び出した時にはInitializeイベントが発生しませんし、また起動元のブックが閉じられてしまうとユーザーフォームが表示されない現象が発生する可能性があります。
そこでユーザーフォームをHideで閉じても、戻り値を受け取り後にUnloadする手段についても紹介します。
1.概要
1ー1.値が戻せないデータ型の確認(標準)
フォームの閉じ方で戻る値が変わる事を確認するため、「サンプルファイル」は図01のような流れとしました。ポイントは、フォーム上の値は「データ型の異なるフォームレベル変数」で保管し、戻り値もフォームレベル変数から取得するところです。図01
シート上の「標準」ボタンをクリックすると、ユーザーフォーム上にある「関数プロシージャ」を呼び出します。関数プロシージャには「様々なデータ型の値」を引数で渡します。
データ型は図02の11種とし、その値はワークシートの2行目(黄色背景)をデータとしました。なお「Object型」は、A2セル自体(Rangeオブジェクト)を渡しています。
|
|
|
ユーザーフォーム上の関数が引数として受け取った様々なデータ型の値は、一旦フォームレベル変数に格納します。これは、フォーム上のコントロール類を使って「引数で渡した値を元に、ユーザーが操作」する事をイメージしています。
フォームレベル変数に値を格納後、ユーザーフォームを起動します。
本来のユーザーフォームは、様々なコントロール類が配置されユーザーが操作する場所ですが、今回はユーザー操作を省略し、「閉じるボタン」のみにしてあります。
閉じるボタンは2種類あり、左側が「Me.Hide」で終了させます。Hideですのでフォームは隠されただけで、メモリ上には残っている状態です。一方右側は「Unload Me」で閉じます。こちらはメモリ上からも削除されます。
どちらの閉じ方をしても、標準モジュール側から「フォーム上の関数」を呼び出しているため、その関数内の処理が完了してからフォームが閉じる事になります。今回のフォーム上の関数は、標準モジュール側に「フォームレベル変数の値(=関数が受け取った値のまま のはず)」を戻すことにしています。
値を戻された標準モジュール側では、その値を文字列として連結し、メッセージボックスで表示させます。
以上を実際のフォームとメッセージで示したのが図03になります。ワークシートの2行目をデータ行とし、1行目に表示してあるデータ型でフォームとの受け渡しを行っています。
図03
シート上の「標準」ボタンをクリックすると、ダイアログが表示されます。そのダイアログ上のボタン「Me.Hide」および「Unload Me」をクリックするとダイアログが閉じ、メッセージボックスが表示されます。
メッセージボックスの内容を比較すると、「Me.Hide」で閉じた方は、フォームに渡した時の値が「全てのデータ型でそのまま戻って」きています。
一方「Unload Me」で閉じると、「Object型」「String型」「Variant型」では、データが空となって戻ってくる事が分かります。尚その他のデータ型では正しく戻り、まとめると図04のようになります。
データ型 | 戻り値 |
---|---|
Object | → Nothing |
String | → "" (値ゼロの文字列) |
Variant | → Empty |
Boolean | 渡した値がそのまま 戻る |
Integer | |
Long | |
Double | |
Single | |
Currency | |
Date | |
Byte |
図04で、空となって戻ってきたデータ型とその値を見ると、それぞれ「データ型の初期値(=宣言したままの値)」である事が分かります。なおVariant型も含まれており、「変数宣言時にデータ型を指定しない」とVariant型を指定した事になるので、事は重大です。
その原因を考えてみましたが、なぜこの3つのデータ型がUnloadすると初期化されてしまうのか、理由は分かりませんでした。しかし、ユーザーフォームとの値の受け渡し方法としては一般的と思われる方法で、このような現象が発生すると、2つの点で困ることがあります。
1つは、ユーザーフォームをUnloadした時にデータ型により戻らない値が出てくる事を避けようとして、フォームの用途が狭められてしまう事です。
もう1つは、それを避けるためにHideメソッドを使用すると、フォームがメモリー上に残ってしまいます。メモリー上に残ることで、フォームを再度起動した時に「Initializeイベントが実行されない」事になります。
それよりも深刻なのは、複数ブックを開くアプリでフォームをHideで閉じた場合です。もし起動元のブックがHide中に閉じられてしまうと再度フォームが起動できない という不具合が発生するのです。(詳細の動きは「両矢印線の図形を日程線としてセル上に描画」を参照下さい)
そこで、フォームレベル変数が初期化されてしまっても、値をキチンと戻す策を「対策1」で紹介します。
またフォームをHideで閉じても(=全データ型が戻る)、標準モジュール側でUnloadする策「対策2」も紹介します。
1ー2.対策1:値を戻すための改善
「Unloadしても、正常な値を戻せるフォーム」の流れが図05です。図05
フォームレベル変数に引数の値を書き込む際に、Object型・String型・Variant型のデータについては「フォーム上のLabelに値を書き込み」ます。そしてフォームが閉じられ、戻し値の処理をする際には、3つのデータ型に対しては「Labelの値を戻す」ようにします。
この流れを実際のフォームとメッセージボックスで示したのが図06になります。
図06
操作は標準の場合と全く同じです。
シート上の「対策1」のボタンをクリックするとフォーム(UserForm1 → UserForm2に変わっています)が開きます。そして右側の「Unload Me」ボタンをクリックして開くメッセージボックスには、「フォームから戻された値」が全て正常値になっている事が確認できます。
1ー3.対策2:フォームをUnloadして終了
「Hideで閉じても、Unloadさせるフォーム」の流れが図07です。図07
Unloadさせる仕掛けは、標準モジュール側にあります。フォームからの戻り値を受け取った後、フォームが「メモリー上に存在するか」を調べ、存在する(=Hideで閉じている)のであればUnloadします。
この流れを、対策1の時と比較したのが図08になります。
図08
Unloadした証拠として、今回は「Initializeイベント発生時刻(INI時刻)」と「Activateイベント発生時刻(ACT時刻)」を比較する事としました。つまりUnloadしているフォームを起動したのであれば、起動時にInitializeイベントとActivateイベントを通過しますので「INI時刻≒ACT時刻」になります。
一方Hideしている状態のフォームを再起動したのであれば、再起動時にはInitializeイベントは発生せずにActivateイベントのみが発生します。ですのでINI時刻は、前回起動時の時刻となるため、「INI時刻 << ACT時刻」となります。
2.システム補助
以下では「サンプルファイル」のプログラムコードについて説明していきます。2ー1.ワークシート(Sheet1)
今回対象としているデータ型(図02)の11種類を値として渡すために、図09のようにシート上にデータを並べました(A2~K2セル)。なおObject型では、A2セルのセル範囲(Range("A2") )を渡すことにしました。図09
また、フォーム起動用のボタンをシート上に3つ並べ、それぞれ標準モジュールのマクロ(UForm01~UForm03)を登録しています。
尚、渡す値は全て数値(サンプルファイルでは 10)としました(文字列だったら"10"となる)。Boolean型(D2セル)も、例えば「10」という数値を指定すれば、ゼロ以外ですので「True」と判断してくれますが、とりあえず「True」としています。
またObject型・String型・Variant型には文字列を指定しても問題ありませんが、Boolean型以降の数値系に文字列を指定すると、引数として渡す時にエラーが出ます。そのような必要性がある場合は、文字列を数値型に変換するような処置が必要です。
2ー2.標準モジュール(Module1)
プログラムの流れの中では、最後の工程である「フォームから戻された値をMsgBoxで表示」するのが図10です。引数として11要素の一次元配列を受け取ります。この配列の中に11種類のデータ型の値が入っています。なお、フォーム内ではArray関数を使って戻し値の配列を作成していますので、インデックスはゼロ始まりです。
- '========== ⇩(1) 戻り値をMsgBoxで表示 ============
- Sub OutPut(Ret As Variant)
- Dim str1 As String '←Object型の戻り値の文字列化
- Dim str As String '←MsgBoxに表示する文字列
- If Ret(0) Is Nothing Then
- str1 = "Nothing"
- Else
- str1 = Ret(0).Value
- End If
- str = "Object=" & str1
- str = str & vbNewLine & "String=" & Ret(1)
- str = str & vbNewLine & "Variant=" & Ret(2)
- str = str & vbNewLine & "Boolean=" & Ret(3)
- str = str & vbNewLine & "Integer=" & Ret(4)
- str = str & vbNewLine & "Long=" & Ret(5)
- str = str & vbNewLine & "Double=" & Ret(6)
- str = str & vbNewLine & "Single=" & Ret(7)
- str = str & vbNewLine & "Currency=" & Ret(8)
- str = str & vbNewLine & "Date=" & Ret(9)
- str = str & vbNewLine & "Byte=" & Ret(10)
- MsgBox str
- End Sub
戻り値の内、Object型の値はMsgBoxで直接表示することが出来ません。今回のObject型はRange型として受け渡ししていますので、指定されたセル範囲(Range)の値(Value値)を使うことにします。またObject型が初期化されてしまっていた場合には「"Nothing"」という文字列を表示することにしました。
そのObject型のデータ処理をしているのが05~09行目です。
05行目「If Ret(0) Is Nothing Then」で、Object型(配列の1番目)の要素位置にセル範囲が入っているかを確認します。
セル範囲が入っていない場合は、06行目「str1 = "Nothing"」でMsgBox表示値を「Nothing」という文字列にします。
セル範囲が入っている場合は、08行目「str1 = Ret(0).Value」で「そのセルの値」を表示します。
なお05~09行目の代わりに「IIf(Ret(0) Is Nothing, "Nothing", Ret(0).Value)」と表現できそうですが、Ret(0)がNothingの場合には「Ret(0).Value」の部分でエラーが出てしまいます。IIF関数内は、全てが常に「エラーとならない値」である必要があります。
その他のデータ型はObject型では無いため、そのまま文字列にする事が出来ます。もし初期化されていた場合には、
・String型は「値ゼロの文字列」ですが、MsgBoxでは「""(長さゼロの文字列)」で表示されます。
・Variant型の初期値はEmptyですが、これも「""(長さゼロの文字列)」と表示されます。
・Boolean型の初期値はFalseですので、「False」と表示されることになります。
・それ以外のデータ型は数値系なので、初期化されたらゼロと表示されます。
ですのでObject型以外はそのまま文字列にできるので、11~21行目で1要素ずつ文字列をつなげていきます。
例えば11行目「str = "Object=" & str1」では「Object=」の後に、05~09行目で作成した「セル範囲の値」又は「Nothingの文字列」をつなげます。そして12行目「str = str & vbNewLine & "String=" & Ret(1)」では、Objectのデータ(str1)の後ろに改行を入れ、その後ろに「String型のデータ(戻り値配列の2要素目の値)」をつなげていきます。
11種類のデータ型の値をつなげた後、23行目「MsgBox str」でメッセージボックス表示させます。
3.標準状態のフォーム
3ー1.標準モジュール(Module1)
シート上の「標準」ボタンをクリックした時に呼び出されるのが図11です。- '========== ⇩(2) 標準フォームを起動 ============
- Sub UForm01()
- Dim Ret As Variant '←UserFormからの戻り値(配列)
- Ret = UserForm1.UFstart(Range("A2"), Range("B2"), Range("C2"), _
- Range("D2"), Range("E2"), Range("F2"), Range("G2"), _
- Range("H2"), Range("I2"), Range("J2"), Range("K2"))
- Call OutPut(Ret)
- End Sub
34~36行目の右辺で、フォーム(UserForm1)内の「UFstart関数プロシージャ(図14)」を呼び出しています。呼び出す際、11個の引数を渡します。
11個の引数は、今回セル範囲(Range)そのもので渡すことにしました。引数を受け取る側(UserFormのUFstart関数)では引数のデータ型を指定していますが、そのデータ型に合った値に勝手に変換をしてくれる機能を使っています。
例えばObject型であればそのままRange型となりますし、その他のデータ型ではRange("〇〇").Valueというセルに入っている値になります。但しVariant型は何でもありのデータ型ですので、Range型としてフォーム側に渡されます。
フォームが閉じた後に、UFstart関数プロシージャから戻り値が戻ってきて、34行目の左辺である変数Retに格納されます。
最後に38行目「Call OutPut(Ret)」で、フォームからの戻り値(配列Ret)を図10のOutPutプロシージャに渡し、メッセージボックスに戻り値を文字列にして表示させます。
3ー2.ユーザーフォーム(UserForm1)
3ー2ー1.フォームレイアウト
ユーザーフォームのレイアウトは図12のようにしました。図12
フォームを閉じるためのボタンを2つ配置し、左側(CommandButton1)は「Hide」を実行し、右側(CommandButton2)は「Unload」を実行するものとしました。
なおボタン表面の文字列は、配置時にCaptionプロパティを手動設定しています。
3ー2ー2.フォームモジュール
3ー2ー2ー1.フォームレベル変数宣言
フォーム内で共通使用する変数の宣言を図13のように宣言部(フォームモジュール先頭部)で行います。今回対象としている11種のデータ型(図02)の変数をUFtype1~UFtype11という名前にしています。
- '========== ⇩(3) フォームレベル変数の宣言 ============
- Dim UFtype1 As Object
- Dim UFtype2 As String
- Dim UFtype3 As Variant
- Dim UFtype4 As Boolean
- Dim UFtype5 As Integer
- Dim UFtype6 As Long
- Dim UFtype7 As Double
- Dim UFtype8 As Single
- Dim UFtype9 As Currency
- Dim UFtype10 As Date
- Dim UFtype11 As Byte
3ー2ー2ー2.フォーム起動関数
標準モジュール側の図11から呼び出されるのが、フォームモジュール(UserForm1)上のUFstart関数(図14)です。11個(データ型11種)の引数を受け取ります。
- '========== ⇩(4) フォーム起動関数 ============
- Public Function UFstart(Type1 As Object, Type2 As String, Type3 As Variant, _
- Type4 As Boolean, Type5 As Integer, Type6 As Long, Type7 As Double, _
- Type8 As Single, Type9 As Currency, Type10 As Date, Type11 As Byte)
- Set UFtype1 = Type1
- UFtype2 = Type2: UFtype3 = Type3
- UFtype4 = Type4: UFtype5 = Type5: UFtype6 = Type6: UFtype7 = Type7
- UFtype8 = Type8: UFtype9 = Type9: UFtype10 = Type10: UFtype11 = Type11
- Me.Show
- UFstart = Array(UFtype1, UFtype2, UFtype3, _
- UFtype4, UFtype5, UFtype6, UFtype7, _
- UFtype8, UFtype9, UFtype10, UFtype11)
- End Function
75~78行目では、受け取った引数値を「フォームレベル変数」に代入します。この内1番目の引数はObject型ですので、Setステートメントを使い75行目「Set UFtype1 = Type1」で、「Object」として変数に代入します。
またVariant型である引数type3は、標準モジュール側からは「Rangeオブジェクト」としてセル範囲が渡されていますが、76行目で「UFtype3 = Type3」とSetを使わずに代入していますので、Value値(数値や文字列:この場合は10という数値)として、フォームレベル変数UFtype3に代入されます。
他の引数は、引数のデータ型に従って数値や文字列、日付に変換された値を受け取るので、「UFtype2 = Type2」の様なコードにより、引数の値がそのままフォームレベル変数に代入される事になります。
代入が完了したら、80行目「Me.Show」で自分自身(UserForm1)を起動します。パラメータ無しですのでモーダル起動となります。
フォーム上の「Me.Hide」か「Unload Me」のボタンをクリックするとフォームが終了処理を開始し、制御は82~84行目に移り、UFstart関数プロシージャの戻り値を設定します。
82~84行目「UFstart = Array(UFtype1, UFtype2, ・・・」では、11個の「フォームレベル変数」をArray関数を使って配列の形にし、関数プロシージャの戻り値に設定します。実際の場面では、これほど多くの戻り値を戻す事は少なく、単一変数を戻り値にするか、または引数を戻り値変数にするか だと思います。
気を付けないといけないのが、80行目で「フォームをモードレスで起動」してしまうと、起動と併行して次のコードに制御が進み「フォーム上で何もデータ操作をしない状態でデータが戻る(≒フォームを表示する意味が無い)」ことになります。ちなみに、この時の状態はフォーム起動中(フォームはメモリー上に居る状態)なので、フォームをHideしたのと同じ形となり、フォームレベル変数は初期化されずに全ての値が戻ります。
3ー2ー2ー3.フォームの終了
フォーム上のボタンをクリックした時に呼び出されるのが、図15です。- '========== ⇩(5) フォームをHideで閉じる ============
- Private Sub CommandButton1_Click()
- Me.Hide
- End Sub
- '========== ⇩(6) フォームをUnloadで閉じる ============
- Private Sub CommandButton2_Click()
- Unload Me
- End Sub
「Me.Hide」ボタン(CommandButton1)をクリックした時には92行目「Me.Hide」が実行され、ユーザーフォームは「隠された」状態になります。
一方「Unload Me」ボタン(CommandButton2)をクリックした時には97行目「Unload Me」が実行され、ユーザーフォームは「閉じられる(=メモリー上から削除される)」状態になります。
どちらの終了処理を実行しても、関数UFstart(図14)には続きがありますので、制御は図14の82行目以降に進みます。
4.対策1(全てのデータ型を戻す仕様)
変数が初期化されてしまう「Object型」「String型」「Variant型」については、値をフォーム上のLabelに保管しておき、フォームを閉じる時に「Labelのデータを使って戻り値とする」のが、この対策1の内容です。4ー1.標準モジュール(Module1)
シート上の「対策1」ボタンをクリックした時に呼び出されるのが図16です。- '========== ⇩(7) 対策1フォームを起動 ============
- Sub UForm02()
- Dim Ret As Variant '←UserFormからの戻り値(配列)
- Ret = UserForm2.UFstart(Range("A2"), Range("B2"), Range("C2"), _
- Range("D2"), Range("E2"), Range("F2"), Range("G2"), _
- Range("H2"), Range("I2"), Range("J2"), Range("K2"))
- Call OutPut(Ret)
- End Sub
コード自体は、「標準」のコード(図11)と全く同じです。
114~116行目の右辺で、フォーム(UserForm2)内の「UFstart関数プロシージャ(図20)」を呼び出しています。呼び出す際に11個の引数を渡します。
フォームが閉じた後に、UFstart関数プロシージャからの戻り値(配列)が左辺の変数Retに格納され、118行目「Call OutPut(Ret)」で、メッセージボックスに戻り値を表示させます。
4ー2.ユーザーフォーム(UserForm2)
対策1と対策2のユーザーフォームは共通としました。4ー2ー1.フォームレイアウト
対策のユーザーフォームのレイアウトは図17のようにしました。図17
フォームを閉じるためのボタン2つは、「標準」と同じです。
その他にボタンの下方に、Labelを3つ配置しています。これは、それぞれ「Object型」「String型」「Variant型」の値を保管しておく為の場所です。
またボタン上方のLabel4~Label5は、フォーム起動時のイベント発生時刻を記入するためのものです。今回はLabel4に「Initializeイベントの時刻(INI時刻)」を、Label5に「Activateイベントの時刻(ACT時刻)」を記入しています。
この2つの時刻を比較することで、図18のように「フォームがどの様な状態だったか」が分かります。
時刻の比較 | フォーム起動時のイベント順序 | 前回フォームの状態 |
---|---|---|
INI時刻 ≒ ACT時刻 | Initialize → Activate の順でイベント発生 | Unload |
INI時刻 « ACT時刻 | Initializeは発生せず、Activateイベントのみ発生 | Hide |
なお今回は、フォームモジュールのUFstart関数の中でLabelへの書き込みも行っているため、INI時刻とACT時刻の差が数秒出てしまう場合もありそうです(もちろんPCの処理速度で変わります)。フォームをHideで閉じ、少し時間をおいてから再度開くと、時刻の比較がし易いかと思います。
4ー2ー2.フォームモジュール
4ー2ー2ー1.フォームレベル変数宣言
フォーム内で共通使用する変数の宣言を図19のように宣言部で行います。内容は図13と同じです。今回対象としている11種のデータ型(図02)の変数をUFtype1~UFtype11という名前にしています。
- '========== ⇩(8) フォームレベル変数の宣言 ============
- Dim UFtype1 As Object
- Dim UFtype2 As String
- Dim UFtype3 As Variant
- Dim UFtype4 As Boolean
- Dim UFtype5 As Integer
- Dim UFtype6 As Long
- Dim UFtype7 As Double
- Dim UFtype8 As Single
- Dim UFtype9 As Currency
- Dim UFtype10 As Date
- Dim UFtype11 As Byte
4ー2ー2ー2.フォーム起動関数
標準モジュールの図16・図23から呼び出されるのが、フォームモジュール(UserForm2)上のUFstart関数(図20)です。11個(データ型11種)の引数を受け取ります。
- '========== ⇩(9) フォーム起動関数 ============
- Public Function UFstart(Type1 As Object, Type2 As String, Type3 As Variant, _
- Type4 As Boolean, Type5 As Integer, Type6 As Long, Type7 As Double, _
- Type8 As Single, Type9 As Currency, Type10 As Date, Type11 As Byte)
' Set UFtype1 = Type1' UFtype2 = Type2: UFtype3 = Type3- UFtype4 = Type4: UFtype5 = Type5: UFtype6 = Type6: UFtype7 = Type7
- UFtype8 = Type8: UFtype9 = Type9: UFtype10 = Type10: UFtype11 = Type11
- Me.Label1.Caption = Type1.Address
- Me.Label2.Caption = Type2
- Me.Label3.Caption = Type3
- Me.Show
- Set UFtype1 = Range(Me.Label1.Caption)
- UFtype2 = CStr(Me.Label2.Caption)
- UFtype3 = CVar(Me.Label3.Caption)
- UFstart = Array(UFtype1, UFtype2, UFtype3, _
- UFtype4, UFtype5, UFtype6, UFtype7, _
- UFtype8, UFtype9, UFtype10, UFtype11)
- End Function
157~158行目では、引数で受け取った値の内で「フォームをUnloadしても、正しく戻せるデータ型」の分をフォームレベル変数に代入しています。例えばBoolean型では「UFtype4 = Type4」と、変数UFtype4に値を代入します。
残りの「フォームをUnloadすると、初期化されてしまう3つのデータ型」は、160~162行目で「フォーム上のLabel」に値を書き込みます。
今回Object型はセル範囲(Range型)としましたので、160行目「Me.Label1.Caption = Type1.Address」で、セルのアドレスをLabelに書き込んでいます。
またString型の場合は、161行目「Me.Label2.Caption = Type2」のように、値をLabelに書き込みます。Variant型の場合は162行目「Me.Label3.Caption = Type3」になります。
なお実際のアプリでは、フォーム上のコントロール類を操作し、例えばString型が変更された場合には、その値を都度Labelに書き込む必要があります。また今回はセル範囲ですのでAddressプロパティが使用できましたが、「文字列には出来ないObject」もありますので、その場合この手法では困難です。
フォームレベル変数、およびLabelへの値書き込みが完了したら、164行目「Me.Show」で自分自身(UserForm2)を起動します。
フォーム上のボタンをクリックし、フォームがHide又はUnload処理をしたら、制御は166行目に戻ってきます。
166~168行目では、Labelに書き込んだ値を一旦フォームレベル変数に戻しています。ここでのポイントは、
フォームレベル変数が初期化されるのは、Unloadステートメントを実行した時
と言うことです。ですので制御が図20の166行目に戻ってきた時には「Unloadされていれば初期化」済みであり、また166行目以降でフォームレベル変数に代入した値は初期化されない 事になります。
166行目「Set UFtype1 = Range(Me.Label1.Caption)」では、Label1に書き込んだ「アドレスの文字列」をセル範囲に変換し、フォームレベル変数UFtype1にセットしています。
また167行目「UFtype2 = CStr(Me.Label2.Caption)」ではLabel2の値をCStr関数で文字列に変換してフォームレベル変数に代入します。
168行目「UFtype3 = CVar(Me.Label3.Caption)」も同様にCVar関数でVariant型に変換し代入します。
これで、フォームレベル変数の値が全部揃いましたので、170~172行目「UFstart = Array(UFtype1, UFtype2,・・・」でArray関数を使って11種のデータを配列にし、UFstart関数プロシージャの戻り値にしています。
なお、お気づきとは思いますが、フォームレベル変数のUFtype1~UFtype3 を無くして、166~168の式を直接Array関数の引数に使用してもOKです。
(戻り値の式が、標準の図14の82~84行目と同一の方が分かり易いかと思い、このような間接的な方法にしました。)
4ー2ー2ー3.フォームの終了
フォーム上のボタンをクリックした時に呼び出されるのが、図21です。- '========== ⇩(10) フォームをHideで閉じる ============
- Private Sub CommandButton1_Click()
- Me.Hide
- End Sub
- '========== ⇩(11) フォームをUnloadで閉じる ============
- Private Sub CommandButton2_Click()
- Unload Me
- End Sub
フォームを閉じるコードは、標準の図15と全く同じです。
「Me.Hide」ボタン(CommandButton1)をクリックした時には182行目「Me.Hide」が実行され、ユーザーフォームは「隠された」状態になります。
一方「Unload Me」ボタン(CommandButton2)をクリックした時には187行目「Unload Me」が実行され、ユーザーフォームは「閉じられる(=メモリー上から削除される)」状態になります。
どちらの終了処理を実行しても、関数UFstart(図20)には続きがありますので、制御は図20の166行目以降に進みます。
4ー2ー2ー4.イベント時刻記入
前回のフォームが「UnloadしたのかHideしたのか」を確認するための、InitializeイベントとActivateイベントの発生時刻をフォーム上のLabelに記入するのが図22です。- '========== ⇩(12) Initializeイベント時刻記入 ============
- Private Sub UserForm_Initialize()
- Me.Label4.Caption = "INI=" & Time()
- End Sub
- '========== ⇩(13) Activateイベント時刻記入 ============
- Private Sub UserForm_Activate()
- Me.Label5.Caption = "ACT=" & Time()
- End Sub
初めてフォームを起動したり、前回フォームをUnloadしたりした時には、Initializeイベントがまず発生します。その時は202行目「Me.Label4.Caption = "INI=" & Time()」で、フォーム上のLabel4にInitializeイベントの発生時刻を書き込みます。
分かり易い様に、時刻の前に「INI=」を加えています。
前回フォームを「Unloadで閉じたかHideで閉じたか」に関係なく、フォーム表示時にはActivateイベントが発生します。
207行目「Me.Label5.Caption = "ACT=" & Time()」では、フォーム上のLabel5にActivateイベントの発生時刻を書き込みます。
5.対策2(フォームを必ずUnloadする仕様)
対策2(フォームを必ずUnloadする仕様)では、ユーザーフォームは対策1と同じ「UserForm2」を使用します。対策1と異なるのは、標準モジュール側での処理のみです。5ー1.標準モジュール(Module1)
シート上の「対策2」ボタンをクリックした時に呼び出されるのが図23です。- '========== ⇩(14) 対策フォームを起動し、最後にUnload ============
- Sub UForm03()
- Dim Ret As Variant '←UserFormからの戻り値(配列)
- Dim i As Long '←メモリー上に存在するユーザーフォームの数
- Ret = UserForm2.UFstart(Range("A2"), Range("B2"), Range("C2"), _
- Range("D2"), Range("E2"), Range("F2"), Range("G2"), _
- Range("H2"), Range("I2"), Range("J2"), Range("K2"))
- For i = UserForms.Count - 1 To 0 Step -1
- If LCase(UserForms(i).Name) = LCase("UserForm2") Then Unload UserForm2
- Next i
- Call OutPut(Ret)
- End Sub
「標準(図11)」「対策1(図16)」のコードに対し、229~231行目で「後からフォームをUnload」する部分を追加しています。
まず225~227行目の右辺で、フォーム(UserForm2)内の「UFstart関数プロシージャ(図20)」を呼び出しています。呼び出す際、11個の引数を渡します。
フォームが閉じ、UFstart関数プロシージャから戻り値(配列)をRet変数で受け取った後、229~231行目で(Unloadされていなければ)フォームをUnloadします。
まず、ユーザーフォームがメモリー上に存在(Showメソッドで開いている、Loadしている、Hideで隠している)している場合には、それらのフォームはUserFormsコレクションでまとめる事が出来ます。そして、そのフォームの数は「UserForms.Count」で数える事ができます(メモリー上に1つも存在しなければ、UserForms.Count = 0 となる)。
229行目「For i = UserForms.Count - 1 To 0 Step -1」では、その存在しているフォームの数だけカウンタ変数iを回します。コレクション内の1つ1つのユーザーフォームは、「UserForms(i)」という形で表す事が出来ます。
なお「Step -1」と逆順で回しています。正順で回してしまうと、Unloadした事で順番が繰り上がって(=For~Nextの途中で、UserForms.Countの数が減る)しまい、当初は最後の番号だったフォームが見当たらなくなってしまいエラーが発生してしまうためです。
230行目「If LCase(UserForms(i).Name) = LCase("UserForm2") Then Unload UserForm2」では、メモリー上に生きている1つ1つのユーザーフォームのオブジェクト名(Nameプロパティ)と、見つけたい名前「"UserForm2"」を比較します。但し、オブジェクト名を調べる場合には、「大文字小文字は区別」されるため、LCase関数を付けて「全て小文字で比較」するようにしています(UCaseで、大文字に揃えてもOKです。)。
そして「"UserForm2"」が存在した場合には、「Unload UserForm2」でユーザーフォームをUnloadします。
Hide状態のフォームを見つける手段として、230行目のような「オブジェクト名を文字列で探す」手段では無く、
「If UserForms(i).Name = UserForm2.Name Then Unload UserForm2」 としても良さそうに思えます。しかしこの方法のデメリットは、UserForm2のNameプロパティ値(=オブジェクト名)を取得するために「一旦UserForm2をLoad」する必要があるのです。Loadするという事はInitializeイベントも発生しますので、そこに記載されているコードが実行されてしまいます。 Initializeイベント内が、今回のようにあまり支障の無いアプリであれば問題無いのですが、影響が発生してしまう場合にはウカツにLoadする訳にもいきません。 また、他サイトで良く紹介されている方法が図24です。
これは、メモリ上に存在するフォームの数が1つ以上存在する場合には、UserForm2をUnloadするものです。もしUserForm2が「Hideで閉じられていれば、メモリー上に存在しているので個数に反映される」という現象を利用しています。 しかし目的のフォームがUnloadされていても、別なフォームが開かれている場合には「Unload UserForm2」が実行されてしまい、上記と同様に「一旦UserForm2をLoad」してしまいます。 ちなみに、Unloadしたフォームを更にUnloadするのは、Initializeイベントが実行される以外は特に問題はありません。 また図23に近い形で、図25のように「For Each~Next でUserFormを1つずつ取得」する方法も考えられます。これでしたら、正順・逆順を考える必要はありません。また図25では、取り出したUserFormは「Is演算子」を使ってオブジェクト同士の比較をしています。
本当は図23と同じように、For Eachで1つずつ取り出したものを、見え消しで示した「If LCase(uf.Name) = LCase しかしそうなると、オブジェクト比較の際に「UserForm2のオブジェクトを確認」する事になるのでの、「一旦UserForm2をLoad」することになります。 またTypeOf演算子を先頭に付け「If TypeOf uf Is UserForm2 Then Unload UserForm2」としても同じ結果が得られるのですが、今回はオブジェクトの型を調べているのではなく、本人か否かを直接調べているようなものなので、「TypeOfは不要」と判断しました。 また、Visibleプロパティを使用した図26のような方法も良く見かけます。
しかし、Hideしたフォームは「Visible = False」ですので、この方法は今回使えません。また、Loadしただけのフォームも「Visible = False」ですので、「Load済みなのに、Unloadの対象にならない」という矛盾した結果にもなります。 |
最後に233行目「Call OutPut(Ret)」で、メッセージボックスに戻り値を表示させます。
6.まとめ
今回の前提状況は、標準モジュール等から「フォーム内に置いた関数プロシージャ」を呼び出し、フォーム上のコントロール類を使用して値を作成・調整し、その値は「フォームレベル変数」に保存している場合です。この場合フォームをUnloadすると、Object型・String型・Variant型のフォームレベル変数は値が初期化されてしまい、関数プロシージャの戻り値も空になってしまいます。なおHideで閉じた場合は、戻り値は正常です。
対策1は、「初期化されてしまう変数値は、Label上で保管」し、戻り値を確保する仕様です。
対策2は、Hideでフォームを閉じた時の不具合を解消するため、フォーム終了後に必ずUnloadするという仕様です。
と言って「対策1」や「対策2」は必須という訳で無く、例えば「フォームとの値のやり取り方法を変更」する事でも目的は達せられますし、また「Hideでフォームを閉じる」事で問題の無いアプリ等では、QueryCloseイベントで「Cancel = True」と「Me.Hide」を実行するようにすれば、標準のままでも正しく値を戻す事になります。
また極端に言えば、苦労してUnloadせずとも、状況さえ許せば「Endステートメント」を実行する方法も有りだと思います。
要は、状況や使われ方をよく把握し、その仕様に合ったものにすることが大切だと思います。
アプリ実例
「備品の予約・貸出・記録ができる貸出管理表」サンプルファイル
フォームのUnload時に値を戻せないデータ型(its-031.xlsm)
セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始) 解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。 |