今回作成するのはボタンを動的に切り替えられるメニューである。
これだけでは意味が分からないと思うので動作サンプルを紹介する。
通常は1つのボタンに1つの処理なので、5つボタンがあれば5つしか処理は書けないが、このメニューは▲と▼で動的にボタンを切り替えることができる。
作り方の紹介
必要なものは、
- クラスモジュール「SelectButton」
- クラスモジュール「PagedButtons」
- フォームモジュール「(任意のオブジェクト名)」
SelectButtonの作り方
クラスモジュールを挿入し、オブジェクト名を「SelectButton」に変更する。
それから以下のコードを張り付け。
OptionExplicitPublicWithEvents btn As MSForms.CommandButton Public Parent As PagedButtons PrivateSub btn_Click() Parent.callBack btn.Caption EndSubPublicPropertyLet Enabled(e AsBoolean) btn.Enabled = e EndPropertyPublicPropertyLet Caption(x AsString) btn.Caption = x EndPropertyPublicPropertyGet Self()AsObjectSet Self =MeEndPropertyPublicSub ReleaseObject()Set btn =NothingSet Parent =NothingEndSub
PagedButtonsの作り方
クラスモジュールを挿入し、オブジェクト名を「PagedButtons」に変更する。
※複数形のsを見落とさずに。
それから以下のコードを張り付け。
OptionExplicitPrivateWithEvents previousButton As MSForms.CommandButton PrivateWithEvents nextButton As MSForms.CommandButton Private pageNumber AsLongPrivate selectButtons As Collection Private menuItems As Collection PublicEvent Selected(x AsString)Sub callBack(x AsString)RaiseEvent Selected(x)EndSubSub Init(previous_button As MSForms.CommandButton, _ next_button As MSForms.CommandButton, _ParamArray select_buttons())Set previousButton = previous_button Set nextButton = next_button Set selectButtons =New Collection Dim b ForEach b In select_buttons WithNew SelectButton Set.Parent =MeSet.btn = b selectButtons.Add.Self EndWithNextSet menuItems =New Collection pageNumber =1EndSubSub addMenuItem(menu_caption AsString) menuItems.Add menu_caption EndSubSub DrawCaptions() previousButton.Enabled = pageNumber <>1 nextButton.Enabled = pageNumber < maxPage Dim itemCursor: itemCursor = selectButtons.Count* pageNumber - selectButtons.CountDim i AsLongFor i =1To selectButtons.CountIf itemCursor + i <= menuItems.CountThen selectButtons(i).Enabled =True selectButtons(i).Caption = menuItems(itemCursor + i)Else selectButtons(i).Enabled =False selectButtons(i).Caption ="-"EndIfNextEndSubPrivatePropertyGet maxPage()AsLong maxPage = roundUp(menuItems.Count/ selectButtons.Count)EndPropertyPrivateFunction roundUp(x AsDouble)AsLong roundUp =Int(x +0.999)EndFunctionPrivateSub nextButton_Click() pageNumber = pageNumber +1 DrawCaptions EndSubPrivateSub nextButton_DblClick(ByValCancelAs MSForms.ReturnBoolean)Call nextButton_Click If pageNumber >= maxPage -1ThenCancel=TrueEndIfEndSubPrivateSub previousButton_Click() pageNumber = pageNumber -1 DrawCaptions EndSubPrivateSub previousButton_DblClick(ByValCancelAs MSForms.ReturnBoolean)Call previousButton_Click If pageNumber <=2ThenCancel=TrueEndIfEndSubPublicSub ReleaseObject()Dim b As SelectButton ForEach b In selectButtons b.ReleaseObject NextSet menuItems =NothingSet selectButtons =NothingEndSub
ユーザーフォームの作り方
ユーザーフォームを挿入し、オブジェクト名を以下のように変更する。
btn1~btn5はCaptionと同じくオブジェクト名もbtn1~btn5にしておく。
そしてフォームのコードに以下を張り付ける。
PrivateWithEvents menu As PagedButtons PrivateSub menu_Selected(x AsString)Me.Label1.Caption = x &"が選択されました。"EndSubPrivateSub UserForm_Initialize()Me.Label1 =vbNullStringSet menu =New PagedButtons menu.Init Me.btnPrevious,Me.btnNext, _Me.btn1,Me.btn2,Me.btn3,Me.btn4,Me.btn5 Dim i AsLongFor i =Asc("A")ToAsc("Z") menu.addMenuItem "項目"&Chr(i)Next menu.DrawCaptions EndSubPrivateSub UserForm_Terminate() menu.ReleaseObject UnloadMeEndSub
これで完成。
このテクニックのポイント
このテクニックのポイントは、メニューボタンが押された際に発生するイベントがmenu_Selectedに集約される点だ。
PrivateSub menu_Selected(x AsString)Me.Label1.Caption = x &"が選択されました。"EndSub
それぞれのボタンがバラバラに機能するのではなく、あたかもひとつのPagedButtonsというコントロールパーツであるかのように扱うことができる。
↓つまりこういう形のひとつのコントロールパーツとして扱うことができるということ。
また、ボタン数の増減がきわめて簡単に行えることもポイントのひとつ。
試しにボタンをひとつ増やしてみた。
コードの変更箇所はたった1箇所。
ユーザーフォームのUserForm_Initializeメソッドのmenu.Initに引き渡すボタンを一つ増やすだけで済む。
ボタンを減らした場合も同様に、menu.Initに引き渡すボタンを減らすだけ。
今回は紹介しないが、動的なコントロールの生成と、APIによるフォームのサイズ変更を組み合わせると、フォームサイズの変化に合わせて表示ボタン数が変わる柔軟なメニューを作成することもできる。
仕組みの解説
さて、どういうことなのか説明しよう。
今回はクラスモジュール、コントロールイベントの共通化、自作イベントなどのテクニックを利用している。
まずPagedButtonsオブジェクトの初期状態はこんな感じの構成。
PagedButtonsオブジェクトにボタンがひとつ渡されると、SelectButtonオブジェクトを生成し、そこにボタンを保持させて自身が持つSelectButtonsCollectionに格納する。
また、このときに自身(PagedButtonsオブジェクト)をSelectButtonオブジェクトに保持させる。
ここで循環参照が発生してしまうが、イベントのコールバック処理で必要になるので仕方がない。
オブジェクトにReleaseObjectプロシージャを作ってあるのはそのためだ。
※SelectButtonオブジェクトからPagedButtonsオブジェクトへの参照をオレンジ線にしたのは、後の図で青だと見づらくなった為で、特別な意味はない。
PagedButtonsオブジェクトにボタンやメニュー項目を引き渡していくと、最終的なオブジェクトの関係図はこうなる。
※実際にはボタンはInitプロシージャで一気に引き渡されますが、最初の図は1つにしておかないとややこしかったので説明の都合上、引き渡していくという表現にしています。
ページ切り替えのボタンまで図に含めると複雑すぎるので割愛したが、ページ切り替えを行うとmenuItemsコレクションから項目が取得され、それぞれのSelectButtonオブジェクトに格納される。
ユーザーがボタンをクリックした際のプロシージャ呼び出しをシーケンス図で書くとこんな感じ。
callbackとSelectedでそれぞれボタンのCaptionが引き渡されるので、ユーザーフォーム側でどのボタンがクリックされたのか検知できる。
利用しているテクニックについての参考記事
thom.hateblo.jp
thom.hateblo.jp
thom.hateblo.jp
thom.hateblo.jp
循環参照についての参考記事
今後の展望
動的なコントロールの生成を組み合わせると柔軟性が高まる。以下の記事で動的にラベルを生成させているので紹介。
thom.hateblo.jp
たとえば上記の記事ではSet L = Me.Controls.Add("Forms.Label.1")としているが、Set btn = Me.Controls.Add("Forms.CommandButton.1")とすれば、新しいボタンが生成されて変数btnに格納される。
あと今回はPagedButtonsのSelectedイベントでキャプションを返しているが、addMenuItemメソッドをSub addMenuItem(menu_caption As String, data As Variant)に改造して押された項目に対応するdataを返すようにすれば更に柔軟性が高まる。たとえば押したボタンに応じたオブジェクトが返ってくると、そこから色々操作できて面白い。
ただし、今後の展望に書いた案については、きっとこの記事に興味がある皆さんが素晴らしい実装を作ってくれるので私はこれ以上作らない。面倒だし。。