Quantcast
Channel: t-hom’s diary
Viewing all articles
Browse latest Browse all 493

VBA クラスモジュールを使ってフォームに動的なメニューを作る

$
0
0

今回作成するのはボタンを動的に切り替えられるメニューである。

これだけでは意味が分からないと思うので動作サンプルを紹介する。
f:id:t-hom:20171209061338g:plain

通常は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

ユーザーフォームの作り方

ユーザーフォームを挿入し、オブジェクト名を以下のように変更する。
f:id:t-hom:20171209062344p:plain

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というコントロールパーツであるかのように扱うことができる。

↓つまりこういう形のひとつのコントロールパーツとして扱うことができるということ。
f:id:t-hom:20171209063932p:plain

また、ボタン数の増減がきわめて簡単に行えることもポイントのひとつ。
試しにボタンをひとつ増やしてみた。
f:id:t-hom:20171209064527p:plain

コードの変更箇所はたった1箇所。
ユーザーフォームのUserForm_Initializeメソッドのmenu.Initに引き渡すボタンを一つ増やすだけで済む。
f:id:t-hom:20171209065211p:plain

ボタンを減らした場合も同様に、menu.Initに引き渡すボタンを減らすだけ。

今回は紹介しないが、動的なコントロールの生成と、APIによるフォームのサイズ変更を組み合わせると、フォームサイズの変化に合わせて表示ボタン数が変わる柔軟なメニューを作成することもできる。

仕組みの解説

さて、どういうことなのか説明しよう。
今回はクラスモジュール、コントロールイベントの共通化、自作イベントなどのテクニックを利用している。

まずPagedButtonsオブジェクトの初期状態はこんな感じの構成。
f:id:t-hom:20171209072843p:plain

PagedButtonsオブジェクトにボタンがひとつ渡されると、SelectButtonオブジェクトを生成し、そこにボタンを保持させて自身が持つSelectButtonsCollectionに格納する。
また、このときに自身(PagedButtonsオブジェクト)をSelectButtonオブジェクトに保持させる。
f:id:t-hom:20171209073114p:plain

ここで循環参照が発生してしまうが、イベントのコールバック処理で必要になるので仕方がない。
オブジェクトにReleaseObjectプロシージャを作ってあるのはそのためだ。
※SelectButtonオブジェクトからPagedButtonsオブジェクトへの参照をオレンジ線にしたのは、後の図で青だと見づらくなった為で、特別な意味はない。

PagedButtonsオブジェクトにボタンやメニュー項目を引き渡していくと、最終的なオブジェクトの関係図はこうなる。
f:id:t-hom:20171209073531p:plain

※実際にはボタンはInitプロシージャで一気に引き渡されますが、最初の図は1つにしておかないとややこしかったので説明の都合上、引き渡していくという表現にしています。

ページ切り替えのボタンまで図に含めると複雑すぎるので割愛したが、ページ切り替えを行うとmenuItemsコレクションから項目が取得され、それぞれのSelectButtonオブジェクトに格納される。

ユーザーがボタンをクリックした際のプロシージャ呼び出しをシーケンス図で書くとこんな感じ。
f:id:t-hom:20171209075244p:plain

callbackとSelectedでそれぞれボタンのCaptionが引き渡されるので、ユーザーフォーム側でどのボタンがクリックされたのか検知できる。

利用しているテクニックについての参考記事

thom.hateblo.jp
thom.hateblo.jp
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を返すようにすれば更に柔軟性が高まる。たとえば押したボタンに応じたオブジェクトが返ってくると、そこから色々操作できて面白い。

ただし、今後の展望に書いた案については、きっとこの記事に興味がある皆さんが素晴らしい実装を作ってくれるので私はこれ以上作らない。面倒だし。。


Viewing all articles
Browse latest Browse all 493

Trending Articles