2023/03/11

フォームのUnload時に値を戻せないデータ型




ユーザーフォームへの値の渡し方・戻し方」では、シート側からフォームモジュール内の関数プロシージャを呼び出す事で、フォーム側と値の受け渡しができる事を紹介しました。しかしユーザーフォームを「Unload で閉じる」と、ユーザーフォーム側から値を戻す際、データ型によっては「戻す値が空」になります。
今回はこの「値を戻せないデータ型」と、その対策について紹介します。

また、Unloadでは無く「Hideで閉じる(=ユーザーフォームを隠す)」のであれば、どんなデータ型でも戻るのですが、Hideではユーザーフォームがメモリ上に残ってしまい、再度呼び出した時にはInitializeイベントが発生しませんし、また起動元のブックが閉じられてしまうとユーザーフォームが表示されない現象が発生する可能性があります。
そこでユーザーフォームをHideで閉じても、戻り値を受け取り後にUnloadする手段についても紹介します。

1.概要

1ー1.値が戻せないデータ型の確認(標準)

フォームの閉じ方で戻る値が変わる事を確認するため、「サンプルファイル」は図01のような流れとしました。ポイントは、フォーム上の値は「データ型の異なるフォームレベル変数」で保管し、戻り値もフォームレベル変数から取得するところです。
シート~フォーム間の流れ
図01


シート上の「標準」ボタンをクリックすると、ユーザーフォーム上にある「関数プロシージャ」を呼び出します。関数プロシージャには「様々なデータ型の値」を引数で渡します。
データ型は図02の11種とし、その値はワークシートの2行目(黄色背景)をデータとしました。なお「Object型」は、A2セル自体(Rangeオブジェクト)を渡しています。
No.データ型
1Object(Range)
2String
3Variant
4Boolean
No.データ型
5Integer
6Long
7Double
8Single
No.データ型
9Currency
10Date
11Byte
 
図02


ユーザーフォーム上の関数が引数として受け取った様々なデータ型の値は、一旦フォームレベル変数に格納します。これは、フォーム上のコントロール類を使って「引数で渡した値を元に、ユーザーが操作」する事をイメージしています。
フォームレベル変数に値を格納後、ユーザーフォームを起動します。

本来のユーザーフォームは、様々なコントロール類が配置されユーザーが操作する場所ですが、今回はユーザー操作を省略し、「閉じるボタン」のみにしてあります。
閉じるボタンは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


図04で、空となって戻ってきたデータ型とその値を見ると、それぞれ「データ型の初期値(=宣言したままの値)」である事が分かります。なおVariant型も含まれており、「変数宣言時にデータ型を指定しない」とVariant型を指定した事になるので、事は重大です。

その原因を考えてみましたが、なぜこの3つのデータ型がUnloadすると初期化されてしまうのか、理由は分かりませんでした。しかし、ユーザーフォームとの値の受け渡し方法としては一般的と思われる方法で、このような現象が発生すると、2つの点で困ることがあります。

1つは、ユーザーフォームをUnloadした時にデータ型により戻らない値が出てくる事を避けようとして、フォームの用途が狭められてしまう事です。
もう1つは、それを避けるためにHideメソッドを使用すると、フォームがメモリー上に残ってしまいます。メモリー上に残ることで、フォームを再度起動した時に「Initializeイベントが実行されない」事になります。
それよりも深刻なのは、複数ブックを開くアプリでフォームをHideで閉じた場合です。もし起動元のブックがHide中に閉じられてしまうと再度フォームが起動できない という不具合が発生するのです。(詳細の動きは「両矢印線の図形を日程線としてセル上に描画」を参照下さい)

そこで、フォームレベル変数が初期化されてしまっても、値をキチンと戻す策を「対策1」で紹介します。
またフォームをHideで閉じても(=全データ型が戻る)、標準モジュール側でUnloadする策「対策2」も紹介します。

1ー2.対策1:値を戻すための改善

「Unloadしても、正常な値を戻せるフォーム」の流れが図05です。
フォーム上のLabelに値を仮保管する対策1の流れ
図05


フォームレベル変数に引数の値を書き込む際に、Object型・String型・Variant型のデータについては「フォーム上のLabelに値を書き込み」ます。そしてフォームが閉じられ、戻し値の処理をする際には、3つのデータ型に対しては「Labelの値を戻す」ようにします。

