- ベストアンサー
VBAのExitイベントについて
VBA(Excel)についてご質問します。 フォームにて、IDというオブジェクト名のテキストボックス、 Private Sub ID_Exit(ByVal Cancel As MSForms.ReturnBoolean) という関数(Exitイベント)を用意しました。 IDにフォーカスがある状態で、フォーカスを移動しようとすると、この関数が必ず実行されるかと思います。 そのとき、特定のボタン(例えばキャンセルボタン)がクリックされたためにフォーカスが移動したときは、この関数の処理を実行したくない。もしくはこの関数内で特定のボタンがクリックされたこと(そのためにフォーカスが移動したこと)を検出して、処理をスキップしたいです。 どうすれば実現できますでしょうか? よろしくお願いします。
- みんなの回答 (10)
- 専門家の回答
質問者が選んだベストアンサー
ANo.1の例です。 ユーザーフォームを作成し、 CommandButton1-4を配置します。 Label1を配置します。 フォームのコードとして下記のコードを作成します。 *下記コードの解説 -各コントロールの移動のイベント(Exit,Enter)では、直接やりたい処理を記述せずに、自分のコントロール名を変数ExitControl, CurrentControl にセットして全体の調整役のSubプロシージャ(ここでは"Mediate"プロシージャ)を呼び出す。 -Mediate内で、移動元のコントロール、移動先のコントロールに応じて実際の処理を行う(例では移動先が"4"の時は処理をスキップし、それ以外にフォーカスが移ったときは一律に処理している) *使用上の注意 -実際の使用に当たっては、数字とかでなく、ちゃんとコントロール名をつけないと分けわかんなくなる。 -複雑な遷移の処理は、Mediate内に集約されるので管理の煩雑さは緩和される。 -各コントロールでMediateを呼び出す処理を繰り返す部分はめんどくさいし、間違いやすいので、コントロールの数が多いときは、出来ればExcelの数式やVBAで自動的に作成する。 '-以下ユーザーフォーム内に記述するコード--------- Option Explicit Private ExitControl As String Private CurrentControl As String 'Mediate 全体の調整役のプロシージャ 'コントロールのフォーカスの移動があるたびに呼び出す。 Private Sub Mediate() Dim EventText As String EventText = "Exit:" & ExitControl & "," & "Current:" & CurrentControl Debug.Print EventText If CurrentControl = "4" Then EventText = EventText & "Skip" Else EventText = EventText & "DoSomething" End If Label1.Caption = Label1.Caption & vbCrLf & EventText End Sub Private Sub CommandButton1_Enter() CurrentControl = "1" Mediate End Sub Private Sub CommandButton1_Exit(ByVal Cancel As MSForms.ReturnBoolean) ExitControl = "1" CurrentControl = "" Mediate End Sub Private Sub CommandButton2_Enter() CurrentControl = "2" Mediate End Sub Private Sub CommandButton2_Exit(ByVal Cancel As MSForms.ReturnBoolean) ExitControl = "2" CurrentControl = "" Mediate End Sub Private Sub CommandButton3_Enter() CurrentControl = "3" Mediate End Sub Private Sub CommandButton3_Exit(ByVal Cancel As MSForms.ReturnBoolean) ExitControl = "3" CurrentControl = "" Mediate End Sub Private Sub CommandButton4_Enter() CurrentControl = "4" Mediate End Sub Private Sub CommandButton4_Exit(ByVal Cancel As MSForms.ReturnBoolean) ExitControl = "4" CurrentControl = "" Mediate End Sub Private Sub CommandButton4_Click() MsgBox "Cancel Clicked" End Sub
その他の回答 (9)
- Oh-Orange
- ベストアンサー率63% (854/1345)
★追記。回答者 Oh-Orange です。 ・Enter イベントで呼ばれる Sub プロシージャについて補足します。 ・このプロシージャ内の最後で渡された引数の『識別コード』をグローバルで用意した変数に セットして下さい。そして、プロシージャの先頭で『グローバル変数』をチェックして前に フォーカスがあったコントロールが『本来の Exit イベント』で行うテキストボックスかを チェックします。もし、一歩手前のフォーカスがそのテキストボックスのときだけ、本来の 『Exitイベント』で行おうとした処理を実行して下さい。それ以外のコントロールのときは 処理をしないように記述して下さい。 ・上記の処理を Enter イベントで呼ばれる Sub プロシージャに先頭と最後にそれぞれ追加して 下さい。 ●まとめ ・呼ばれる Sub プロシージャの先頭は、グローバル変数から前フォーカスのコントロールをチェック。 ・呼ばれる Sub プロシージャの最後は、グローバル変数に渡された『識別コード』をセット。 ・上記の処理を追加しないと ID というオブジェクト名のテキストボックス以外の場所にフォーカス があったコントロールでも Sub プロシージャで処理が実行されてしまいます。 ・前フォーカスと現在フォーカスの発生順序をもう一度考えて下さい。 ・以上。おわり。→参考に!
お礼
追記していただき、ありがとうございました。 わかりやすくまとめていただいたおかげで、イメージがわきました。 それでは、これで回答を締め切らせていただきます。 重ねて、ありがとうございました。 <皆様へ> 今回は多くの皆様にご回答をいただき、本当にありがとうございます。 2人の方にしかポイントをお付けできないのが大変心苦しいです。 さらに技術を身につけ、皆様のように回答できる立場なりたいと思っています。 それでは、今後ともよろしくお願いいたします。
- onlyrom
- ベストアンサー率59% (228/384)
向学心旺盛な質問者なので再度の回答をしたいと思います。 KeyDownイベントですが、 CommandButtonのではなくてTextBoxのKeyDownイベントです。 CommandButtonを1、Textboxを2つ配置して以下をお試しください。 '----------------------------------------------------------- Private Sub UserForm_Initialize() Dim myCtrl As Control For Each myCtrl In Me.Controls myCtrl.TabStop = False Next myCtrl TextBox1.SetFocus End Sub '------------------------------------------------------------ Private Sub CommandButton1_Click() MsgBox "キャンセルしました" End Sub '----------------------------------------------------------- Private Sub TextBox1_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer) If KeyCode <> vbKeyReturn Then Exit Sub If TextBox1.Text = "" Then MsgBox "Text1は未入力です" TextBox1.SetFocus Exit Sub End If TextBox2.SetFocus End Sub '----------------------------------------------------------- Private Sub TextBox2_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer) If KeyCode <> vbKeyReturn Then Exit Sub If TextBox2.Text = "" Then MsgBox "Text2は未入力です" TextBox2.SetFocus Exit Sub End If CommandButton1.SetFocus End Sub --------------------------------------------------------- ●上記コードの要のところは、UserformInitializeの部分です。 そこが無ければうまく動作しません。 またまた老婆心ながらの一言。 イベントの発生順番、ご自分で確認されましたか。 であれば今回の件はTextBoxのExitイベントを使う限り無理だということがお分かりになったと思います。 Enterイベントについての評はしませんがそれは試しているうちに気づくものがあるでしょう。 何れにしろUserFormにおいてはイベントの発生順番が全てです。 色んな場合を考えそのときどういった順番で発生するのかをを理解した上でコーディングすることが肝要だと思います。
お礼
onlyromさん、本当にありがとうございます。 ご回答として掲載していただいたコードを拝見しました。 そして、実際に試してみました。 UserForm_Initialize()がないといけない理由、とても良くわかります。 実はこのご回答をいただく前に、先ほどまでのご回答を元に、自分のコードを改良していました。 その際にわかったのが、Enterイベントを利用だけだと、Tabキーで移動されたときに困るということでした。 簡単な説明のコードを掲載します(この質問を参考にされている方はぜひどうぞ)。 フォームに、テキストボックスを3つ用意してください。 以下のようなコードを記述してください。 -------------------------------------------------------- Option Explicit Private Sub TextBox3_Enter() Call Check End Sub Private Sub TextBox2_Enter() Call Check End Sub Private Sub TextBox1_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer) If ((KeyCode = 9) Or (KeyCode = 13)) Then If Check = False Then KeyCode = 0 End If End If End Sub Private Function Check() As Boolean If TextBox1.Value = "" Then TextBox1.SetFocus MsgBox "戻るよ" Check = False Else Check = True End If End Function -------------------------------------------------------- エンターキー、Tabキーでの移動もできます。 もし、よりいい案、このコードの問題点がありましたら、ぜひご回答ください。 (本当は問題となるコードも書いたのですが、字数の問題で掲載できませんでした。) 字数の問題で、すべてにお答えできず申し訳ありません。それでは、どうもありがとうございました。
- robbie21
- ベストアンサー率55% (5/9)
コード生成のためのようなコードというと難しそうに感じますが 今回のようなものなら、一つ見本を作って、Print文で次々つないでやるだけで出来ます。 UserForm1上にコントロールを配置して以下のコードを実行すると、イミディエイトウィンドウにプログラムを書き出すので、コピーして貼り付けでOKです。 Sub GenerateEnterExitEvents() Dim c As Control For Each c In UserForm1.Controls Debug.Print Debug.Print "Private Sub " & c.Name & "_Enter()" Debug.Print vbTab & "CurrentControl = """ & c.Name & """" Debug.Print vbTab & "Mediate" Debug.Print "End Sub" Debug.Print "Private Sub " & c.Name & "_Exit(ByVal Cancel As MSForms.ReturnBoolean)" Debug.Print vbTab & "ExitControl = = """ & c.Name & """" Debug.Print vbTab & "CurrentControl = """"" Debug.Print vbTab & "Mediate" Debug.Print "End Sub" Debug.Print Next End Sub ちなみにOh-Orangeさんの言うとおり、この例の場合についてはExitイベントでのMediate呼び出しは不要ですので省いてかまいません。
お礼
ご回答、ありがとうございました。 まず、Debug.Printを知らなかったので、それにびっくりしました。 そして、それをコードの生成に使ってしまおうというのにもびっくりしました。 おもしろいですねぇ。 実際に出力されることも確認しました。 今後はDebug.Printを活用していこうと思います。 なお、皆さんのご回答のおかげで、無事自分のプログラムに組み込めました。 No.9の回答のお礼に参考程度にのせてありますので、もしよろしければどうぞ。
- Oh-Orange
- ベストアンサー率63% (854/1345)
★VBAでは『タイマー・イベント』が使えないのですか。→お勉強になります。 ・それならば、すべてのコントロールに、フォーカスが移った直前 Sub プロシージャを 呼び出すようにします。 ・コントロールがフォーカスを失うイベントが『Exit』ですので、フォーカスが移った 瞬間の『Enter』関数に決まった Sub プロシージャを呼びます。 このとき、引数でコントロールの ID など識別できるコードを渡します。 ・そして Sub プロシージャ側では、受け取った引数を元に『キャンセル』ボタンのときだけ 処理をスキップして、それ以外ならば本来の『Exitイベント』で行おうとした処理を記述 します。 ・これは回答者 No.5 さんのサンプルとほぼ同じ考えですが、フォーカスが移った瞬間の 『Enter』関数のみ記述すればいいです。これは、すべてのコントロールでフォーカスを 失った後で処理するのと、すべてのコントロールのフォーカスが移った瞬間に処理しても 同じになるからです。 ・サンプル・コードはご紹介できませんので、アルゴリズムでのアドバイスになります。 ・回答者 No.5 さんのサンプルと合わせてお読み下さい。 ・以上。おわり。
お礼
度々ありがとうございます。 実は、ご回答いただいた考え方と良く似たコードを書いてみました。 No.5さんへのお礼に書いてあります。 投稿が前後してしまったかもしれませんね。申し訳ありません。 ただ、自分の書いたコードに自信がつきました。 ありがとうございます。 ご回答にありますように、コントロールの ID など識別できるコードは引数で渡すほうが良いかも知れませんね。 今はグローバル変数を使っているので。 検討してみます。 本当にありがとうございました。 もう少ししましたら、ご回答を締め切りたいと思います。
- robbie21
- ベストアンサー率55% (5/9)
ANO.5の補足です。 例のコードでは処理の様子をLabel1に出力しているので、Label1は縦横に十分な大きさのラベルを作ってください。
- onlyrom
- ベストアンサー率59% (228/384)
質問の件は、Exitイベントを使う限りは不可能です。 フラグ云々でもできません。 イベントの発生順番を確認すればそれが分かるはずです。 KeyDownイベントを使うことにより解決できますのでお試しください。 老婆心ながら一言。 No1さんのお礼にあるような >非常に煩雑なコードになりそうです。 >フラグをチェックせねばならず、あんまりきれいではない とかは完璧に動作するコードを書いた後に考えればいいことです。
お礼
ご回答ありがとうございます。 KeyDownイベントについて調べ、試してみました。 テキストボックス1個と、ボタン1個をフォームに配置し、 ------------------------------------------------------- Private Sub CommandButton1_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer) MsgBox KeyCode End Sub Private Sub CommandButton1_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single) MsgBox Button End Sub Private Sub TextBox1_Exit(ByVal Cancel As MSForms.ReturnBoolean) If TextBox1.Value = "" Then MsgBox "入力してください。" Cancel = True Exit Sub End If End Sub ------------------------------------------------------- というコードを書きました。 TextBox1にフォーカスがある状態で、CommandButton1を クリック、もしくはフォーカスを移動しエンターキー を押すことでメッセージが表示されればOKというものです。 ですが、これですとやはりKeyDownイベント、MouseDownイベント を実行する前に、TextBox1_Exitイベントが実行され、 メッセージを表示することができません。 いまさらですが、質問にもっと具体的にコードをのせるべきでした。 大変申し訳ありません。 ですが、これを機会にKeyDownイベントの使い方がわかり、 とてもためになりました。 最後に、いただいたアドバイスにつきまして。 そうですね、どんなにひどいコードでも動かないよりは 動いたほうがずっといいわけですから、まずは チャレンジすべきでした。 その中で極力きれいなコードを書けばいいのかもしれません。 ご指導ありがとうございました。
- Oh-Orange
- ベストアンサー率63% (854/1345)
★どうも『Exitイベント』の処理だけでは実現できそうにないですね。 ・『キャンセル』ボタンなどの時だけ『Exitイベント』の実行を阻止したいのならば、 『Exitイベント』内でタイマーをセット(1ms)します。そして 1ms 後に実行される タイマー・ルーチンで本来『Exitイベント』で行う処理を記述します。なお、この処理 の最後でタイマー・イベントを破棄するようにして下さい。 ・それで『キャンセル』ボタンなどの処理の最初の段階で、『Exitイベント』内でセット したタイマーをキャンセル(破棄)します。これで、1ms 後のタイマー・ルーチンで本来 『Exitイベント』で行う処理をスキップ(阻止)出来ると思います。 ・『キャンセル』ボタン以外では、タイマーをキャンセルしないため 1ms 後にタイマー 処理により実行されます。→特に何も記述しなければ実行される。 ●まとめ ・この方法ならば、『キャンセル』ボタン以外では 1ms 後にタイマー・ルーチンで処理 されますし、特定の『キャンセル』ボタンなどでタイマーをキャンセルすることで阻止 (スキップ)出来ると思います。 最後に: ・タイマー・イベントの記述方法は分かりますか? 私は C/C++ 言語専門で、最近 JavaScript、VBScript を趣味でお勉強中です。 VBA はちょっと専門外ですが、アルゴリズムや実現方法のヒントくらいはアドバイスで きます。以上。おわり。
お礼
ご回答ありがとうございました。 タイマー・イベントを使うという発想は思いつきませんでした! タイマー・イベントを知っていたとしても考えなかったと思います。 皆さんの回答はとてもためになります。 そこで、VBAでもタイマー・イベントがあるかを調べてみました。 そうすると、残念ながらVBAではないようです。 VBならありそうなのですが・・・。 (もしVBAでもご存知の方がいらっしゃいましたら、ご指摘ください!) C/C++ 言語専門というところは尊敬です。 私も昔はC++をかじったのですが、難しくて・・・。 メモリの管理とか、可変配列が使えないとか・・・。 いずれ再挑戦したいと思っていますが。 それでは、よろしくお願いします。
- Oh-Orange
- ベストアンサー率63% (854/1345)
★マウスのクリックを調査すれば? ・フォーカスが別の場所に移るときに ID_Exit() 関数が必ず実行されるのならば、実行した最初の段階で マウスの左ボタン(右ボタン)を調査して押し下げられていたら処理をスキップします。 ・TAB キーなどでフォーカスが移動するときに ID_Exit() 関数を実行したい場合は、TAB キーを押されたら フラグを ON として ID_Exit() 関数の最初にチェックして処理をスキップという方法もあります。この場合 ID_Exit() 関数を抜けるときにフラグを OFF にしておきましょう。 ・実現方法は多数あります。もう少し ID_Exit() 関数の動作について知らないと上記ぐらいのアドバイスしか 出来ません。→キーボードでのフォーカス移動のときだけ ID_Exit() 関数を実行させたいのですか? ・以上。ちょっとだけ補足要求します。
補足
ご回答ありがとうございます。 >ID_Exit() 関数の動作について とのことですが、私がその関数の中で実現したい内容についてでしょうか? それともVBAがどう扱っているかでしょうか? ご質問に質問で返してしまい、申し訳ありません。 見当違いかもしれないのですが、まずは前者について。 質問で例に挙げましたキャンセルボタンがクリック (もしくはそれにわる動作)されたときは、 ID_Exit()関数の処理をキャンセルしたいと考えています。 なので、関数内の処理は関係ないかと思ったのですが、 必要でしたらおっしゃってください。 次に後者ですが、VBAのヘルプには「Exit イベントは、 同一フォーム上にある別のコントロールにフォーカスを 移す直前に発生します。」とあります。 これでよろしいでしょうか? キャンセルボタン(フォームに配置されたボタン)が クリックされたことを調査する方法を調べてみたのですが、 なかなかありませんでした。 もしこのような補足で何かご教授いただけることが ありましたら、よろしくお願いいたします。
- robbie21
- ベストアンサー率55% (5/9)
Exitイベント内で、次にどこにフォーカスが移るかということを知ることは出来ないと思います。 処理の内容によってはExitイベント内ではフォームの変数にフラグを立てるなどだけしておいて、違うタイミングで実際の処理を実行するなどの方策は立てられる可能性はあります。
お礼
ご回答ありがとうございます。そうですか・・・。 処理の内容が、テキストボックスからフォーカスが 移動したことを検知した上で行いたいことで、 それをフラグを立てて処理しようとすると、 非常に煩雑なコードになりそうです。 移動先になる可能性のあるフォーム内のオブジェクト毎に フラグをチェックせねばならず、あんまりきれいではないですね・・・。 ただ、アイディアとしてはぜひ参考にさせていただきます。 ありがとうございました。
お礼
ご回答ありがとうございました。 実際のコードまでつけていただき、非常に理解しやすかったです。 このコードを見ながら、以下のようなコードを書きました。 ユーザーフォームを作成し、 CommandButton1を配置します。 TextBox1-2を配置します。 Option Explicit Private CurrentControl As String Private Sub UserForm_Initialize() TextBox1.SetFocus End Sub 'Mediate 全体の調整役のプロシージャ 'コントロールのフォーカスの移動があるたびに呼び出す。 Private Sub Mediate() 'TextBox1が空の時はCommandButton1以外無効 If ((TextBox1.Value = "") And (CurrentControl <> "CommandButton1")) Then MsgBox "入力してね" TextBox1.SetFocus End If End Sub Private Sub CommandButton1_Enter() CurrentControl = "CommandButton1" Mediate MsgBox "キャンセル成功!" End Sub Private Sub TextBox2_Enter() 'TextBox1が空でなければ、ここに書き込むことが可能 CurrentControl = "TextBox2" Mediate End Sub CommandButton1をキャンセルボタンと想定しています。 これで実際のコードにも組み込めそうな気がしてきました。 >-各コントロールでMediateを呼び出す処理を繰り返す部分はめんどくさいし、間違いやすいので、コントロールの数が多いときは、出来ればExcelの数式やVBAで自動的に作成する。 これについて、おっしゃるとおりなのですが、実現方法がわかりませんでした・・・。 >-複雑な遷移の処理は、Mediate内に集約されるので管理の煩雑さは緩和される。 につきましては、まさにそうだと思います。 ありがとうございました。本当に助かりました。 もう少ししてから回答を締め切らせていただきますね。