この流れを実際のフォームとメッセージボックスで示したのが図06になります。
フォーム上のLabelに値を仮保管する対策1のイメージ
図06


操作は標準の場合と全く同じです。
シート上の「対策1」のボタンをクリックするとフォーム(UserForm1 → UserForm2に変わっています)が開きます。そして右側の「Unload Me」ボタンをクリックして開くメッセージボックスには、「フォームから戻された値」が全て正常値になっている事が確認できます。

1ー3.対策2:フォームをUnloadして終了

「Hideで閉じても、Unloadさせるフォーム」の流れが図07です。
Hideで閉じてもUnloadさせる対策2の流れ
図07


Unloadさせる仕掛けは、標準モジュール側にあります。フォームからの戻り値を受け取った後、フォームが「メモリー上に存在するか」を調べ、存在する(=Hideで閉じている)のであればUnloadします。

この流れを、対策1の時と比較したのが図08になります。
Hideで閉じてもUnloadさせる対策2のイメージ
図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. '========== ⇩(1) 戻り値をMsgBoxで表示 ============
  2. Sub OutPut(Ret As Variant)
  3.  Dim str1 As String    '←Object型の戻り値の文字列化
  4.  Dim str As String    '←MsgBoxに表示する文字列
  5.  If Ret(0) Is Nothing Then
  6.   str1 = "Nothing"
  7.  Else
  8.   str1 = Ret(0).Value
  9.  End If
  10.  str = "Object=" & str1
  11.  str = str & vbNewLine & "String=" & Ret(1)
  12.  str = str & vbNewLine & "Variant=" & Ret(2)
  13.  str = str & vbNewLine & "Boolean=" & Ret(3)
  14.  str = str & vbNewLine & "Integer=" & Ret(4)
  15.  str = str & vbNewLine & "Long=" & Ret(5)
  16.  str = str & vbNewLine & "Double=" & Ret(6)
  17.  str = str & vbNewLine & "Single=" & Ret(7)
  18.  str = str & vbNewLine & "Currency=" & Ret(8)
  19.  str = str & vbNewLine & "Date=" & Ret(9)
  20.  str = str & vbNewLine & "Byte=" & Ret(10)
  21.  MsgBox str
  22. End Sub
図10


戻り値の内、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です。
  1. '========== ⇩(2) 標準フォームを起動 ============
  2. Sub UForm01()
  3.  Dim Ret As Variant   '←UserFormからの戻り値(配列)
  4.  Ret = UserForm1.UFstart(Range("A2"), Range("B2"), Range("C2"), _
  5.      Range("D2"), Range("E2"), Range("F2"), Range("G2"), _
  6.      Range("H2"), Range("I2"), Range("J2"), Range("K2"))
  7.  Call OutPut(Ret)
  8. End Sub
図11


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のようにしました。
UserForm1のレイアウト
図12


フォームを閉じるためのボタンを2つ配置し、左側(CommandButton1)は「Hide」を実行し、右側(CommandButton2)は「Unload」を実行するものとしました。
なおボタン表面の文字列は、配置時にCaptionプロパティを手動設定しています。

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

3ー2ー2ー1.フォームレベル変数宣言
フォーム内で共通使用する変数の宣言を図13のように宣言部(フォームモジュール先頭部)で行います。
今回対象としている11種のデータ型(図02)の変数をUFtype1~UFtype11という名前にしています。
  1. '========== ⇩(3) フォームレベル変数の宣言 ============
  2. Dim UFtype1 As Object
  3. Dim UFtype2 As String
  4. Dim UFtype3 As Variant
  5. Dim UFtype4 As Boolean
  6. Dim UFtype5 As Integer
  7. Dim UFtype6 As Long
  8. Dim UFtype7 As Double
  9. Dim UFtype8 As Single
  10. Dim UFtype9 As Currency
  11. Dim UFtype10 As Date
  12. Dim UFtype11 As Byte
図13


3ー2ー2ー2.フォーム起動関数
標準モジュール側の図11から呼び出されるのが、フォームモジュール(UserForm1)上のUFstart関数(図14)です。
11個(データ型11種)の引数を受け取ります。
  1. '========== ⇩(4) フォーム起動関数 ============
  2. Public Function UFstart(Type1 As Object, Type2 As String, Type3 As Variant, _
  3.      Type4 As Boolean, Type5 As Integer, Type6 As Long, Type7 As Double, _
  4.      Type8 As Single, Type9 As Currency, Type10 As Date, Type11 As Byte)
  5.  Set UFtype1 = Type1
  6.  UFtype2 = Type2: UFtype3 = Type3
  7.  UFtype4 = Type4: UFtype5 = Type5: UFtype6 = Type6: UFtype7 = Type7
  8.  UFtype8 = Type8: UFtype9 = Type9: UFtype10 = Type10: UFtype11 = Type11
  9.  Me.Show
  10.  UFstart = Array(UFtype1, UFtype2, UFtype3, _
  11.      UFtype4, UFtype5, UFtype6, UFtype7, _
  12.      UFtype8, UFtype9, UFtype10, UFtype11)
  13. End Function
図14


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です。
  1. '========== ⇩(5) フォームをHideで閉じる ============
  2. Private Sub CommandButton1_Click()
  3.  Me.Hide
  4. End Sub
  5. '========== ⇩(6) フォームをUnloadで閉じる ============
  6. Private Sub CommandButton2_Click()
  7.  Unload Me
  8. End Sub
図15


「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です。
  1. '========== ⇩(7) 対策1フォームを起動 ============
  2. Sub UForm02()
  3.  Dim Ret As Variant   '←UserFormからの戻り値(配列)
  4.  Ret = UserForm2.UFstart(Range("A2"), Range("B2"), Range("C2"), _
  5.     Range("D2"), Range("E2"), Range("F2"), Range("G2"), _
  6.     Range("H2"), Range("I2"), Range("J2"), Range("K2"))
  7.  Call OutPut(Ret)
  8. End Sub
図16


コード自体は、「標準」のコード(図11)と全く同じです。
114~116行目の右辺で、フォーム(UserForm2)内の「UFstart関数プロシージャ(図20)」を呼び出しています。呼び出す際に11個の引数を渡します。

フォームが閉じた後に、UFstart関数プロシージャからの戻り値(配列)が左辺の変数Retに格納され、118行目「Call OutPut(Ret)」で、メッセージボックスに戻り値を表示させます。

4ー2.ユーザーフォーム(UserForm2)

対策1と対策2のユーザーフォームは共通としました。

4ー2ー1.フォームレイアウト

対策のユーザーフォームのレイアウトは図17のようにしました。
UserForm2のレイアウト
図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
図18


なお今回は、フォームモジュールのUFstart関数の中でLabelへの書き込みも行っているため、INI時刻とACT時刻の差が数秒出てしまう場合もありそうです(もちろんPCの処理速度で変わります)。フォームをHideで閉じ、少し時間をおいてから再度開くと、時刻の比較がし易いかと思います。

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

4ー2ー2ー1.フォームレベル変数宣言
フォーム内で共通使用する変数の宣言を図19のように宣言部で行います。内容は図13と同じです。
今回対象としている11種のデータ型(図02)の変数をUFtype1~UFtype11という名前にしています。
  1. '========== ⇩(8) フォームレベル変数の宣言 ============
  2. Dim UFtype1 As Object
  3. Dim UFtype2 As String
  4. Dim UFtype3 As Variant
  5. Dim UFtype4 As Boolean
  6. Dim UFtype5 As Integer
  7. Dim UFtype6 As Long
  8. Dim UFtype7 As Double
  9. Dim UFtype8 As Single
  10. Dim UFtype9 As Currency
  11. Dim UFtype10 As Date
  12. Dim UFtype11 As Byte
図19


4ー2ー2ー2.フォーム起動関数
標準モジュールの図16図23から呼び出されるのが、フォームモジュール(UserForm2)上のUFstart関数(図20)です。
11個(データ型11種)の引数を受け取ります。
  1. '========== ⇩(9) フォーム起動関数 ============
  2. Public Function UFstart(Type1 As Object, Type2 As String, Type3 As Variant, _
  3.      Type4 As Boolean, Type5 As Integer, Type6 As Long, Type7 As Double, _
  4.      Type8 As Single, Type9 As Currency, Type10 As Date, Type11 As Byte)
  5. ' Set UFtype1 = Type1
  6. ' UFtype2 = Type2: UFtype3 = Type3
  7.  UFtype4 = Type4: UFtype5 = Type5: UFtype6 = Type6: UFtype7 = Type7
  8.  UFtype8 = Type8: UFtype9 = Type9: UFtype10 = Type10: UFtype11 = Type11
  9.  Me.Label1.Caption = Type1.Address
  10.  Me.Label2.Caption = Type2
  11.  Me.Label3.Caption = Type3
  12.  Me.Show
  13.  Set UFtype1 = Range(Me.Label1.Caption)
  14.  UFtype2 = CStr(Me.Label2.Caption)
  15.  UFtype3 = CVar(Me.Label3.Caption)
  16.  UFstart = Array(UFtype1, UFtype2, UFtype3, _
  17.      UFtype4, UFtype5, UFtype6, UFtype7, _
  18.      UFtype8, UFtype9, UFtype10, UFtype11)
  19. End Function
図20


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です。
  1. '========== ⇩(10) フォームをHideで閉じる ============
  2. Private Sub CommandButton1_Click()
  3.  Me.Hide
  4. End Sub
  5. '========== ⇩(11) フォームをUnloadで閉じる ============
  6. Private Sub CommandButton2_Click()
  7.  Unload Me
  8. End Sub
図21


フォームを閉じるコードは、標準の図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です。
  1. '========== ⇩(12) Initializeイベント時刻記入 ============
  2. Private Sub UserForm_Initialize()
  3.  Me.Label4.Caption = "INI=" & Time()
  4. End Sub
  5. '========== ⇩(13) Activateイベント時刻記入 ============
  6. Private Sub UserForm_Activate()
  7.  Me.Label5.Caption = "ACT=" & Time()
  8. End Sub
図22


初めてフォームを起動したり、前回フォームを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です。
  1. '========== ⇩(14) 対策フォームを起動し、最後にUnload ============
  2. Sub UForm03()
  3.  Dim Ret As Variant   '←UserFormからの戻り値(配列)
  4.  Dim i As Long   '←メモリー上に存在するユーザーフォームの数
  5.  Ret = UserForm2.UFstart(Range("A2"), Range("B2"), Range("C2"), _
  6.     Range("D2"), Range("E2"), Range("F2"), Range("G2"), _
  7.     Range("H2"), Range("I2"), Range("J2"), Range("K2"))
  8.  For i = UserForms.Count - 1 To 0 Step -1
  9.   If LCase(UserForms(i).Name) = LCase("UserForm2") Then Unload UserForm2
  10.  Next i
  11.  Call OutPut(Ret)
  12. End Sub
図23


「標準(図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です。
  •  If UserForms.Count > 0 Then Unload UserForm2
図24


これは、メモリ上に存在するフォームの数が1つ以上存在する場合には、UserForm2をUnloadするものです。もしUserForm2が「Hideで閉じられていれば、メモリー上に存在しているので個数に反映される」という現象を利用しています。
しかし目的のフォームがUnloadされていても、別なフォームが開かれている場合には「Unload UserForm2」が実行されてしまい、上記と同様に「一旦UserForm2をLoad」してしまいます。
ちなみに、Unloadしたフォームを更にUnloadするのは、Initializeイベントが実行される以外は特に問題はありません。

また図23に近い形で、図25のように「For Each~Next でUserFormを1つずつ取得」する方法も考えられます。これでしたら、正順・逆順を考える必要はありません。また図25では、取り出したUserFormは「Is演算子」を使ってオブジェクト同士の比較をしています。
  •  Dim uf As UserForm
  •  
  •  For Each uf In UserForms
  •   If uf Is UserForm2 Then Unload UserForm2
  • '  If LCase(uf.Name) = LCase("UserForm2") Then Unload UserForm2
  •  Next uf
図25


本当は図23と同じように、For Eachで1つずつ取り出したものを、見え消しで示した「If LCase(uf.Name) = LCase("UserForm2") Then Unload UserForm2」で、目的のフォームを見つけたらUnload としたかったのですが、なぜか「For Eachで取り出したUserForm(ここでは変数uf)には、Nameプロパティが無い」ようなのです。そこで仕方なく「オブジェクト比較」としました。
しかしそうなると、オブジェクト比較の際に「UserForm2のオブジェクトを確認」する事になるのでの、「一旦UserForm2をLoad」することになります。
またTypeOf演算子を先頭に付け「If TypeOf uf Is UserForm2 Then Unload UserForm2」としても同じ結果が得られるのですが、今回はオブジェクトの型を調べているのではなく、本人か否かを直接調べているようなものなので、「TypeOfは不要」と判断しました。

また、Visibleプロパティを使用した図26のような方法も良く見かけます。
  •  If UserForm2.Visible = True Then Unload UserForm2
図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月より切替開始)
解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。