[[20150316182531]] 『クラスモジュールの勉強 イベント編』(稲葉) ページの最後に飛ぶ

[ 初めての方へ | 一覧(最新更新順) | 全文検索 | 過去ログ ]

 

『クラスモジュールの勉強 イベント編』(稲葉)

[[20140926184519]]

 こちらで大変お世話になりました。
 時間が作れましたので、前回投稿から少しブラッシュアップしたPopupと、
 自分がクラスモジュールに興味を持った原点から考え直してみました。
[[20131030060619]]

 下記の通り作ってて、わからないことができたので教えてください。
 【質問】
 1)★の変数の型宣言で、Office.CommandBar型では、コレクションの中身をSetできず
   Object型にしておかないと入らない理由はなんですか?

 2)クラスに独自イベントを実装できることを、実際に作りながら勉強していたのですが、
   何かのイベントをクラスに受け取らせて、配列として返す以外の使い方がわかりません。
   どういったときに、独自のイベント単独で便利になるのでしょうか?

 3)今回はシートモジュールですが、標準モジュールでイベントを受け取りたい場合、
   どのような手段があるでしょうか?(2)に関連して)
   標準モジュールから呼び出すのであれば、イベントからではなく、直接呼び出せば
   良いのだとは思いますが・・・

 4)コールバックの言葉の定義が具体的にどういうものなのか理解できていません。
   C言語での説明はあるのですが、(理解できていませんが)
   VBAではどのような実装がコールバックに当たるのでしょうか?

 コード
    '◆クラスモジュール「clsNewPup_M」◆
    '[参考]
    'http://www.excel.studio-kazu.jp/kw/20131030060619.html(自分の原点)
    'http://www.excel.studio-kazu.jp/kw/20140926184519.html
    'http://www.excel.studio-kazu.jp/kw/20100921183617.html
    'http://www.h3.dion.ne.jp/~sakatsu/Breakthrough_P-Ctrl_Arrays.htm
    Public Event Click(ByVal PrcName As String, ByVal Args As Variant)
    Private WithEvents PMenu As Office.CommandBarButton
    Private Popup            As Office.CommandBar
    Private Pop              As Collection
    Private Menus()          As clsNewPup_S
    Private Count            As Long

    '=======================================================
    Private Sub Class_Initialize()
        Set Popup = CommandBars.Add(Position:=msoBarPopup)
        Set Pop = New Collection
        Count = 0
    End Sub

    '=======================================================
    Private Sub Class_Terminate()
        Popup.Delete
    End Sub

    '=======================================================
    Public Sub Show()
    'ポップアップの表示
        Popup.ShowPopup
    End Sub

    '=======================================================
    Public Sub AddPops(ByVal Title As String, ByVal PopID As String, _
                      Optional ByVal Nest As String = "")
    'ポップに表示するタイトル、このポップ階層のID(名前、Nest)
    'このポップをどの階層(Nest)の下に付けるか
        With GetPop(Nest)
            On Error Resume Next
                Pop.Add .Controls.Add(Type:=msoControlPopup), CStr(PopID)
                If Err > 1 Then MsgBox "既にPopIDが追加されています。": Exit Sub
            On Error GoTo 0
            Pop(PopID).Caption = Title
        End With
    End Sub

    '=======================================================
    Public Sub AddMenu(ByVal Title As String, _
                       Optional ByVal ProcName As String = "", _
                       Optional ByVal ProcArgs As Variant = 0, _
                       Optional ByVal Nest As String = "0", _
                       Optional ByVal FaceID As Long = 483)
    'メニューに表示するタイトル、
    '実行したいプロシージャの名前(省略した場合タイトルがプロシージャ名になる)、
    '     〃      引数、
    'ポップアップのネスト(階層)、
    'FaceIDを入力する
        Dim tmpMenu As Office.CommandBarButton

        '◇  ↓で指定したPopに、メニューを追加し、コレクションに加える
        With GetPop(Nest)
            '◇メニューを一時的に作成
            Set tmpMenu = .Controls.Add(msoControlButton)
            With tmpMenu
                 .Caption = Title
                 .FaceID = FaceID
            End With

            '◇作成したメニューを配列に入れる
            Count = Count + 1
            ReDim Preserve Menus(1 To Count)
            Set Menus(Count) = New clsNewPup_S
            With Menus(Count)
                .Name = IIf(ProcName = "", Title, ProcName)
                .Args = ProcArgs
                .Item = tmpMenu
                .Parent = Me
            End With
        End With
    End Sub

    '=======================================================
    Private Function GetPop(Nest As String) As Object 'As Office.CommandBar ★
    'Popのコレクションに、Nestがあればそのポップ、そうじゃなければ大本のPopを返す
        On Error Resume Next
            Set GetPop = Pop(CStr(Nest))
            If Err > 0 Then Set GetPop = Popup
        On Error GoTo 0
    End Function

    '=======================================================
    Public Sub clsRaisePopClick(ByVal PName As String, ByVal PArgs As Variant)
    'サブクラスから呼び出すプロシージャ
    '呼ばれたら、Clickイベントを発生させる
        RaiseEvent Click(PName, PArgs)
    End Sub

    '◆クラスモジュールclsNewPup_S◆
    Private WithEvents PupMenu As Office.CommandBarButton
    Private clsParent          As clsNewPup_M
    Private ProcName           As String
    Private ProcArgs           As Variant

    '=======================================================
    Private Sub PupMenu_Click(ByVal Ctrl As Office.CommandBarButton, CancelDefault As Boolean)
    'ポップアップメニューがクリックされたら、
    'clsNewPup_MのclsRaisePopClickを実行する
        clsParent.clsRaisePopClick ProcName, ProcArgs
    End Sub

    '=======================================================
    Public Property Let Name(ByVal NewName As String)
        ProcName = NewName
    End Property

    '=======================================================
    Public Property Let Args(ByVal NewArgs As String)
        ProcArgs = NewArgs
    End Property

    '=======================================================
    Public Property Let Item(NewPop As Office.CommandBarButton)
        Set PupMenu = NewPop
    End Property

    '=======================================================
    Public Property Let Parent(NewParent As clsNewPup_M)
        Set clsParent = NewParent
    End Property

    '◆シートモジュール◆
    Dim WithEvents PopEvents As clsNewPup_M
    Sub PopEvents_Click(ByVal PrcName As String, ByVal Args As Variant)
        'イベント発生
        MsgBox PrcName & vbNewLine & Args
    End Sub
    Sub 呼び出し()
        If PopEvents Is Nothing Then
            Set PopEvents = New clsNewPup_M
            With PopEvents
                .AddPops Title:="次の階層(2)", PopID:="A"
                .AddPops Title:="次の階層(3)", PopID:="B", Nest:="A"
                .AddMenu Title:="1階層目"
                .AddMenu Title:="2階層目(2)", ProcName:="DifProcName", Nest:="A"
                .AddMenu Title:="3階層目(3)", Nest:="B"
            End With
        End If
        PopEvents.Show
    End Sub

 以上長々となりましたが、よろしくご指導お願い致します。
   

< 使用 Excel:Excel2007、使用 OS:Windows7 >


 クラスは(クラスも)初心者なんですが、昔、某掲示板で、そうそうたるメンバが、
 VBA でクラスは必要なのか、あるいは有効なのかということについて議論があったことを
 思い出しています。
(その掲示板は、過去ログの保存時間が超短いので、せっかくの議論をふりかえることができないんですが)
 多くは肯定派というか、こんなに便利だよと、事例をあげて、こういったものをつくっておいて、新規開発時に
 インポートして使いまわせば、楽ちんだねと。

 でも、あるエキスパートさんは(この人はクラスを扱わせてもトップレベルの力量の人)
 順次逐一処理をすることが多いVBAコードでは、通常のSubプロシジャやFunctionプロシジャで十分じゃないか。
 それを共通的おなモジュールにしておいて、必要なときにインポートすれば使いまわしがきくじゃないか。
 で、そのエキスパートさんがいうには、自分にとって有用なクラスといえば、数多くのメンバの領域を管理する時のツール。
 それぐらいだといっておられました。

 βとしては、これほど、極端ではないですが、メンバごとの領域管理には便利だろうなと思います。
 (クラスなしでやろうとすると、たとえばDictionaryの中にDictionaryを持たせ、またその中にDictionaryを持たせ 
  なんてことをやったりして、自分でも無理やりっぽさを感じることがあります)

 以下の例は噴飯ものです。
 おいおい、クラスなら Property Get とか Property Let だろう なんて突っ込みがあがりそう。
 また、こんな処理なら、こうやればいい。何を回りくどいことをしてるんだ。 そういうお叱りも頂戴するかも。

 そのあたりには目をつぶって眺めててください。以下の例では、メンバも2つ、管理項目も2つだけなんですが
 これも、サンプルとして見てください。

 Test1 と Test2 は同じようなことをやろうとしています。
 で、(コードを見るとあたりまえですが)Test1 は1つのメンバの管理しかできません。

 ●標準モジュール

 Type Data
    Name As String
    age As String
 End Type

 Dim myData As Data

 Sub Test1()
    Dim wk As Data

    SetData "太郎", 25

    SetData "花子", 18

    wk = GetData()

    MsgBox wk.Name & vbLf & wk.age

    'あたりまえですが花子のデータのみ。
    'もうこの時点では 太郎のデータの取得は不可能

 End Sub

 Private Sub SetData(nm As String, age As Long)
    myData.Name = nm
    myData.age = age
 End Sub

 Private Function GetData() As Data
    GetData = myData
 End Function

 Sub Test2()
    Dim cls太郎 As New Class1
    Dim cls花子 As New Class1
    Dim wk As Data

    cls太郎.clsSetData "太郎", 25

    cls花子.clsSetData "花子", 18

    wk = cls太郎.clsGetData
    MsgBox wk.Name & vbLf & wk.age

    wk = cls花子.clsGetData
    MsgBox wk.Name & vbLf & wk.age

 End Sub

 ●クラスモジュール(Class1)

 Private clsData As Data

 Sub clsSetData(nm As String, age As Long)
    clsData.Name = nm
    clsData.age = age
 End Sub

 Function clsGetData() As Data
    clsGetData = clsData
 End Function

(β) 2015/03/16(月) 19:34


 ↑ テーマが【イベント処理】ということでしたから、あさってのようなレスでしたね。
 お邪魔ならスルー願います。

(β) 2015/03/16(月) 20:20


 1については,

  Private Function GetPop(Nest As String) As Object 'As Office.CommandBar ★

    'Popのコレクションに、Nestがあればそのポップ、そうじゃなければ大本のPopを返す
        On Error Resume Next
            Set GetPop = Pop(CStr(Nest))
            If Err > 0 Then Set GetPop = Popup
        debug.print typename(getpop)
    On Error GoTo 0
    End Function

 なんてして  呼び出し を実行してみると わかります。 Getpopにセットされるのは、
 CommandBarだけではないです。

 2 これは私も滅多に仕事には使いませんが、あるデータを監視する ループして、
    状態が変わった時点でイベントを発生させるという手法です。

    不安定な面もあるのであまり使いませんが、場合によって 役に立つかもしれません。

[[20141218082047]]

 この事例、最初は、APIのSettimerを使った例で記述しましたが、

 最後に別の方法で同じ仕様を実現した例ですが、この時 特定のデータを監視し続け、イベントにした例です。

 イベントにするメリットは、要らないときは、イベントを書かなくてもいという事です。

 とりあえず、1,2だけです。

( ichinose) 2015/03/16(月) 23:51


 βさん
 確認遅くなって申し訳ないです。
 >順次逐一処理をすることが多いVBAコードでは、通常のSubプロシジャやFunctionプロシジャで十分じゃないか。
 確かにわざわざクラス作る理由ないときが多いですね!
 非表示ブックを用意して、普段よく使うツールなんかを呼び出していますが
 ほぼSubとFunctionです。

 前回の勉強時、「コードの隠蔽」と「複数のインスタンス」についてご指導いただきました。
 βさんから提示されたコードを見ていて、ごめんなさい、何を言いたいのか私にはわかりませんでした。

 下記のようにすればいいのかなとか思いましたが・・・
 もう少し噛み砕いて説明いただければありがたいです。

    Type Data
       Name As String
       age As String
    End Type
    Dim myData() As Data
    Sub test3()
       Dim i As Long
       Dim msg As String
       ReDim myData(1 To 1)
       SetData "太郎", 25, 1
       SetData "花子", 18, 2
       For i = 1 To UBound(myData)
           With myData(i)
               msg = msg & .Name & "は" & .age & vbNewLine
           End With
       Next i
       MsgBox msg
    End Sub
    Private Sub SetData(nm As String, age As Long)
       Dim i As Long
       ReDim Preserve myData(1 To UBound(myData) + 1)
       i = UBound(myData)
       myData(i).Name = nm
       myData(i).age = age
    End Sub

 ichinoseさん
 1について
 CommandBarとCommandBarPopupの二種類を確認できました!
 一番最初のオブジェクトと、その中身(階層)は別のオブジェクトだったんですね。
 納得いたしました。
 ちなみに、2種類の変数型しか入らないとわかっている場合、その二つの型しか入らないように
 宣言することは可能なのでしょうか?
 またはこういうFunctionの組み方が誤っているのでしょうか?

 2について
 明日、時間作って熟読してみます!
 ご紹介のスレ、眺めていましたが、難しすぎて文字通り眺めているだけでした。

 取り急ぎ、ありがとうございます。

(稲葉) 2015/03/17(火) 00:20


 そもそもが、イベントに関するテーマでしょうから、申し上げたように、ピント外れになって
 ごめんなさいね。ここはichinose師匠におまかせしてβは撤退します。

 たしかに、稲葉さんが書かれたように配列に格納したり、あるいは、太郎、花子の直接呼出しをするなら
 それにDictionaryをかませたり、やり方はいくらでもあります。
 >また、こんな処理なら、こうやればいい。何を回りくどいことをしてるんだ。 そういうお叱りも頂戴するかも。
 こうコメントしたとおりです。

 ただ、管理項目が、もっと多く、またそれが階層構造になっていたり、各項目にセットするタイミングが
 ばらばらであったり・・・といった時に、もちろん、標準モジュール側でも、なんとでも、できるわけですが
 クラスにしておくことで、そこがさらっと、使う側は、そのデータ構造を意識することなく、利用できるということなんですが・・・

 たとえば、伝票->元帳->決算書 なんてのをこの形にしておいて、必要な記帳を行ったり、参照を行ったり・・・

 まぁ、テーマが異なるので、すておいてください。
 お邪魔しました。

(β) 2015/03/17(火) 06:21


 私が馬鹿なせいで気を使わせてしまってすみません(汗)
 でも、仰りたいことは分かりました!
 WriteやLeadメソッドを作って特定様式の帳票の読み書きや
 Find Deleatなども実装してあげれば、ユーザーは帳票の形を
 意識することなく使えそうですね!

 多段階層はクラスの再帰処理みたいになるのでしょうか?
 クラスが自分自身を呼び出すコード、
 パッと思い付きませんが、勉強してみたいと思います
 ありがとう御座いました!
(稲葉) 2015/03/17(火) 06:42

    Private Function GetPop(Nest As String) As Office.CommandBar
    'Popのコレクションに、Nestがあればそのポップ、そうじゃなければ大本のPopを返す
        On Error Resume Next
            Set GetPop = Pop(CStr(Nest)).CommandBar
            If Err > 0 Then Set GetPop = Popup
        On Error GoTo 0
    End Function

 このようにして試してみてください。

 3について

 イベントは、オブジェクトモジュールにしか書けませんからねえ

 標準モジュールするならおっしゃる通り、

 >直接呼び出せば

 でしょうね!!

 4については、私もよくわかりませんが、先にご紹介した

[[20141218082047]]

 Settimerで使われている Addressof演算子を通して 関数のポインタを送っていますが、
 この関数をAPIで実行させる行為をコールバックというのだと思いますが・・・。

 同じような事をVBAでするには?

 稲葉さんのクラスsNewPup_Sで例にとると・・・、

 Private Sub PupMenu_Click(ByVal Ctrl As Office.CommandBarButton, CancelDefault As Boolean)

    'ポップアップメニューがクリックされたら、
    'clsNewPup_MのclsRaisePopClickを実行する
        clsParent.clsRaisePopClick ProcName, ProcArgs
  End Sub

 このコードsNewPup_Sの結合度を考えると、sNewPup_Sは、何故 clsRaisePopClickというメソッドを知っているのか?

 ということになります。オブジェクトまでは、パラメータで指定しているので わかりますが、
 呼び出すメソッドまでは わかりませんよね!!

 汎用性を考えると ここをどうするか? なんて 問題が残ります。

 私もまだ 答えをもっていません。Callbyname これが使えるか? というところでしょうか?
 
( ichinose) 2015/03/17(火) 07:23

 >Set GetPop = Pop(CStr(Nest)).CommandBar
 >このようにして試してみてください。
 CommandBarPopupのメンバのCommandBarなんですね!
 オブジェクトブライザで確認致しました。

 一番頭がCommandBarで、その中身がCommandBarPopupなのに、そのメンバがCommandBarっていうのが
 いまいち理解できていませんが・・・。
 後々わかってくると思いますので、次の課題に取り組みたいと思います。

 3について、やっぱりそうですよね。
 隠蔽しているところから呼び出すなら、値を評価した結果をもとに、標準モジュール側で制御すれば
 いいだけですよね!

 結合度についてですが
 CallByNameを使って書き直してみました。
 で、今後はclsNewPup_M(以下MとS)がSに対して結合度が強くなってしまったような気がします。
 少なくとも、S側の汎用性が高くなったので、S単体でフォームやシートモジュールから使えるように
 なったと思います
 (この考え方であってるでしょうか・・・?)

 むしろ、Mはクラスモジュールで持つより、シート、フォームモジュールで設計したほうがスマートな
 印象になりました。

 ●書き換えたS
    Private CallByProc         As String
    Private CallByParent       As Object 'As clsNewPup_M

    '=======================================================
    Private Sub PupMenu_Click(ByVal Ctrl As Office.CommandBarButton, CancelDefault As Boolean)
        'ポップアップがクリックされたら、指定したプロシジャを実行する
        CallByName CallByParent, CallByProc, VbMethod, ProcName, ProcArgs
    End Sub

    '=======================================================
    Public Property Let Parent(NewParent As Object)  ' As clsNewPup_M
        Set CallByParent = NewParent
    End Property

    '=======================================================
    Public Property Let Proc(NewProc As String)
        CallByProc = NewProc
    End Property

 ●M側の設定
            With Menus(Count)
                .Name = IIf(ProcName = "", Title, ProcName)
                .Args = ProcArgs
                .Item = tmpMenu
                .Parent = Me
                .Proc = "clsRaisePopClick"
            End With

 ご紹介先、まだ見きれていません。
 わかるところだけ、とりあえず消化致しました。

 ありがとうございます。
(稲葉) 2015/03/17(火) 10:50

 >CommandBarPopupのメンバのCommandBarなんですね!
 同じ機能のものを指しているのにちょっと配置場所が違うだけでオブジェクトが違うので
 扱いの整合性をとる必要性に迫られたのでないでしょうか!!
 ShapeオブジェクトにDrawingobjectというプロパティ、DrawingobjectにShaperangeプロパティがあるの
 と似たようなもの ぐらいに認識していればよいと思いますよ!!
   
   
   

 >clsNewPup_M(以下MとS)がSに対して結合度が強くなってしまったような気がします

 親が子供の仕様に合わせるのは ある程度は、仕方ないでしょうねえ!!

 でも私も ここ考え中なんです。

 clsRaisePopClickプロシジャーのインターフェースはどうするのか?

 なんてことも課題です。

( ichinose) 2015/03/18(水) 06:59


 ご紹介先、確認致しました。

 フォームのDo Loopで、条件がそろうまでRaiseEventでシートモジュールまで持ち上げて処理を実行
 しているわけですね。
 で、このスレッドでもありました、CallByNameでも置き換えられることを利用して、
 クラスモジュールに変数を二つ追加
 Public CallByProc As String   'CallByNameで呼び出すプロシージャ名を指定
 Public CallByParent As Object 'CallByNameで呼び出すオブジェクト

 シートモジュールに、
 <省略>
 .CallByProc = "repeat"
 Set .CallByParent = Me
 <省略>

    Public Sub repeat(ByVal avariable As Variant)
        With avariable.Font
           .Color = IIf(.Color = vbBlue, 0, vbBlue)
        End With
    End Sub
 とすれば同じ動作になりますね!
 こうすれば、イベントを経由しなくてよいので、標準モジュールでも実装できますね。

 一昔前の自分なら、途中でわからなくなっていたと思いますが、少しは成長できているのでしょうか・・・

 勉強ついでに、ユーザーフォームじゃなくて、シートモジュールにActiveXコントロールでも
 大丈夫じゃないかな?と試行錯誤して 嵌りました。

 なんでかモジュールがあるブック(プロジェクト)だと、モジュールレベルで宣言した変数も
 初期化されてしまうんですね。
 再現コード

    '●clsOLEObj_Test
    Private WithEvents btn As CommandButton
    Private OLEs           As OLEObject
    '============================================================
    Private Sub Class_Initialize()
        Set OLEs = ActiveSheet.OLEObjects.Add( _
                ClassType:="Forms.CommandButton.1", _
                DisplayAsIcon:=False, _
                Left:=10, Top:=10, Width:=100, Height:=100)
            Set btn = OLEs.Object
    End Sub

    '============================================================
    Private Sub btn_Click()
        MsgBox "テスト成功"
    End Sub

    '============================================================
    Private Sub Class_Terminate()
        OLEs.Delete
    End Sub

    '●標準モジュール
    Dim testcls As clsOLEObj_Test
    Sub a()
        Set testcls = New clsOLEObj_Test
    End Sub
    Sub b()
        Set testcls = Nothing
    End Sub

 上記aをコードが記載されているブックで実行すると、実行と同時にボタンが消えますが
 コードが記載されているブックの別ブックで実行すると、なぜかクリックできます。

 またステップ実行しようとすると、「中断モードで〜〜」とか怒られてしまいました。
 デバッグすらさせないのでしょうか!!
 なんででしょう・・・

 ActiveXは諦めて、Shapeで実装しようとすると、今度はOnActionで標準モジュール以外のプロシージャ
 が実行できないので、こちらも諦めました。

 まだまだ知らないことばかりで、楽しいのです。
(稲葉) 2015/03/18(水) 16:01

 > とすれば同じ動作になりますね!
 標準モジュール内のプロシジャーを呼び出すなら Runメソッドになるのかなあ

 ただ、RaiseEventを使う方が 実際に呼び出すプロシジャーが存在するとかしないとか という点を
 気にしなくてもよいので便利ですけどね イベントプロシジャーがなければ、無視されるだけですから。

 シート上にActiveXControlを生成するコードを実行すると プロジェクトはリセットされる

 は、私も以前投稿したことがありました。

http://www.vbalab.sakura.ne.jp/vbaqa/c-board.cgi?cmd=one;no=7011;id=excel

 手動でActiveXControlを作成すれば、デザインモードがOnになりますから、当然と言えばそうなんですがね。

 この時から、シートとActiveXControlは、相性がよくない  という思いがありました。

 この事象は、幸い早くに知ったので こういうことは仕事では行いませんが、大きいプログラムを
 作っていると 原因が不明でこの変数の初期化ということが起こってしまうことがありました。

 プロジェクトがリセットされるのだから、別のブックにデータを置けば、この現象は起こらないはず、
 という仮説を立て、アドインとしてデータ保管専門のブックに値を長時間保持したい変数を置くようにしました(このアドインには、参照設定はなし)。

 リンクスレッドの投稿のほんの少し後だと思いますが、それ以来この現象には、遭遇していません。

( ichinose) 2015/03/19(木) 06:53


 >ただ、RaiseEventを使う方が 実際に呼び出すプロシジャーが存在するとかしないとか という点を
 >気にしなくてもよいので便利ですけどね イベントプロシジャーがなければ、無視されるだけですから。
 その便利さは、書き換えたときに感じました。
 一々記述のあるモジュールのセットやプロシジャーを指定しなくても、候補にあがってきてしかも引数まで
 付けてくれるのですから!

 >シート上にActiveXControlを生成するコードを実行すると プロジェクトはリセットされる
 私がやっていることは、12年遅れていますね(汗
 検索しても、全然情報がないので 余り重要がないのでしょうか。

 > という仮説を立て、アドインとしてデータ保管専門のブックに値を長時間保持したい変数を置くようにしました(このアドインには、参照設定はなし)。
 以前から、この運用を伺っておりますが、まだ運用に達していないというか、
 そこまで仕事上有用なコードを作れていないだけで、構築を考えるときに活用致します。

 今回の質問 特に2に関しては大変勉強になりました。
 言葉の定義を含め、まだまだ実務経験が浅いこともわかりましたので、今後もとにかく挑戦と失敗を
 繰り返しながら、IFと同じ感覚で簡単にイベントを使いこなせるよう努力していきたいと思います。

 長々とありがとうございました。
 またきっと、わからないことができたときにスレッドを立てさせていただきたいと思います。

(稲葉) 2015/03/19(木) 10:18


 一段落したようですね。

 私には難し過ぎるトピックなんですけど、
 野次馬根性が抑えられないので、ちょっとお邪魔します。

 > 2)クラスに独自イベントを実装できることを、実際に作りながら勉強していたのですが、
 >   何かのイベントをクラスに受け取らせて、配列として返す以外の使い方がわかりません。
 >   どういったときに、独自のイベント単独で便利になるのでしょうか?

 逆読みすると「配列として返す」使い方がある、と云うことになりますが、
 (そうなんでしょうか? そうだとすると)少し違和感があるんですけど。

 そう言うことは出来ない、と云うのが「擬似からの脱却」でも認識されていることだと思うのですが。

  >   ・受け手であるフォーム側で、配列宣言とWithEvents宣言が共存できないので
  >     カスタムイベントが使えない

 それで「・・脱却」ではどうしたかと云うと、クラスの2重化をして2段目のクラスの配列を1段目のクラスに作る、
 と云うことにしたのですけど、そこでも2段目のクラスにEventステートメントは書いていないです。

 必然的に、2段目のクラスにRaiseEventはありませんし、
      1段目のクラスにもそのイベントプロシージャがありません。

 代わりに、2段目にあるメンバーのコントロールのイベントプロシージャで、
 1段目のメソッド(MyParent.RaiseClick MyIndex、これはイベントプロシージャではない)をコールさせています。

 結局、独自イベントが持てれば配列を作れる、と云うことにはつながらないと思います。
 じゃ、何が出来るようになったのと云われると困るんですけど、
 大本に一段目のクラスのコールバックプロシージャが書けるようになった、
 と云う当たり前なことしか分かりません。

 言葉の使い方の問題かも知れませんけども。

(半平太) 2015/03/19(木) 14:00


 > 2)クラスに独自イベントを実装できることを、実際に作りながら勉強していたのですが、
 >   何かのイベントをクラスに受け取らせて、配列として返す以外の使い方がわかりません。
 >   どういったときに、独自のイベント単独で便利になるのでしょうか?

 私は、↑これ 「配列として返す」というフレーズに重点を置いていませんでした。
 結果的に ユーザフォームで WithEvents 付きで 宣言した クラスを使い、
 イベントを共有したい個々のコントロールは、最初に登録します。

 このクラスが実質的な配列(コレクション)そのものです。オブジェクトですから、
 これを配列風に扱うには 添え字の役目をするプロパティが要ります。

 ユーザーフォームで共有するイベントには、配列ではなく対象コントロールの添え字が返されるのですよね

 その添え字とユーザーフォーム内で宣言したクラス(例 ClsCtrl)を使って、
 clsctrl.item(id) などとすれば、配列風に扱えるということです。

 このような仕組みが 理解されたうえでの「配列として返す」というフレーズではないでしょうか?

 2では、イベントを提供するトリガーとなるものが 別のオブジェクトの既成のイベント以外に
 VBAでは、方法はないのか?

 という意味だったのではないでしょうか?

 よって、VBAで直に 何かの変数を監視する方法もあるという記述をしたのですが・・・。

 > 大本に一段目のクラスのコールバックプロシージャが書けるようになった

 稲葉さんは、イベントプロシジャーが標準モジュールにかけないということがご不満だったみたいです。

 それを解消すためにインターフェースづくりだったのですが、結果的に
 RaiseEventの便利さが再確認できたスレッドだったと認識しています。

( ichinose) 2015/03/20(金) 05:45


 返事遅くなってすみません
 今日は遊びに出掛けちゃってるので、後日お返事書かせていただきます
 日本語というか、表現って難しいですね
 なにより、用語の定義から分かっていないわけですから、、、

(稲葉) 2015/03/20(金) 09:21


 >このような仕組みが 理解されたうえでの「配列として返す」というフレーズではないでしょうか?

 私の読み違いでした。撤回いたします。

 稲葉さん すみませんでした。m(__)m

 久しぶりに「疑似からの脱却」を読んでみて、胡散臭いながらも、その価値が認識できました。
 ・・と同時に新たな受け入れられないことが生じてしまった。

 なぜ、Item Property ではなく Addメソッドにしたのでしょうかねぇ。
      .Add cmdSun  → Set .Item(1) = cmdSun

 と書ければ、雰囲気がもっとよくなるような気がするんですけど。

 気が向いたら、トライするつもりです。
 暇はたっぷりあるんですが、やる気を起こすのが大変で。

 ご指導、ありがとうございました。

(半平太) 2015/03/20(金) 10:36


 お返事大変遅くなって申し訳ありません。

 自分なりに読み返して、多分聞かれたことの回答になっているであろうと思う回答を
 準備致しました。

 「配列として返す」の意味
 ↑で掲載させていただいた例ですと、Menuが実質的な配列で
 Getプロパティを用意していませんが、ichinoseさんがおっしゃったように
 Get Item(id As Long)プロパティを用意して参照できますよね、って意味でした。
 今回はサブプロシジャーにLetとPrivateしか置いていないので何もできませんが・・・

 >なぜ、Item Property ではなく Addメソッドにしたのでしょうかねぇ。
 >   .Add cmdSun  → Set .Item(1) = cmdSun
 これについて、ちょっとできるのかなと試してみました。
 結論から言うと、私にはできませんでした!!
 が、プロパティを使って間接的に入れることは可能でした。

 試したこと
 ×Publicで配列(添え字)は宣言できなかった
 ×Property Letで配列(添え字)は宣言できなかった
 △Property Getに引数を付けて、サブクラスを返し、間接的に入れる(Setではない)
  ★の部分参照

 ここまで試しましたが、どっちがいいのかまで優劣判定できませんでした。

 以下再現コード
    '●ユーザーフォーム1
    '==================================================
    Private WithEvents clsEv As Class1

    '==================================================
    Private Sub UserForm_Initialize()
        Set clsEv = New Class1
        '★Setで宣言できなかった。
        clsEv.Item(1).Item = Me.Controls.Add("Forms.TextBox.1", , True)
        With clsEv.Item(1).Item
            .Top = 10
            .Left = 10
            .Width = 100
            .Height = 20
        End With
    End Sub

    '==================================================
    Private Sub clsEv_tbc(ByVal num As Long, ByVal str As String)
        Debug.Print num & str
    End Sub

    '●Class1
    '==================================================
    Public Event tbc(ByVal num As Long, ByVal str As String)
    Private clsItem() As Class2

    '==================================================
    Private Sub Class_Initialize()
        ReDim clsItem(10)
    End Sub

    '==================================================
    Public Property Get Item(id As Long) As Class2
        If clsItem(id) Is Nothing Then
            Set clsItem(id) = New Class2
            With clsItem(id)
                Set .CallByParent = Me
                .CallByProc = "RsEv"
                .num = id
            End With
        End If
        Set Item = clsItem(id)
    End Property

    '==================================================
    Public Sub RsEv(x As Variant)
        RaiseEvent tbc(x(0), x(1))
    End Sub

    '●Class2
    '==================================================
    Private WithEvents tb As MSForms.TextBox
    Public CallByParent   As Object
    Public CallByProc     As String
    Public num            As Long

    '==================================================
    Private Sub tb_Change()
        CallByName CallByParent, CallByProc, VbMethod, Array(num, tb.Value)
    End Sub

    '==================================================
    Public Property Let Item(ByVal NewItem As MSForms.TextBox)
        Set tb = NewItem
    End Property

    '==================================================
    Public Property Get Item() As MSForms.TextBox
        Set Item = tb
    End Property

 私はここまでしかわからなかったのですが、添え字つきプロパティのやり方があるのかもしれません。
 わかる方がいれば私も教えてもらいたいです。
 逆に難しく考えすぎて単純なことを見落としている可能性がありますが・・・(いつもの癖)

 気に入らなかった点を再度まとめると
 1)標準モジュールにイベントがおけない
 2)独自イベントを持てるのに、何かのイベントをフックするしかトリガーを持てない(と考えていた)
 3)では独自イベントの利便性は何か?
 以上3点の中の3)で、RaiseEventは使い方によって、構築が楽になる
 という結論を得たことが私の収穫です。
(稲葉) 2015/03/23(月) 12:10

こんばんは。

 >'★Setで宣言できなかった。
 >       clsEv.Item(1).Item = Me.Controls.Add("Forms.TextBox.1", , True)

この部分ですが原因はClass2のLetの為ですね
'==================================================

    Public Property Let Item(ByVal NewItem As MSForms.TextBox)
        Set tb = NewItem
    End Property

プロパティの型がオブジェクトの場合通常Property Setを使いますよね?

    Public Property Set Item(ByVal NewItem As MSForms.TextBox)
        Set tb = NewItem
    End Property

この2つのクラスは循環参照を起こしていますので、Class_Terminateイベントが発生しません。
エクセルを終了するまで、この2つのクラスは解放されません。
循環参照を解除する為にはひと手間必要です。「擬似からの脱却」の筆者、角田さんはClearメソッド
を親クラス、子クラスに用意して使用されています。
知っていた事であればご容赦下さい^^;

稲葉さんは、mougの方は見られてます?
現在進行形の質問でmattuwan44さんのクラスの質問に、循環参照について投稿していますので
そちらも是非ご覧下さい。

http://www.moug.net/faq/viewtopic.php?p=452385#452385
(みそじのおじさん) 2015/03/23(月) 22:52


 >お返事大変遅くなって申し訳ありません。

 お手数をお掛けします。
 既に撤回しておりますので、放って置いていただいても構わなかったのですが、
 そうなると、私も「起きていないやる気を」すこし起こさないとならないです ^_^;

 先ず(斜に構え気味の)私の原点を述べます。

 それは、「疑似からの脱却」と云うキャッチフレーズに胡散臭いものを感じる、と云うものです。

 「疑似配列は劣るものであり、俺はそれを克服出来たー」と言うニュアンスを感じさせるのですが、
 実際は、疑似配列は充分スマートであると私は思うので、なぜ面倒な2段階オブジェクトに仕立ててまで、
 脱却しなければならないのか疑問に感じます。

 脱却の必要性は、そのページに書かれていましたが、私には理解不能。
 悪く言えばこじつけ、よく言えばVBAを仕事でフルに使っている人しか分からない深遠な理由。

 2段階オブジェクトは名実ともに配列とは呼べないフランケンシュタインのようなものであり、
 失礼な表現になりますが、「疑似からの『疑似』脱却」であり、別種の疑似を作ったに過ぎない。

 ※「名実とも」などと云う概念は以前は持っていなかったのですが、
  「実質的な配列」と云う定義が今回出てきたので、誤解が生じない様に使用しております。

 > 「配列として返す」の意味
 > ↑で掲載させていただいた例ですと、Menuが実質的な配列で
 > Getプロパティを用意していませんが、ichinoseさんがおっしゃったように
 > Get Item(id As Long)プロパティを用意して参照できますよね、って意味でした。

 「実質的な配列」との意味であれば異論ありません。私の読み違えです。

 > >なぜ、Item Property ではなく Addメソッドにしたのでしょうかねぇ。
 > >   .Add cmdSun  → Set .Item(1) = cmdSun
 > これについて、ちょっとできるのかなと試してみました。
 > 結論から言うと、私にはできませんでした!!
 > が、プロパティを使って間接的に入れることは可能でした。
 > 試したこと
 > ×Publicで配列(添え字)は宣言できなかった
 > ×Property Letで配列(添え字)は宣言できなかった
 > △Property Getに引数を付けて、サブクラスを返し、間接的に入れる(Setではない)
 >  ★の部分参照

 「Publicで配列(添え字)は宣言できなかった」については、こちらでも確認できました。

 それ以外は、ちょっと意味が分かりませんでした。

 私としては、下記PropertySetステートメントを書けば、実現可能と思ったのですけど。
 <Class1>
   Public Property Set Item(id As Long, TxB As MSForms.TextBox)
           Set clsItem(id) = New Class2
           With clsItem(id)
               Set .CallByParent = Me
               .CallByProc = "RsEv"
               .num = id
               Set .Item = TxB
           End With
   End Property

 <Class2>
     Public Property Set Item(ByVal NewItem As MSForms.TextBox) 
         Set tb = NewItem
     End Property

 それにしても、オブジェクト変数に代入するのに、Propety Let でもいいとは知らなかったです。
 それだと、Property Setの存在意義が分からなくなります・・・

 > 気に入らなかった点を再度まとめると
 > 1)標準モジュールにイベントがおけない
 > 2)独自イベントを持てるのに、何かのイベントをフックするしかトリガーを持てない(と考えていた)
 > 3)では独自イベントの利便性は何か?
 > 以上3点の中の3)で、RaiseEventは使い方によって、構築が楽になる
 > という結論を得たことが私の収穫です。

 1)については、コントロールが属するオブジェクトに置くのが自然だと思います。
 ただ、「疑似からの脱却」の筆者が主張するような、個別オブジェクトにイベント処理を書き込まず
 一か所でプロシージャを管理するのが便利なんだ、と云うことなら、いっそのこと、イベントに限らず、
 全ての処理用プロシージャを一カ所に集める方がもっと良いハズだと考えることも理屈です。
 その一カ所として標準モジュールにすると云うのもありかと思います。

 ・・・なこと言ったって、イベントプロシージャが書けないじゃないか、と云うことになりますが、
 どうせ、インチキをやっているんですから、標準モジュールのメソッドに飛ぶようにしたって
 後ろめたいことは増えないような気がします。(そういう運用に慣れていないだけかもです)
 2段ロケットにしないでいい分だけ、シンプルになります。

 3)については、兎に角、配列と絡めて考えるのは止めたいって気分です。
 ・・・そうは言っても、配列が絡まないと考える価値もないトピックかも知れません。

 では、一体何個の同種オブジェクトが必要であり、どんなクソ長いプロシージャを持つオブジェクトなのか、
 それが、独自イベントを利用するかどうかの現実的な分かれ道になりそうな気がします。

 多分、ここで仕事でフル活用している人種と趣味の人種で対応が分かれるんでしょうね。
 仕事なら、さぞ沢山の同種オブジェクトを作ることもあるでしょうし、
 クソ長いプロシージャも必要なんでしょうね・・・本当かなぁ?

(半平太) 2015/03/23(月) 23:50


 >「疑似からの脱却」と云うキャッチフレーズに胡散臭いものを感じる
 半平太さんのこの「胡散臭い」というフレーズ好きなんですが、
 私が仮に思っていても言えない、角田さんには、40歳手前から「VBA研究所」で直接的・間接的に
 色々と教えていただいたので・・・(今57歳)。
 VBA研究所という名称も理研ぽくって胡散臭い? いえいえ、私は、好きですが・・・。

 >「疑似配列は劣るものであり、俺はそれを克服出来たー」

 またあ、心の中を見透かしたような記述・・・、止めましょうよ!!
 そんなこと 思ってないって・・・、たぶん。

 コントロール配列の一番の目的は、配列そのものというより、同種のコントロールのイベントの共有化です。よね?

 このイベントの共有化を実現するのに 、

 疑似だと モジュールを分けなければならない(フォームモジュールとクラスモジュール)。
 脱却だと 同じモジュールで実現できる(フォームモジュールだけで実現できる)。

 大きくは、この違いです。

 この違いは、とどのつまり メンテしやすいわかりやすいプログラム構造が実現できるか否か

 という問題になります。

 脱却では、オブジェクトを提供していますから、われわれは、このインターフェースを理解すれば、
 フォームモジュール内で処理が管理できますから VBAプログラマがわかりやすいコードが書けます。
 2段階方式の長いコードは 一回作れば 再利用できますし、そもそも隠蔽化されていますから、
 インターフェースと機能だけ理解すればよいですよね。

 疑似だとフォームモジュールとクラスモジュールとでユーザーフォームのコードを分けて管理しなければ
 ならないので メンテするのが脱却より 大変。そもそもオブジェクトって、データとアルゴリズムを
 隠蔽化したもの のはずですが、これでは隠蔽化されていません。

 プログラム作成する手法として、構造化プログラミングという手法を学んできました。
 結果として、構造化された に近いものが脱却の方だと思います。

 私が 疑似からの脱却 を参考にお勧めするのは この点が理解できるのではないかと思ったからです。

 この話を突き詰めれば、構造化プログラミングの是非まで言及しなくてはならないでしょうねえ!!

 規模の大きいプログラムを作成してくると こういう考え方も必要になってくると思います。

 >それにしても、オブジェクト変数に代入するのに、Propety Let でもいいとは知らなかったです。
 >それだと、Property Setの存在意義が分からなくなります・・・

 これは同感です。自分が書くときは、肝に銘じたいことです。

 が、

 range("a1").value=range("b1") というコードは、

 property let ですよね!!  

 パラメータは、あくまでも range("b1") というオブジェクトです。

 こういう事例があるから Letでオブジェクトも渡せるようにしているのでしょうね!!

( ichinose) 2015/03/24(火) 06:48


 みそじのおじさんからもご紹介がありましたけど、同時進行で進んでいる、2つのトピ、
 いずれも、大変勉強になり、興味深くROMしております。
 あちらは、領域のハンドリングですが。

 どうも、Property Let や Property Get や Property Set が、いつまでたっても頭にスキッと入らず
 ふつうの Sub プロシジャでいいじゃないかとか、ふつうの Functionプロシジャでもいいじゃないかとか
 Public 変数 宣言しておけば、立派なプロパティじゃないかとか、そういうレベルですので、ついていくのが
 大変ですが。

(β) 2015/03/24(火) 10:22


 ▲みそじのおじさん(二重敬語にしてもおかしかったので・・・)
 Setの件、全く存在を忘れていました。
 早速書き直したところ、無難にSetできました。
 Clearの件も改めて記述することではございませんが、ちゃんとTerminateの動作確認致しました。
 読んで理解したつもりが、片手落ちだったようです・・・。
 添え字付きプロパティについても、一度エクスポートして〜〜既定のプロパティにするところで理解
 できました。
 ありがとうございます。

 moguのほう、すみません 私はエクセルの学校しか拝見しておりません。
 他サイトではセキュリティ上、書込みできないので。。

 で、早速確認致しました。
 まず、お大事にしてください!!
 内容については、難しすぎます!!

 自分にとって、相当背伸びなのですが、せっかくですので教えてください。
 Implementsによる隠蔽です。
 まずこれ 今日知りました。 どういう仕組みかどうかも付け焼刃でざっとヘルプを読んだ程度です。
 (稲葉) 2015/03/23(月) 12:10で上げたコードのクラス1 RsEvを隠蔽しようと考えました。

 Class3を作って、Public RsEv
 Class1にImplements Class3 と Private Sub Class3_RsEvを記載し、cls3 As Class3のインスタンスを作って
 Class2のCallByParentにcls3を渡して、CallByNameで呼び出すようにしました。
 結果、イベントはちゃんと取得して、Class3のコードまでたどり着くのですが、Class1のPrivate Sub Class3_RsEvまで
 到達致しません。

 なんとなくだめなのはわかっているのですが、体系的に理解できておりません。
 よろしければご教授いただけないでしょうか?

 ▲半平太さん
 >私としては、下記PropertySetステートメントを書けば、実現可能と思ったのですけど。
 確認できました。
 Property Set、前述したとおり忘れていました。
 またPropertyは第二引数を指定すると、添え字みたいに使えたのですね・・・
 思いこみで引数は一つだけだと思っていました。
 ちなみに

 >×Property Letで配列(添え字)は宣言できなかった
 については
                         ↓この部分
 Public Property Set Item()(TxB As MSForms.TextBox)  
                         ~~
 のことを「できない」と表現しておりました。
 赤っ恥ですね。

 となると、益々「Addメソッドの必要性・利便性は何か?」というところ
 私もわからなくなりました。
 たくさん引数を渡すならともかく、なんでわざわざAddなのでしょう

 ※わき道ですが、以前国取りのロジックで、クラスの中に構造体を定義できないと言っていた点
  その後進展あったでしょうか?
[[20140805153308]]
  そもそも私なんかじゃわからないので、講義受けられるうちに聞いておきたいなと・・・

 ちょっと時間が取れなくて、ここまでですが、自分なりに確認できた点です。

(稲葉) 2015/03/24(火) 13:03


 こんにちは。

 私自身mougのクラス研究室内でYU-TANGさんに教えて頂いたことなので
 うまく説明出来るか心配ですが。。

 稲葉さんの説明で大体何をされたか理解しているつもりですが、間違っていればご容赦を^^

 >Class3のコードまでたどり着くのですが

 Class3を作成し、もしかしてRsEvプロシージャ内に実際の処理を書かれてしまいました?
 Public Sub RsEv(x As Variant)
    ''処理
 End Sub

 インターフェースクラスとは、定義のみを書き実際の処理をクラス内に書いてはいけません。
 Class3だと意味合いがわかりづらいので、ここからはIClass1(Interface Class1)とします。
 処理を行うのは、このインターフェースクラスを実装したクラスになります。

 クラス名 IClass1 インターフェースクラス
 '●IClass1
 Option Explicit
 Public Sub RsEv(x As Variant)

 End Sub

 IClass1は、これだけです^^

 クラス名 Class1
    Option Explicit

    '●Class1
    Implements IClass1 ''IClass1をImplementsします

    '==================================================
    Public Event tbc(ByVal num As Long, ByVal str As String)
    Private clsItem() As Class2

    '==================================================
    Private Sub Class_Initialize()
        ReDim clsItem(10)
    End Sub

    '==================================================
    Public Property Get Item(id As Long) As Class2
        If clsItem(id) Is Nothing Then
            Set clsItem(id) = New Class2
            With clsItem(id)
                Set .CallByParent = Me ''Me(Class1)を渡せば十分です
               '' .CallByProc = "RsEv" 使用しません
                .num = id
            End With
        End If
        Set Item = clsItem(id)
    End Property

    '==================================================
    '注目するのはImplementsしたプロシージャはPrivateスコープになる事です
    Private Sub IClass1_RsEv(x As Variant)
        RaiseEvent tbc(x(0), x(1))
    End Sub

    ''クラス名 Class2
    '●Class2
    Option Explicit

    '==================================================
    Private WithEvents tb As MSForms.TextBox
    Public CallByParent   As Object
    Public CallByProc     As String
    Public num            As Long

    '==================================================

    Private Sub tb_Change()
    ''  CallByNameでは公開(Public)しているものしか呼べないので×
    ''  CallByName CallByParent, CallByProc, VbMethod, Array(num, tb.Value)

    ''CallByParent(実体はClass1)をIClass1型にキャスト(型変換)しCallする
        CastIClass1From(CallByParent).RsEv Array(num, tb.Value)
    End Sub

    '==================================================
    Public Property Set Item(ByVal NewItem As MSForms.TextBox)
        Set tb = NewItem
    End Property

    '==================================================
    Public Property Get Item() As MSForms.TextBox
        Set Item = tb
    End Property

    '==================================================
    'このFunctionが味噌ですね!
    Private Function CastIClass1From(ByVal obj As Object) As IClass1
        Set CastIClass1From = obj
    End Function

    ''UserFormモジュール
    Private WithEvents clsEv As Class1

    '==================================================
    Private Sub UserForm_Initialize()
        Set clsEv = New Class1
        'Setに変更しました^^
        Set clsEv.Item(1).Item = Me.Controls.Add("Forms.TextBox.1", , True)
        With clsEv.Item(1).Item
            .Top = 10
            .Left = 10
            .Width = 100
            .Height = 20
        End With
    End Sub

    '==================================================
    Private Sub clsEv_tbc(ByVal num As Long, ByVal str As String)
        Debug.Print num & str
    End Sub

 ユーザーフォームモジュール内で、clsEv. として出てくるインテリセンス候補にRsEvは
 出てきませんよね!

 CallByNameとの組み合わせではNGですが、プロシージャの隠蔽をこのように私はやって
 おります。
(みそじのおじさん) 2015/03/24(火) 15:45

 みそじのおじさん
 早速ありがとうございます。
 試してみました!!
 Class1をIClass1に入れる(Implements)いなければいけなかったことなんですね!!
 Castって何のことだろう? IClassに入れてばいいだけなんじゃないのかなー?
 ってことで、勝手に省いてました。 以下言い訳と誤解の内容です。

 御指摘頂いたところは、見よう見まねでClass1にPrivateスコープでちゃんと宣言できていましたが
 Class1からClass2に渡すところで、IClass1を渡していたので、IClass1がただ実体化されて、なにもない
 メソッドが実行されていたわけですね。
 (あってますかね??)

 で、CallByNameについて実験したら、できてしまいました。
 CallByName CastIClass1From(CallByParent), "RsEv", VbMethod, Array(num, tb.Value)
 理由は、Class1は隠蔽のためにPrivateで宣言しているけど、IClass1はPublicで宣言しているから、
 CallByNameが使えたってことでいいんですかね?

 疑問に残ったのが、隠蔽化と結合度についてです。

 Class2の子クラスのみ結合度を下げようと、ichinoseさんからご指導いただいておりました。
 今回のImplementsを使った場合、親クラスにメソッドと型が結合してしまいます。
 メソッドはCallByで回避できたとして
 CastFunctionで型変換しないといけないので、どうすればいいのでしょうか?
 型変換を親クラスのメソッドで実現したとしても、そのメソッドを隠すためにさらにImplementsが必要・・・
 と頭の中で堂々巡りです・・・

 結合度と隠蔽は両立できない課題なのでしょうか?

(稲葉) 2015/03/24(火) 18:10


 仕事中の投稿とはいえ検証不足でした。大変失礼しました。
 私も勉強になります!稲葉さんありがとうございます。

 さて、結合度のお話ですが、私もichinoseさんからExcel質問箱IVというサイトで学ばせて頂いて
 おりました。「それは結合度が強い」その言葉をみた時の衝撃は未だに忘れておりません。

 私はプロではありませんし(ただの町工場のおっちゃんです^^;)語るだけの知識も持ち合わせていませんので感じた事を少しだけ。

 確かにClass2だけを見ると、誰から呼ばれるからは知らなくてもいいし(プロパティで押さえられるので)
 呼び出すプロシージャも設定されるまで知らなくてもいいですよね。

 ですが、Class1(親クラス)からみるとClass2は立派に結合していると思うのです。
 Class1は、clsItem(Class2型)をメンバに持ち、今のClass2のメンバがどれ一つ欠けても成り立ちません。

 この時点でClass1とClass2は気っても切れない親子関係なのではないでしょうか?
 「結合度を下げる」と逆の意味合いの「密結合」にあたると思います。

 例えばClass2は独立性が高いから、Class5からも利用しようという場面があったとして
 Class5からも利用するにはClass2に若干の変更が必要だった、なんて事になるとClass1は破綻します。

 Class5はChangeイベントが起きた時に、Class2のIndexとTextBoxのValueではなくTextBoxのTagを
 要求したとします。すると
 CallByName CallByParent, CallByProc, VbMethod, Array(num, tb.Tag)
 とClass2の書き換えが必要になります。(まあこのくらいならTextBoxそのものの参照を返せば解決
 しますが。。)

 Class2も実は、Class1が要求している形になっているのです。

 私の感想は、密結合なら密結合らしく、振舞っていいのではと思いました。
 Class2のメンバに呼び出し元であるオブジェクトを設定してますので、子→親への経路は確保
 出来ているのでCallByNameに拘らなくてもいいのではと思いました。

 決してCallByNameの非推進派というわけではありません^^;  CallBackするときは私もよく使います。

 この辺りのお話は、ichinoseさんの見解を私もお聞きしたいと思っています!!
 どうぞよろしくお願い致します。

(みそじのおじさん) 2015/03/24(火) 20:38


 > 疑似だと モジュールを分けなければならない(フォームモジュールとクラスモジュール)。
 > 脱却だと 同じモジュールで実現できる(フォームモジュールだけで実現できる)。
 正直言って、何を言っているのか理解不能です。
 フォームモジュール単独で何が実現しているのですか?

 > 脱却では、オブジェクトを提供していますから
 そのオブジェクトは上述フォームモジュールの一部と云えるんですか? 
 言えるのであれば単独ですけど。

 >われわれは、このインターフェースを理解すれば、
 >フォームモジュール内で処理が管理できますから VBAプログラマがわかりやすいコードが書けます。
 > 2段階方式の長いコードは 一回作れば 再利用できますし、そもそも隠蔽化されていますから、
 > インターフェースと機能だけ理解すればよいですよね。
 ここも理解不能です。
 我々は(私はかも)、いまクラスの設計から自分で考える、との前提で論議が進んでいると認識しております。
 メンテナンスは一つのクラスを相手にすれば済む話であり、疑似で負担が余分に増えることはないです。

 > 疑似だとフォームモジュールとクラスモジュールとでユーザーフォームのコードを分けて管理しなければ
 > ならないので メンテするのが脱却より 大変。
 ここも分からないですねぇ。
 もともと別のものなんですから、分けて管理するのは当たり前と思うのですけど。

 >オブジェクトをそもそもオブジェクトって、データとアルゴリズムを
 > 隠蔽化したもののはずですが、これでは隠蔽化されていません。
 ここは私の力量不足の為だと思いますが、分かりません。

 何を隠蔽する必要が「現実に」あるのですか?
 必要なとこだけ隠蔽を考えればいいと思うのですけど、その対象が分からないと私には思考が進みません。
 (浅学な私だけでなく)誰がやっても疑似ではそれを実現出来ないことなんですね?

 >プログラム作成する手法として、構造化プログラミングという手法を学んできました。
 これも分からないです。
 疑似配列を使うと、構造化プログラミングが出来なくなるとは。。
 概念の次元が違うような気がします。

 >この話を突き詰めれば、構造化プログラミングの是非まで言及しなくてはならないでしょうねえ!!
 >規模の大きいプログラムを作成してくると こういう考え方も必要になってくると思います。
 私はオブジェクト指向プログラミングの真似事ができればいいと思っています。

 当然、大規模プログラミング、分業プログラミングに対応するものです
 (大風呂敷だなぁ 汗。因みに私はシステム開発をしたことはありません。開発をしてくれと云う立場だけ)
 ただ、VBAで・・、ここの掲示板で・・、そこまで思いを巡らさないとならないのか、と云う気分にはなります。

 稲葉さんへ

 >以前国取りのロジックで、
 済みません。ハッキリ帰趨を覚えておりません。
 現状、引数のデータの型をキッチリ合わせれば問題が無いであろう、との印象を持っております。

(半平太) 2015/03/24(火) 20:41


 考え直しました。 m(__)m

 やっぱり、大規模開発なんて実際は無いでしょうけど、
 あったら、脱却の方がいいですね。

(半平太) 2015/03/24(火) 22:45


 最初の方の投稿で
 >私もまだ 答えをもっていません。Callbyname これが使えるか? というところでしょうか?
 と記述しました。
 以前からここ迷っていたんです。

 コールバックするメソッド名を送ることは出来ても パラメータの数や内容・・・。

 どうしてもClass2の仕様という形で納めるしかないのか?

 実際には、Class2で発生するイベントも一つではないですよね、

 Textboxであれば、 Change Keydown Mouseup etc。

 この時 Class1側で コールバック関数にどのように対応させるか

 例えばですが、

 パラメータは最初の形式を決める

 Arg1は、イベント名(又は、イベントコード)、Arg2は、ID、Arg3...必要なパラメータ 

 とし、Class2がテキストボックスのイベントを管理するなら、イベントによって返すパラメータを細かく
 定義する。

 返す名前は固定でも良いのかもしれません(それをも定義して)、もっとも
 ここは、CallByNameが使えるのですから メソッド名は、自由に設定できるのなら それに越したことはないか?

 Class2で、配列を返すか 又はClass1で ParamArray 等を使って工夫するか?

 Class1がClass2の仕様に合わせてコードを書くのは当然ですよね。だってClass2を使うのですから。

 Class2がどこまで違うオブジェクトから扱われても汎用性を保てるかが問題だ。

 このようなことが堂々巡りしていました。

 Implementsステートメント 正直私は 使いません。

 稲葉さんの

 >ちなみに、2種類の変数型しか入らないとわかっている場合、その二つの型しか入らないように
 >宣言することは可能なのでしょうか?

 最初に頭に浮かんだのは このImplementsステートメントだったんですが、提供されているオブジェクトを
 インプリメントすることができるのか? なんて思っていたら 別のプロパティが提供されていたので
 それを投稿したのです。その時点で これも問題になるだろうな という予感めいたものがありました。

 みそじのおじさんさんのコード拝見しました。

 Iclass1を使うことで Class2を使うオブジェクトを限定するという意味では拝見したコード賛成です。
 そういう意味でClass2の中でCastIClass1Fromという関数を使っているいのですよね?
 チェック機能も含めていると言えます。(Iclass1仕様でなければエラーになりますから)

 もしそうだとすれば、使えるオブジェクトは限定されますが、Iclass1が要求しているのは、
 RsEv というインターフェースだけで、戻る関数は不可欠ですからねえ。

 逆に上記の意味ではなく、Iclass1の構造は簡単な例で Class1メンバ隠蔽が目的ならば、何故Class2の中でインプリメントするのか? という疑問が生まれます。

 Class1の中でインプリメントを行い、それをClass2に渡せばよい ことになります。
 この場合だとCallByNameが少し生きてくるかなあ? 稲葉さんのご期待には添えますが、

 こうしてみると、CallByNameを使用するか否かが大きな問題ではなくなってきますね。

 私は前者の意味なら みそじのおじさんさんのコードが良いと思いますけどねえ

 Class2の仕様として、Iclass1を呼び出しオブジェクトとして規定する。

 私は、良い仕様だと感心しました。

 みそじのあじさんさんは、まだみそじなんですか?

(ichinose ) 2015/03/25(水) 06:25


 おはようございます。

 >逆に上記の意味ではなく、Iclass1の構造は簡単な例で Class1メンバ隠蔽が目的ならば、何故Class2の中でインプリメントするのか? という疑問が生まれます。

 >Class1の中でインプリメントを行い、それをClass2に渡せばよい ことになります。
 >この場合だとCallByNameが少し生きてくるかなあ? 稲葉さんのご期待には添えますが、

 私が作成するクラスでは、子クラスが親クラスのIClass1のメンバだけ使えればいいという場面が
 少なく、親そのものを子クラスに渡すという発想しか持っていませんでした。なるほどです!

 mougの方で投稿していたのは、実は

    Private Sub tb_Change()

        ''もしCallByParentがIClass1をImplementsしていたら

        If TypeOf CallByParent Is IClass1 Then 
            ''CallByParent(実体はClass1)をIClass1型にキャスト(型変換)しCallする
            CastIClass1From(CallByParent).RsEv Array(num, tb.Value)
        End If

    End Sub

 というものでした。。書き忘れていました、すみません。

 「Class2にIClass1にキャストしたClass1を渡す」これからやってみます!!
 ありがとうございます。

 # 来月で38ですので、まだ「みそじ」です(笑)
 # 最近は、自分のHNを失敗したな〜と感じております。40歳になったら変えないといけないですね^^;
(みそじのおじさん) 2015/03/25(水) 07:14

 みなさんありがとうございます。
 下記の通り書き換え、(繰り返しになってすみませんが、過去のコードも残しておきたかったので)
 大変勉強になりました。
    Option Explicit
    '●Class1
    Implements IClass1 ''IClass1をImplementsします
    '==================================================
    Public Event tbc(ByVal Index As Long, ByVal TBValue As String)
    Private clsItem() As Class2

    '==================================================
    Private Sub Class_Initialize()
        ReDim clsItem(10)
    End Sub

    '==================================================
    '★Property Setで引数を二つにし、添え字でオブジェクトの代入ができるようにする
    Public Property Set Item(id As Long, tb As MSForms.TextBox)
        If clsItem(id) Is Nothing Then
            Set clsItem(id) = New Class2
            With clsItem(id)
                Set .CallByParent = CastIClass1From(Me)
                .num = id
                Set .Item = tb
            End With
        End If
    End Property

    '==================================================
    Public Property Get Item(id As Long) As MSForms.TextBox
        Set Item = clsItem(id).Item
    End Property

    '==================================================
    '注目するのはImplementsしたプロシージャはPrivateスコープになる事です
    Private Sub IClass1_RsEv(num As Long, str As String)
        RaiseEvent tbc(num, str)
    End Sub

    '==================================================
    '★Class1をIClass1に型変換(Implements)して、Class2に渡す準備
    Private Function CastIClass1From(ByVal obj As Class1) As IClass1
        Set CastIClass1From = obj
    End Function

    '==================================================
    Public Sub Clear()
        Erase clsItem
    End Sub

    '==================================================
    Private Sub Class_Terminate()
        Debug.Print Time & " : Class1解放"
    End Sub

    ''クラス名 Class2
    '●Class2
    Option Explicit
    '==================================================
    Private WithEvents tb As MSForms.TextBox
    Public CallByParent   As Object
    Public num            As Long

    '==================================================
    '★今回の仕様で、RsEvというメソッドしか必要としないならば、
    ' CallByで呼ぶ必要がなく、親クラスにRsEvメソッドを提供すればよい
    Private Sub tb_Change()
      CallByParent.RsEv num, tb.Value
    End Sub

    '==================================================
    Public Property Set Item(ByVal NewItem As MSForms.TextBox)
        Set tb = NewItem
    End Property

    '==================================================
    Public Property Get Item() As MSForms.TextBox
        Set Item = tb
    End Property

    '==================================================
    Private Sub Class_Terminate()
        Debug.Print Time & " : Class2-" & num & "解放"
    End Sub
    Option Explicit

     '●IClass1

     Public Sub RsEv(num As Long, str As String)
     End Sub

 頂いたお返事はすべて読ませていただいておりますが、時間が取れなかったので、
 とりあえずここまでです。
 後ほど、お返事書かせていただきます。

 ※ちなみにみそじは、あと一か月後に迫っている私です。
(稲葉) 2015/03/25(水) 09:29

 すみませーん。<(_ _)>

 途中で一旦解決したので、気に掛かっていた配列についてだけ考えていたのですが、
 未だ、始めから続いていたんですねぇ・・・

 道理で、私には訳が分からない話の展開になるハズです。 退散します。 <(_ _)>

(半平太) 2015/03/25(水) 09:53


 ▲半平太さん
 既に覗かれていないかもしれませんが
 >我々は(私はかも)、いまクラスの設計から自分で考える、との前提で論議が進んでいると認識しております。
 こちらについては、私も「自分で考える」ところがスタートだと思っています。
 なので、脱却→提供されたオブジェクトを使う だと話がかみ合わない感じがしました。

 疑似からの脱却は、胡散臭いというより、提供されるクラスが便利か?という視点で見ると
 自分で必要最低限作ったほうが(少なくとも自分は)メンテナンスしやすいかなと最近思いました。
 汎用クラスを使うほど、大きなプログラム組むことはないですし・・・。

 ▲回答頂いたみなさん
 まず、「こんな簡単なこともわかんねーのかよ ググレカス」と思われたかもしれませんが
 私のようなわからず屋に懇切丁寧にありがとうございました。
 一度片づけたにも関わらず、さらに脱線した内容までご回答いただき、ありがとうございました。

 当初の課題であった独自イベントの運用について、
 具体的に「Do Loopでオブジェクト(に限らずですが)を監視」という概念を教えていただきました。
 私の無知から来た、Propertyの添え字、Imprementsも大変勉強になりました。
 結合度や隠蔽の共存など、私には難しい課題ばかりですが、これからも勉強に励みたいと思います。

 最後になりますが、今回の「独自イベントを活用する」から大幅に外れてしまいますが
 「イベントの共有」に課題を絞った場合
 「最初から作ることが前提で、配列風コントロールをどこまで簡単?にできるか」
 に挑戦してみました。(隠蔽とか関係なく)
 一番良く使うWorkSheetのイベントと、自分自身のTextBoxイベントを代用したら、できるのでは?とやってみて、
 自分なりのメリットとデメリットを以下の通り考えてみました。

 メリット
 共通
 1)フォームとクラスを一つずつでClassをWithEventsしないから、Classを配列としてモジュールレベルで宣言できる
 2)モジュールレベルで宣言できるので、メソッド・プロパティーの候補が出る
 3)RaiseEventのためのメソッドがないので、2)の候補を隠蔽する必要がない
 4)クラスがひとつなので、ユーザーフォームの記述を見れば(自分が)全体を把握しやすい
 5)既存のイベントを代用するので、Classのイベントよりはわかりやすい?

 デメリット
 シートを使ったイベント
 1)Excel以外で使えない
 2)シートを使うので、ブックの保護がされてると使えない

 TextBoxを使ったイベント
 3)ユーザーフォームのメソッドが多く、どれがどんな役割をするかわかりにくい

    【シートのイベント】
    Option Explicit
    '●UserForm1
    'イベントをシートから受け取る
    Private WithEvents WS   As Worksheet
    Private clsClass1(5)  As Class1

    '==================================================
    Private Sub UserForm_Initialize()
        Dim i As Long
        Dim F As String
        Set WS = SetWs
        For i = 0 To 2
            F = "Forms.TextBox.1"
            Set clsClass1(i) = New Class1
            With clsClass1(i).Add(Me.Controls.Add(F, , True), WS, i)
                .Height = 20
                .Width = 100
                .Left = 10
                .Top = .Height * i + 10
            End With
        Next i
    End Sub

    '==================================================
    'コレクションと使ったシートを消す
    Private Sub UserForm_Terminate()
        Erase clsClass1
        Application.DisplayAlerts = False
            WS.Delete
        Application.DisplayAlerts = True
    End Sub

    '==================================================
    'シートのチェンジイベントを受け取って、
    'Rangeの値を添え字にする
    Private Sub WS_Change(ByVal Target As Range)
        With clsClass1(Target.Value).Form
            Debug.Print .Value
        End With
    End Sub

    '==================================================
    'シートの有無を判断して、シートをセットする
    Private Function SetWs() As Worksheet
        Const ShName = "RaiseEventSheet"
        With ThisWorkbook
            On Error Resume Next
                Set SetWs = .Sheets(ShName)
            On Error GoTo 0
            If SetWs Is Nothing Then
                Set SetWs = .Sheets.Add
                SetWs.Name = ShName
            End If
        End With
        SetWs.Visible = xlSheetHidden
    End Function

    Option Explicit
    '==================================================
    '●Class1
    Private WithEvents TB As MSForms.TextBox
    Private WS            As Worksheet
    Private ID            As Long

    '==================================================
    Private Sub Class_Terminate()
        Debug.Print Time & " : Class1 解放"
    End Sub

    '==================================================
    Public Property Get Form() As MSForms.TextBox
        Set Form = TB
    End Property

    '==================================================
    '変数のセットとともに、Addでフォームの参照を返す
    Public Function Add(obj As Object, Sh As Worksheet, Key As Long) As MSForms.TextBox
        ID = Key
        Set WS = Sh
        Set TB = obj
        Set Add = obj
    End Function

    '==================================================
    '指定したシートのRangeに変更を加えることで、SheetのChangeイベントを発生させる
    Private Sub RaiseWorkSheetChange()
        WS.Range("A1").Value = ID
    End Sub

    '//クラスで受け取るイベント//
    '==================================================
    Private Sub TB_Change()
        RaiseWorkSheetChange
    End Sub

    【テキストボックスを使ったイベント】
     Option Explicit
    '◆UserForm2
    'イベントを自身のテキストボックスから受け取る
    Private WithEvents RaiseEvTxB As MSForms.TextBox
    Private clsClass2(5)  As Class2

    '==================================================
    Private Sub RaiseEvTxB_Change()
        RaiseEvTxBCallBack (Split(RaiseEvTxB.Value, "_")(0))
    End Sub

    '==================================================
    Private Sub RaiseEvTxBCallBack(Key As Long)
        With clsClass2(Key).Form
            Debug.Print .Value
        End With
    End Sub

    '==================================================
    Private Sub UserForm_Initialize()
        Dim i As Long
        Dim F As String
        F = "Forms.TextBox.1"
        Set RaiseEvTxB = Me.Controls.Add(F, , False)
            RaiseEvTxB.Enabled = False
        For i = 0 To 2
            Set clsClass2(i) = New Class2
            With clsClass2(i).Add(Me.Controls.Add(F, , True), RaiseEvTxB, i)
                .Height = 20
                .Width = 100
                .Left = 10
                .Top = .Height * i + 10
            End With
        Next i
    End Sub

    '==================================================
    'コレクションを消す
    Private Sub UserForm_Terminate()
        Erase clsClass2
    End Sub

    Option Explicit
    '==================================================
    '◆Class2
    Private WithEvents TB As MSForms.TextBox
    Private CallBackTB    As MSForms.TextBox
    Private WS            As Worksheet
    Private ID            As Long

    '==================================================
    Private Sub Class_Terminate()
        Debug.Print Time & " : Class2 解放"
    End Sub

    '==================================================
    Public Property Get Form() As MSForms.TextBox
        Set Form = TB
    End Property

    '==================================================
    '変数のセットとともに、Addでフォームの参照を返す
    Public Function Add(obj As MSForms.TextBox, TxB As MSForms.TextBox, Key As Long) As MSForms.TextBox
        ID = Key
        Set TB = obj
        Set Add = obj
        Set CallBackTB = TxB
    End Function

    '==================================================
    '指定したテキストボックスのValueに変更を加えることで、TextBoxのChangeイベントを発生させる
    Private Sub RaiseTexboxChange()
        CallBackTB.Value = ID & "_" & TB.Value
    End Sub

    '【クラスで受け取るイベント】
    '==================================================
    Private Sub TB_Change()
        RaiseTexboxChange
    End Sub

 今回のスレッドで一番勉強になったのは、コントロール配列にする方法は別にもあり
 Classの階層化やRaiseイベントは必ずしも必要ないということが勉強になりました。

(稲葉) 2015/03/25(水) 17:55


 色々記述したいことがあるのですが、年度末でとても記述できません。
 一段落したら 再度記述したいと思っていますが、直近の稲葉さんのコードを拝見して。
 >クラスがひとつなので、ユーザーフォームの記述を見れば(自分が)全体を把握しやすい
 ツールとしての体をなしていて、コントロール配列が実現できて ユーザーフォームのコードで全体が把握できるのなら、
 脱却のプログラム構造にこだわっているわけではありません。

 又、これを目的に色々考えることは良いことだと思います。

 まず、ClassをWithEventsしないことの重要性がはっきりわからなかったのですが・・・。
 配列は、現状の Cls.Item(1)このしくみで実現できますし、Itemが気になるなら、
 既定のプロパティをの手法を模索する方法もあります。

 と思っていましたが、私がこういうクラスを作る場合、たいていアドイン化して別ブックで運用します。
 Witheventsを宣言する場合、アドインブックを参照設定しなければなりません。
 そうすると、ユーザーフォームをモードレスで運用しようと思うと、例の変数の初期化のことが気になるので変数をモジュールレベルで扱うことが問題になります。

 この観点から 本当にClassをWithEventsしない が実現できれば 参照設定が要らないので良いと思いますが。

 拝見したどちらのコードについても気になったこと。

 イベントがChangeのときは よいですが、KeydownやMousedownのときは、どうするのですか?

 教えてください。

( ichinose ) 2015/03/27(金) 07:17


 >参照設定が要らないので良いと思います
  Private clsClass2(5)  As Class2
 userformに配列宣言しているのでいずれにせよ 無理ですね!!

 でも、参考のために

  >KeydownやMousedownのときは、どうするのですか?

 教えてください
( ichinose ) 2015/03/27(金) 07:26

 >色々記述したいことがあるのですが、年度末でとても記述できません。
 お忙しいところ、お付き合い頂いてありがとうございます。
 気長にお待ちしております。

 >KeydownやMousedownのときは、どうするのですか?
 単純にクラスモジュールに
 こうすればできると思うんですが、聞かれていることが違うのでしょうか。。。?

    Private Sub TB_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
        RaiseTexboxChange
    End Sub

    Private Sub TB_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
        RaiseTexboxChange
    End Sub

 疑似からの脱却で(§9〜10)
 >配列宣言とWithEvents宣言が共存できない
 >(クラスで)『イベント発生』 と 『配列化』 を役割分担させましょう

 『イベントの発生』『配列化』をクラスで行うのではなく、ユーザーフォーム(シート)で行わせるという
 発想ってだけでした。

 アドイン化等は考えておりません。(揮発的なものではありませんが、1つのブック単体で動かして、
 単一のイベント以外必要としないときに、0から作るならこうしてみよう、という実験的な意味合いで
 投稿致しました。)
(稲葉) 2015/03/27(金) 09:13

 コードを確認いたしました。

 シートを使ったものの方が完成度は高いですね!! テキストボックスのそれは、現状だと
 忠実に元のテキストボックスのイベントが再現されていませんでした。

 テキストボックスを仲介に使用したChangeイベントの場合、

 ・ユーザーフォームが立ち上がった時点で a と入力します。ここでは、イベントが発生します。
 ・←キーで 文字 a の手前にカーソルを移します。
 ・キーのモードを挿入モードから 上書きモードにしてください。
 ・ここで上書きするように a と入力してください  イベントが発生しません。
 ・TextBox Changeイベントでは この動作で イベントは発生しています。

 Changeイベントでこの操作でイベントの発生の有無は大したことではありませんが、
 この動作でイベントが発生しないことが 他のイベントを考えたときにいくつかの不具合を生みます。

 KeydownやMousudownは、イベントにパラメータがあるのでどうされるのかなあ と思い質問しました。

 又、KeyDownイベントの場合、フォーカスを移す場合、ユーザーフォーム側からKeycodeを操作する必要があります。

 これは、どうされるつもりなのだろう とも思いました。これらは、クラスのプロパティとして
 管理すれば何とかなりそうですが、この時TextBoxを仲介した場合は、前述のイベントの発生有無が問題になります。一度、考えてみてください。

 >脱却→提供されたオブジェクトを使う
 こうは、言っていません。コントロール配列を実現すためにオブジェクトを使うなら、オブジェクト指向
 の本筋の構造だと言えるのでこのような構造にしてみては? という意見です。

 作ってしまえばコントロール配列を使いたいユーザーフォームの仕様がいくつ出てきても、
 インタフェースは、簡単だし、ユーザーフォーム内でコードが管理できます。再利用可能になるので
 完成度も使えば使うほど 上がります。

 脱却オブジェクトを元に私が考えたというより、ロジックの大筋は、同じですが、

 class1のモジュール

 '==============================================================================
 Option Explicit
 Dim col As Collection
 Event change(ByVal id As Long)
 Event keydown(ByVal id As Long, ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
 Event BeforeDragOver(ByVal id As Long, ByVal Cancel As MSForms.ReturnBoolean, ByVal Data As MSForms.DataObject, ByVal X As Single, _
                     ByVal Y As Single, ByVal DragState As MSForms.fmDragState, ByVal Effect As MSForms.ReturnEffect, ByVal Shift As Integer)
 Event BeforeDropOrPaste(ByVal id As Long, ByVal Cancel As MSForms.ReturnBoolean, ByVal Action As MSForms.fmAction, ByVal Data As MSForms.DataObject, ByVal X As Single, ByVal Y As Single, _
                        ByVal Effect As MSForms.ReturnEffect, ByVal Shift As Integer)
 Event DblClick(ByVal id As Long, ByVal Cancel As MSForms.ReturnBoolean)
 Event DropButtonClick(ByVal id As Long)
 Event KeyPress(ByVal id As Long, ByVal KeyAscii As MSForms.ReturnInteger)
 Event KeyUp(ByVal id As Long, ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
 Event MouseDown(ByVal id As Long, ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
 Event MouseMove(ByVal id As Long, ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
 Event MouseUp(ByVal id As Long, ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
 Sub add(obj As Object)
    Dim cls2 As Class2
    Set cls2 = New Class2
    With cls2
       Set .obj = obj
       Set .parent = Me
       .method = "callback"
       .id = col.Count + 1
    End With
    col.add cls2
 End Sub
 Private Sub Class_Initialize()
    Set col = New Collection
 End Sub
 Property Get item(id As Long) As Object
    Set item = col(id).obj
 End Property
 Sub callback(ParamArray evary() As Variant)
    Select Case evary(0)
       Case 1
         RaiseEvent change(evary(1))
       Case 2
         RaiseEvent keydown(evary(1), evary(2), evary(3))
       Case 3
         RaiseEvent BeforeDragOver(evary(1), evary(2), evary(3), evary(4), evary(5), evary(6), evary(7), evary(8))
       Case 4
         RaiseEvent BeforeDropOrPaste(evary(1), evary(2), evary(3), evary(4), evary(5), evary(6), evary(7), evary(8))
       Case 5
         RaiseEvent DblClick(evary(1), evary(2))
       Case 6
         RaiseEvent DropButtonClick(evary(1))
       Case 7
         RaiseEvent KeyPress(evary(1), evary(2))
       Case 8
         RaiseEvent KeyUp(evary(1), evary(2), evary(3))
       Case 9
         RaiseEvent MouseDown(evary(1), evary(2), evary(3), evary(4), evary(5))
       Case 10
         RaiseEvent MouseMove(evary(1), evary(2), evary(3), evary(4), evary(5))
       Case 11
         RaiseEvent MouseUp(evary(1), evary(2), evary(3), evary(4), evary(5))
    End Select
 End Sub
 Sub clear()
    Dim g0 As Long
    For g0 = col.Count To 1 Step -1
       Set col(g0).obj = Nothing
       col.Remove g0
    Next
 End Sub

 Class2のモジュール

 Option Explicit
 Public WithEvents obj As MSForms.TextBox
 Public id As Long
 Public parent As Object
 Public method As String
 Private Sub obj_Change()
    CallByName parent, method, VbMethod, 1, id
 End Sub
 Private Sub obj_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
    CallByName parent, method, VbMethod, 2, id, KeyCode, Shift
 End Sub
 Private Sub obj_BeforeDragOver(ByVal Cancel As MSForms.ReturnBoolean, ByVal Data As MSForms.DataObject, ByVal X As Single, ByVal Y As Single, ByVal DragState As MSForms.fmDragState, ByVal Effect As MSForms.ReturnEffect, ByVal Shift As Integer)
     CallByName parent, method, VbMethod, 3, id, Cancel, Data, X, Y, DragState, Effect, Shift
 End Sub
 Private Sub obj_BeforeDropOrPaste(ByVal Cancel As MSForms.ReturnBoolean, ByVal Action As MSForms.fmAction, ByVal Data As MSForms.DataObject, ByVal X As Single, ByVal Y As Single, ByVal Effect As MSForms.ReturnEffect, ByVal Shift As Integer)
     CallByName parent, method, VbMethod, 4, id, Cancel, Action, Data, X, Y, Effect, Shift
 End Sub
 Private Sub obj_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
    CallByName parent, method, VbMethod, 5, id, Cancel
 End Sub
 Private Sub obj_DropButtonClick()
        CallByName parent, method, VbMethod, 6, id
 End Sub
 Private Sub obj_KeyPress(ByVal KeyAscii As MSForms.ReturnInteger)
    CallByName parent, method, VbMethod, 7, id, KeyAscii

 End Sub
 Private Sub obj_KeyUp(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
    CallByName parent, method, VbMethod, 8, id, KeyCode, Shift
 End Sub
 Private Sub obj_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
     CallByName parent, method, VbMethod, 9, id, Button, Shift, X, Y
 End Sub
 Private Sub obj_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
     CallByName parent, method, VbMethod, 10, id, Button, Shift, X, Y
 End Sub
 Private Sub obj_MouseUp(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
     CallByName parent, method, VbMethod, 11, id, Button, Shift, X, Y
 End Sub

 理屈さえ理解すれば、後は、難しいというより 一見、コードは長いですが、力仕事です。

 これで Textboxのおおよそのイベントが共有できます。
 enter exit のイベントはありませんが、これも今は出来るみたいですよ!!

 ユーザーフォーム側では、

 Userform1に テキストボックスを4つ(Textbox1〜Textbox4)を用意してください。
 以下の例は、

 changeイベントで数字にカンマを付ける編集を行い、
 KeydownイベントでEnterキーで 数字のチェック行い、数字でなかたっら再入力を促し、
 DblClickイベントで対象テキストボックスの内容をメッセージボックスで表示しています。

 Option Explicit
 Private WithEvents txtary As Class1
 Private Sub txtary_change(ByVal id As Long)
    Dim txt As MSForms.TextBox
    Set txt = txtary.item(id)
    With txt
       .Value = Format(.Value, "#,##")
    End With
 End Sub
 Private Sub txtary_DblClick(ByVal id As Long, ByVal Cancel As MSForms.ReturnBoolean)
    Dim txt As MSForms.TextBox
    Set txt = txtary.item(id)
    MsgBox txt.Text
    Cancel = True
 End Sub
 Private Sub txtary_keydown(ByVal id As Long, ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
    Dim txt As MSForms.TextBox
    Set txt = txtary.item(id)
    If KeyCode = 13 Then
       If Not IsNumeric(txt.Value) Then
          MsgBox "数字を指定"
          KeyCode = 0
       End If
    End If
 End Sub
 Private Sub UserForm_Initialize()
    Dim g0 As Long
    Set txtary = New Class1
    With txtary
       For g0 = 1 To 4
           .add Controls("textbox" & g0)
       Next
    End With
 End Sub
 Private Sub UserForm_Terminate()
    txtary.clear
    Set txtary = Nothing
 End Sub

 txtary.item(id)としていますが、

http://d.hatena.ne.jp/cartooh/20090718/1247926819

 このサイトの例に倣ってitemプロパティに記述し、操作し、itemを既定のプロパティにすれば、

 txtary(id) と記述できます。

 私も脱却オブジェクトをそのまま使っているわけではありませんし、自分で完成させてもいません。

 Class2は、Textboxのイベントです。同じようなコードでラベルやコマンドボタンは、作成してあります。

 コントロール配列で扱いたいコントロールが増える度に Calss2と同じようなクラスがコントロール数だけ
 増えていきます。コードが蓄積されますから、次回にラベルでイベントの共有を実現したい時は、
 簡単に実現できます。

 家を建てて 毎月ローンを払うのと 賃貸で家賃を払う 払うのは同じでも将来自分のものになるから
 思い切って家を買う感覚に似ているかなあ・・・。

 私が 脱却オブジェクトそのものを使わないのは、もし、バグを発見した場合、すぐに直してもらえるか心配だったこともあります。これでも報酬はいただいているので・・。
 もっともMS社のバグよりは、報告すれば迅速に動いてくれると思います。が、タダでもらっているのに
 早くしてとは言えませんから。

 幸い、

http://www.h3.dion.ne.jp/~sakatsu/Bpca_Common.htm

 Ver 2.0では、ソースが公開されています。これだと仮にバグがあっても急場は自分で対処も可能です。

 本家脱却は、クラスモジュールを統括管理するクラスと 実際のイベントを発生させる二つのクラスだけで
 成り立っています。

 私は、前述のようにコントロール毎にイベントを発生させるクラスは分けていこうと思っています。

 まだ 具体的なことは考えていませんが、近い将来はイベントを発生させるクラス側をインプリメントを使ってインターフェースを共有化させることは考えています。

 脱却のサイトを紹介した時点では、脱却の理論を理解するのが難しいと思います。
 だったら、これを使ってみれば、便利なところたくさんあるはずです。

 その便利なところに気がつけば、クラスの作成はこうすればよいのか

 と思って頂ければよいなあ という思いから、脱却のサイトを何度もご紹介してきました
 又、これからもご紹介していくつもりです。

( ichinose ) 2015/03/29(日) 07:27


 >規模の大きいプログラムを作成してくると こういう考え方も必要になってくると思います。

 と、前述していますが、これは、構造化プログラミングが大きいプログラムを作成するのに
 どうやったら わかりやすいプログラムが作れるのか? というテーマから出てきた手法のなので
 このような記述になりました。提唱したたのは、ノストラダムス??、もとい ダイクストラですが、

 その後、これについて構造化プログラミングするためには このようにすればよい
 という人が何人も現れました。その中の一つに 複合/構造化設計というものがありました。

 この中に何度か記述しましたモジュールの強度/結合度という指標が出てきています。
 情報的強度と命名された形式でモジュールを作成すると、 強度/結合度という指標を満たしたものになると書かれています。この考え方を受け継いだものがオブジェクト指向プログラミングです。

 機能が独立していて、外界とは、パラメータだけで繋がっている よって、再利用しやすくなる。

 よって、クラスはこの考え方を受け継いで作成されると 良いものになる という考えです。

 これは、規模の大きいプログラムだけというより、このクラスを使う機会がどれだけあるのかということは
 オブジェクトを作成するときの迷う点ではあります。

 >>>われわれは、このインターフェースを理解すれば、
 >>>フォームモジュール内で処理が管理できますから VBAプログラマがわかりやすいコードが書けます。
 >>> 2段階方式の長いコードは 一回作れば 再利用できますし、そもそも隠蔽化されていますから、
 >>> インターフェースと機能だけ理解すればよいですよね。

 >>我々は(私はかも)、いまクラスの設計から自分で考える、との前提で論議が進んでいると認識しております。
 >こちらについては、私も「自分で考える」ところがスタートだと思っています。
 >なので、脱却→提供されたオブジェクトを使う だと話がかみ合わない感じがしました。

 この「われわれ」には、作成した人も ただこれを利用する人も含まれています。
 作った人は、脱却(ユーザーフォームモジュール内に共有イベントプロシジャーを実現する)オブジェクトの
 メンテナンスはあります。
 汎用化されたものは、そうでないものに比べると大変だと思います。

 私が申し上げたいのは これを作ってしまえば、個々のユーザーフォームで
 コントロール配列を実現するのが疑似より容易になります。
 それは、作った人も利用するだけの人も同じです。

 疑似オブジェクトは、ユーザーフォームを作成する毎に
 ユーザーフォームのモジュールと疑似クラスと二つをメンテしなければなりません。
 全部のコードをクラスで行うなんて方法もあるかもしれませんが(ユーザーフォームはモジュールコードなし)、使えないイベントがでたりそれを克服する処理が大変だったりする可能性もでてきます。

 普通に想像すると そのユーザーフォーム毎に疑似の構築とそのユーザーフォーム専用のコード処理を行うクラスが付いてきます。
 ユーザーフォームのモジュールと疑似クラスは、作成する仕様によって中身のコードは違ってきます。
 作成時、一発でプログラムが作動することは殆どないですよね、
 この時、プログラマは、二つのモジュールを精査しながらプログラムを修正していかなけばなりません。

 「脱却」オブジェクトを利用すれば、ユーザーフォームのモジュールだけを精査することになります。
 ここで疑似と同じように、脱却オブジェクトの中身までユーザーフォームを作成する度に気にしなくては
いけないのであれば、もはや脱却オブジェクトは、再利用可能なオブジェクトでは、ありません。

 コントロール配列を使いたいユーザーフォームがこの「脱却」オブジェクトを利用していくのですから、完成度は、増していきます。この観点からもこの構造にする意味はあります。
 コントトロール配列を形成する という機能を持ったオブジェクト
 こういう形式でオブジェクトを作成することでオブジェクトを利用する(呼び出す)だけでやりたいことが
実現できる。複数のプログラムから簡単利用できる便利性、本来のオブジェクトの姿でないでしょうか?

 この掲示板でユーザフォームのコントロールのイベントの共有化の質問は何度もありました。
 その時、「疑似からの脱却」HPをご紹介してきたのは、疑似コントロール配列で質問者のやりたいことは
 とりあえず実現はできますが、何故、それではよくない理由や発展したものなどの説明があり、
 イベントの共有化の実現ができたのに何故 これでは問題があるのか?
 ということを考える良い素材だと思ったからです。

 冒頭に記述したように 脱却オブジェクトに費やす労力と この先にそれを利用する(コントロール配列を実現するユーザーフォーム)プログラムの数とを秤に掛けるなら(プログラムの規模の大きい 小さいではありません)、
 角田さんの既製品「脱却」オブジェクトを利用すことをお奨めします。

 実現したいのは、個々のプログラムでユーザーフォームのモジュール内だけ一元管理できるイベントの共有化です。

 これが出来れば わかりやすいプログラム構造になるからです。

( ichinose) 2015/03/29(日) 08:35


 お忙しいところ、ありがとうございます。
 お伝えしたいことが私がしっかり受け止められているか? 不安ではありますが
 御指摘を受けた点に関して、お返事致します。

 >シートを使ったものの方が完成度は高いですね!! テキストボックスのそれは、現状だと
 >忠実に元のテキストボックスのイベントが再現されていませんでした。
 事象、確認致しました。
 WorksheetChangeイベントからTextBoxでも代替できるのでは?と考えたときに
 テストして動かないときがある → 値が変わらないとき でどうしようか悩んでいました。
 たぶん、NowやTimerで値を無理やり書き換えれば動くかなと思いますが、
 指摘されたKeyDownイベント等の問題を考えたとき、値の取得はできても、
 KeyCode、Cancel等書き換えできないので考え直しました。
 浅はかで、申し訳ないです。

 頂いたコードも確認致しました。
 >こういう形式でオブジェクトを作成することでオブジェクトを利用する(呼び出す)だけでやりたいことが
 >実現できる。複数のプログラムから簡単利用できる便利性、本来のオブジェクトの姿でないでしょうか?
 こちらも、その通りだと思います。

 何一つその通りだと思っているのですが、
 この隠蔽(脱却してコードを作る人)と、
 コードを読んで学習する(もらったコードを見て、仕組みを理解しようとする)

 というテーマを実現しようとすると、

 前者は脱却オブジェクトを使うと便利であるのに対し
 後者は必要最低限のコードだけあれば、学習効率が良い

 私は後者の立場だと思っているので、冒頭に記述したコードのURLでichinoseさんが投稿されているコード
 が教科書でした。
 習うより慣れろが私の勉強方針なので、とてもありがたいスレでした。
 正直、疑似からの脱却だけ読んでも理解できない人が多いのかなとも思いました。(私がバカなだけ?)

 なのでオブジェクトは再利用可能なものである、という点も理解しつつ、
 最初から簡単に作るにはどうすればいいのか(つまり自分で書かないと忘れる!!!)
 というテーマも持ちたかったのです。

 話がそれてしまいましたが、疑似オブジェクトのだめな点も、オブジェクトの本来の姿についても
 ちゃんと伝わっております。

 今後の勉強方針として、脱却オブジェクトをそのまま利用せず、自分でコツコツ再利用可能なオブジェクト
 を作って、使うたびにブラッシュアップしていくことを目的に勉強したいと思います。

 みなさま、長々と、ありがとうございました。

 >もっともMS社のバグよりは、報告すれば迅速に動いてくれると思います。が、タダでもらっているのに
 >早くしてとは言えませんから。
 不思議ですね! MS社にはお金も払っているのに・・・。
 もとはと言えばVBAでコントロール配列が使えれば、こんな回りくどいことしないで済むんですけどね!

(稲葉) 2015/03/30(月) 10:44


 終わった内容ではございますが・・・
 ちょっと躓いているのでまた教えてください。

 >Class2は、Textboxのイベントです。同じようなコードでラベルやコマンドボタンは、作成してあります。
 >コントロール配列で扱いたいコントロールが増える度に Calss2と同じようなクラスがコントロール数だけ
 >増えていきます。コードが蓄積されますから、次回にラベルでイベントの共有を実現したい時は、
 >簡単に実現できます。

 の部分で、Class2に当たる各コントロールのクラスをテストでいくつか作りました。
 仮にテキストボックスとラベルとして、
    Select Case TypeName(obj)
        Case "TextBox":       Set clsObj = New Class2 'テキストボックス
        Case "Label":         Set clsObj = New Class3 'ラベル
        Case Else:            MsgBox TypeName(obj) & "は対応していないオブジェクトです。"
    End Select
 大本でこのように分岐させたとき、ラベルしか使わないブックでは、Class2を置きたくないので
 テキストボックスのところでコンパイルエラーになります。

 コンパイルエラーが出ない方法として、条件付きコンパイルを試そうとしましたが、その条件の付け方
 がわかりません。
 この場合、諦めてどちらのクラスも取り込むか、あるいはクラスを使うときに自分で書き換える必要が
 あると思うのですが、

 そのブックにあるクラスを判断して、条件付きコンパイルできる方法はあるのでしょうか?

(稲葉) 2015/04/15(水) 12:57


 条件付コンパイルですか・・・?

 私なら

 1.Case "TextBox":       Set clsObj = New Class2 'テキストボックス これそのものを削除。
 2.ダミーでからっぽの Class2モジュールを挿入しておく。

 いずれかでやりますね。

(β) 2015/04/15(水) 14:04


 クラスの目的、利点を「隠蔽」とした場合、
 クラスの利用者には必要なクラスを準備して実行すれば、
 動的に各コントロールのイベントが実装されるようにしたいのです。

 現状2つですが、3つ、4つと増えると、それだけコードを変更する必要が出てきますので
 それをどうにかこうにか動的に分岐できればいいなと考えていました。

 すべてのクラスを置いたブックをアドイン、参照設定、または非表示で開いておき
 ブック名でクラスを指定する方法も考えましたが、できれば1ブックで完結させたいと考えています。

 わがままですかね・・・

(稲葉) 2015/04/15(水) 14:32


 このスレ自体を順を追って精読しているわけではないので、とんちんかんなレスになっているのでしょね。

 そもそもが、アプリケーションブックがあって、そこでつかう汎用的なクラスモジュールをアプリ開発者がインポートするんですよね。

    Select Case TypeName(obj)
        Case "TextBox":       Set clsObj = New Class2 'テキストボックス
        Case "Label":         Set clsObj = New Class3 'ラベル
        Case Else:            MsgBox TypeName(obj) & "は対応していないオブジェクトです。"
    End Select

 このコードは、どこに書かれているんでしょうか。
 アプリ部分にあるのでしょうか。それとも、たとえば Class1 というものがあって、そこに書かれているんでしょうか。
 Class1 といったものに書かれているとすれば、アプリ開発者は、Class1をインポートするんでしょうね。
 で、今回のアプリは、これこれだから、Class3はインポートするけど、Class2はインポートしない。
 そう考えて、インポートする、しないを区別しているんでしょうか?

 もし、そうなら、その時点で、アプリ開発者にとって Class1 の中のロジックが丸見えなんですよね。
 自分のアプリでは、Class1が相手にしているこれとこれは必要だけど、これとこれは不要と。
 Class1で相手にしているクラスが Class2 や Class3 といった名前まで把握しているんですね。
 それって、隠蔽になっているんでしょうか?

 隠蔽って、単に、インテリセンスででてくるでてこない、参照できるできないということだけではなく
 その実装がどうなっているかを把握し、その実装要件によって、これは必要、これは不要と、そう判断できる時点で
 破たんしているのでは?

 まぁ、このあたりは全くの素人ですので、スルーしてもらってOKです。

(β) 2015/04/15(水) 15:18


 情報の出しかたが中途半端でした。
 お察しの通りで

 ichinoseさんの 2015/03/29(日) 07:27 の投稿で頂いたコードの内、
 Class1モジュールの
 Sub add(obj As Object)メソッドの中に記載するとお考えください。

 >そう考えて、インポートする、しないを区別しているんでしょうか?
 ユーザーフォームでラベルとテキストボックスを使い
 そのうちラベルだけを配列(イベント)を扱いたいと考えたときに
 例ではClass3ですが、clsLabel等のオブジェクト名に変更すれば、少なくとも区別はつくと考えています。

 ロジックが見えていようがいまいが、そのオブジェクトの使い方が分かり、書き換える必要がなければ
 隠蔽と呼べると私は考えていました。
 βさんのおっしゃる通りでしたら、すみません。

 話を戻しますと、このたび改めて質問させていただいた内容は、結合度の問題になるかと思います。

 利用者ができるだけ少ない記述の変更(または不必要)で再利用可能なオブジェクトを模索した場合、
 先に述べた2件の考えが間違っているのでしょうか?

 βさんのおっしゃることももっともだと思うのですが、なんか腑に落ちないのです。

(稲葉) 2015/04/15(水) 16:41


 >ロジックが見えていようがいまいが、そのオブジェクトの使い方が分かり、書き換える必要がなければ
 >隠蔽と呼べると私は考えていました。

 今までの流れをほとんど追いかけないままのレスになっていたようですね。失礼しました。

 公開情報として、clsAll(親)はかならず準備してください、加えて、Label処理があるなら clsLabelを用意してください。
 こういった仕様公開があるという前提なら問題ないですよね。
 (ここのところを誤解していたようです。つまり、利用者が自分で判断すると考えてしまいました)

 ところで、ロジックが見えていても、その見えている部分を前提にしたコードを書かなければ、十分に隠蔽だと、私もそう理解していますよ。
 たとえば、MS提供の様々なオブジェクト、これは中が見えないことになっています。でも、MSの開発者たちには
 実は中が見えているんですよね。
 でも、その実装がこうだからといって、そこに手を伸ばした処理はせず、あくまで公開されているメソッドやプロパティを相手にしますよね。

 VBAでのクラスで悩ましいのは、「静的に事前にブックに取り込んでおかなければいけない」ところなんでしょうね。
 親クラスが動的に必要な子クラスを必要に応じて実行時バインディングのような形で利用できればいいんですけどね。

 でも、そんな宿命なんでしょうから、稲葉さんが避けたいとおっしゃる、フルセットのブックを参照設定して
 そのブックに対してクラスハンドリング要求を出す。私なら、そうやると思います。
 (テーマは異なるんですが、アドインは、私個人は、このようなケースではやりません。
 これらクラスに関係のない、単なるエクセル作業でも呼び出されているというのが好きじゃないので)

(β) 2015/04/15(水) 17:56


 私の説明が悪かっただけです。
 遠回りさせてしまい申し訳ございません。
 > こういった仕様公開があるという前提なら問題ないですよね。
 最初からこのように説明すればよかったですね!!

 > でも、そんな宿命なんでしょうから、稲葉さんが避けたいとおっしゃる、フルセットのブックを参照設定して
 そうなんですよね・・・
 なので、条件付きコンパイルでできるだけ変更箇所が少なくなるように、クラスモジュールの頭に

 '使用するコントロールのみTrueにセットしてください
 #Const cText = False
 #Const cLabel = True

 '以下、Newの部分の抜粋
 Select Case TypeName(obj)
     Case "TextBox"
         #If cText = True Then
             set clsObj = New Class2
         #End If
     Case "TextBox"
         #If cLabel = True Then
             set clsObj = New Class3
         #End If
 End Select

 として、お茶を濁しました。
 ブックの参照設定は、やっぱりネットワークドライブ上に置くのでできるだけ避けたいというのが現状です。

 >十分に隠蔽だと、私もそう理解していますよ。
 こちらも誤解していました。
 言葉尻を捉えて、「丸見え」のみ受け取っておりました。
 申し訳ございません。

 他に、何か良い手があればいいのですが・・・
(稲葉) 2015/04/15(水) 18:18

 >クラスの目的、利点を「隠蔽」とした場合、
 >クラスの利用者には必要なクラスを準備して実行すれば、
 >動的に各コントロールのイベントが実装されるようにしたいのです。
 >現状2つですが、3つ、4つと増えると、それだけコードを変更する必要が出てきますので
 >それをどうにかこうにか動的に分岐できればいいなと考えていました。

 コントロール配列を実現する場合になるべく 利用プログラマが簡単な手順 ということですよね!!
 私は、もうアドイン化するが一番簡単だと思いますけどねえ!!
 同じブックにクラスをインポートという発想だとどうしてもClass1と今回必要なコントロールを管理する
 Class2、Class3・・・もインポートするという発想になります。この場合に必要最小限の
 コントロールに関するクラスだけインポートしたいという発想になってしまいます。

 アドイン化して プロジェクトとして捉えた場合、脱却プロジェクトそのものが隠蔽化されていることになります。

 中身なんか知らなくても 脱却プロジェクトを参照設定すれば 使用可能になります。
 この時、Class2以下の事なんて ユーザーは考える必要はないですよね!!
 そもそもユーザーから見えるのは Class1だけですしね(そういう設定はできます)!!

 アドイン化すれば要らないクラスも抱えることには なりますが、要らなければ
 インスタンスが作成されませんから、リソースは消費されませんよね!!

 今回の脱却プロジェクトは、どうしても参照設定が必要ですが、結構アドイン化してあるクラス、私は多いですよ。

 例の右クリックで 切取、コピー 貼付けのメニューもアドイン化してあります。

 これの場合、クラス側に全部 任しているので参照設定もいらず、ただインスタンスを作成して
 使用するコントロールを登録するだけにしています。

 拝見したコードは 脱却オブジェクトの開発途中の施しとしては、興味深いものがあります。

 Class1の変更をフラグだけで制御できますからね!!

 ただ、アドイン化のこと 考えてみてください。

(ichinose) 2015/04/16(木) 05:55


 やっぱりアドイン化、ですよね。

 βさんの言葉を素直に受け取らなかったわけではないですが、(大変失礼で申し訳ございません)
 ichinoseさんにも言われると、アドイン化以外ないんじゃないかと・・・
 そこで、アドイン化したときに、現在の状態でどのような不具合が有るのか
 検証してまいりました。

 質問当初は例え話でしたが、カレンダーフォームを手作りして、他の場面でも使いたいと考えて
 いました。(Officeのバージョンが変わると、現行のカレンダーコントロールが使えなくなると聞いて・・)

 どうせアドインにするなら、ユーザーフォームもアドインから呼び出そうと思って試行錯誤しておりましたが
 また自力解決ができそうにないので、教えてください。

 【問題点1】
 カレンダーフォームの仕様は、ラベルを日付分並べて、クリックしたラベルの日付を
 フォームのイベントとして出力し、シートモジュールで受け取る仕様です。
 要は、カレンダーフォームでは値の受け渡しだけ行い、制御をシートモジュールで行いたいと考えています。

 この、「フォーム→シート」のところで、躓きました。

 以下のコードは、イベントの制御部分をわかりやすくまとめたものです。
 ユーザーフォームにコマンドボタンを一つ配置します。

    'UserForm1
    Option Explicit
    Public Event Click(ByVal msg As String)
    Public Event Term()
    Private Sub CommandButton1_Click()
        RaiseEvent Click(CommandButton1.Caption)
    End Sub
    Private Sub UserForm_Terminate()
        RaiseEvent Term
    End Sub

    'シートモジュール
    Option Explicit
    Private WithEvents uf As UserForm1
    Const CELL_ADR = "$A$1"
    Private Sub uf_Click(ByVal msg As String)
        If Selection.Address = CELL_ADR Then
            MsgBox msg
        End If
        Unload uf
        Set uf = Nothing
    End Sub
    Private Sub uf_Term()
        Unload uf
        Set uf = Nothing
    End Sub
    Private Sub Worksheet_SelectionChange(ByVal Target As Range)
        If Target.Address = CELL_ADR Then
            If chkUF = False Then
                Set uf = UserForm1 '★
                uf.Show vbModeless
            End If
        Else
            If chkUF = True Then
                Unload uf
                Set uf = Nothing
            End If
        End If
    End Sub
    Private Function chkUF()
        Dim f
        For Each f In UserForms
            If f Is UserForm1 Then
                chkUF = True
            End If
        Next f
    End Function

 上記コードを同じプロジェクト内で実行すると、問題なくできました。

 次に、標準モジュールに
http://www.vbalab.net/vbaqa/c-board.cgi?cmd=ntr;tree=8153;id=excel
 こちらを参考にしながら、

    '標準モジュール
    Option Explicit
    Function mk_uf() As UserForm1
        Set mk_uf = UserForm1
    End Function
 とし、ユーザーフォームアドインテスト.xlamとして保存します。

 新しいブックを立ち上げ、参照設定でユーザーフォームアドインテスト.xlamを参照し、
 シートモジュールの★の部分を
 Set uf = mk_uf
 このように書き換えればできるはず・・・

 だったのですが、
 WithEvents uf As UserForm1
 コンパイルエラー ユーザ定義型は定義されていません
 になってしまいます。

 ユーザーフォームにもクラスモジュールのinstancingに当たるプロパティがあるのか調べたのですが、
 見つからず・・・
 回避策として、テストしていませんが、カレンダーフォーム用のクラスを準備して、クラスからイベント
 を発生させるなんてことも考えています。

 【問題点2】
 複数人が使う読み取り専用ブックに参照設定させて、そのブックをコピーしてデータを蓄積しています。
 参照設定されていると、アドインブックが参照されなくなるまで(誰かが開いていると)変更できません。

 Workbook_BeforeCloseイベントでアドインのクローズを試みましたが、参照設定されている間は閉じられ
 ないと叱られました。

 ググってみたところ、
http://www.asahi-net.or.jp/~ef2o-inue/haifu/sub06_030_04.html
 こちらがヒットしましたが、参照設定まで考慮されていないようで使えず・・・

 思い付いたのが、このスレで紹介していただいた、
[[20141218082047]] 
 こちらの、DoLoopとSetTimerで監視して、イベントをRaiseする仕組みを応用して
 アドインの中に「呼び先のブックが閉じられることを監視して、閉じられたら自分を閉じる」というロジックを作り、
 呼び元のBeforeCloseで呼び出す、というアイデアです。

 それを試す前に、参照設定されていても閉じる方法があるんじゃないかと思い質問させていただきました。

 一度終わったのに、また長々と申し訳ございませんが、ご教授お願い致します。

(稲葉) 2015/04/16(木) 13:30


 【問題点2】の自分なりの解答です。
 ◆とりあえず試したこと
 ・参照元のブックを保存した後、参照設定を動的に解除し、アドイン→保存しないで参照元ブックを閉じる
  →セキュリティで参照設定変更できなかった!!

 ・参照元のBeforeCloseをフックして、参照元が閉じられるまでDo〜Loopで監視してから閉じる
  →BeforeCloseの処理が終わらず、無限ループした!!

 ◆なんとなくできたこと
 概要
 呼び元のブックを、アドインのクラスに登録し、BeforeCloseイベントをフックし
 OnTimeで参照先のイベントを先に終了させ(ブックを閉じて)
 アドインのブックを閉じる

 '参照元のブック
    Option Explicit
    '============================================================
    '参照元のThisWorkBookイベント
    Private Sub Workbook_Open()
    '============================================================
        Closing ThisWorkbook
    End Sub

 '以下アドインブック
    Option Explicit
    '標準モジュール
    '============================================================
    'clsClosingをコレクションに登録する
    Private col As Collection

    '============================================================
    '参照先のブックのOpenイベントで呼ばせ、クラスのインスタンスを作る
    Sub Closing(ByRef WB As Workbook) '
    '============================================================
        Dim clsC As clsClosing
        If col Is Nothing Then
            Set col = New Collection
        End If
        col.Add New clsClosing
        Set clsC = col(col.Count)
        Set clsC.CallBook(col.Count) = WB
    End Sub

    '============================================================
    'OnTimeで呼び出すコールバック用メソッド indexで呼び出したクラスを呼び直す
    '(クラスに戻す必要はないかも?)
    Sub CallBackClose(index As Long)
    '============================================================
        Dim clsC As clsClosing
        Set clsC = col(index)
        clsC.LateClose
    End Sub

    Option Explicit
    'clsClosing
    Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
    Private WithEvents eWB As Workbook
    Private looping        As Boolean
    Private index          As Long
    Const MAX_RETRY        As Long = 50
    Const RETIME           As Long = 100

    '============================================================
    'クラスにブックを登録し、ブックごとのBeforeCloseイベントをフックする
    Public Property Set CallBook(ByVal iD As Long, ByRef WB As Workbook)
    '============================================================
        Set eWB = WB
        index = iD
    End Property

    '============================================================
    '登録したブックのBeforeCloseイベントをフックする
    '閉じるときに、OnTimeメソッドで、呼び元を先に閉じさせる
    Private Sub eWB_BeforeClose(Cancel As Boolean)
    '============================================================
        If eWB.Saved = False Then
            Select Case MsgBox("保存して閉じますか?", vbYesNoCancel)
                Case vbYes
                    eWB.Save
                    SetOnTime
                Case vbNo
                    SetOnTime
                Case vbCancel
                    Cancel = True
            End Select
        Else
            SetOnTime
        End If
    End Sub

    '============================================================
    'OnTimeメソッドで標準モジュールのコールバックメソッドを呼び出す
    Private Sub SetOnTime()
    '============================================================
        Dim PrcName As String
        Set eWB = Nothing
        PrcName = "'CallBackClose """ & index & """'"
        Application.OnTime Now + TimeValue("00:00:01"), PrcName
    End Sub

    '============================================================
    'OnTimeで読んだプロシジャーからのコールバック
    '参照元のブックが閉じられた後、このブックがエラーにならずに閉じられるまで繰り返す
    '(OnTime先で直接閉じればいいかも?)
    Public Sub LateClose()
    '============================================================
        Dim retry As Long
        DoEvents
        looping = True
        Do While looping = True
            On Error Resume Next
                ThisWorkbook.Close
                If Err > 0 Then
                    looping = False
                End If
            On Error GoTo 0
            Sleep RETIME
            DoEvents
            retry = retry + 1
            If retry = MAX_RETRY Then Exit Do
        Loop
    End Sub

 【問題点1】については、引き続きできなくて困っています。
(稲葉) 2015/04/16(木) 18:31

 >カレンダーフォーム用のクラスを準備して、クラスからイベント
 を発生させるなんてことも考えています。

 これが良さそうですね

 私が試した限りでは、イベントが拾えましたよ

(ichinose) 2015/04/16(木) 23:39


 一応、検証結果です(昨日は、眠すぎて ダウン)。

 アドインブックを formtest.xlam(プロジェクト名 formtest)

 アドイン側のユーザーフォーム(UserForm1)
 コマンドボタン一つ配置(CommandButton1)

 これをラップするクラス(Class1 モジュールは、InstancingをPublicNotCreatableに設定)

 UserForm1のモジュール

 '==================================================================
 Option Explicit
 Public Event Click(ByVal msg As String)
 Public Event Term()
 Private Sub CommandButton1_Click()
   RaiseEvent Click(CommandButton1.Caption)
 End Sub
 Private Sub UserForm_Terminate()
    RaiseEvent Term
 End Sub

 Class1のモジュール

 '==============================================
 Option Explicit
 Private WithEvents frm As UserForm1
 Event Click(ByVal msg As String)
 Event Term()
 Public Enum Frmmodal
   Frmmdl = 1
   Frmmdless = 0
 End Enum
 Sub Showform(Optional ByVal modal As frmmodal = 1)
    Set frm = New UserForm1
    frm.show modal
 End Sub
 Sub Unloadform()
    Unload frm
    Set frm = Nothing
 End Sub
 Private Sub frm_Click(ByVal msg As String)
    RaiseEvent Click(msg)
 End Sub
 Private Sub frm_Term()
    RaiseEvent Term
 End Sub
 Function chkUF() As Boolean
  Dim f
  For Each f In UserForms
     If f.Name = "UserForm1" Then
        chkUF = True
     End If
  Next f
 End Function

 formtest.xlamを参照設定するブックをTest.xlsmとすると、

 このブックのSheet1のモジュールに

 '============================================
 Option Explicit
 Private WithEvents uf As class1
 Const CELL_ADR = "$A$1"
 Private Sub uf_Click(ByVal msg As String)
        If Selection.Address = CELL_ADR Then
            MsgBox msg
        End If
        uf.Unloadform
        Set uf = Nothing
 End Sub
 Private Sub uf_Term()
        uf.Unloadform
        Set uf = Nothing
 End Sub
 Private Sub Worksheet_SelectionChange(ByVal Target As Range)
        If Target.Address = CELL_ADR Then
           If uf Is Nothing Then
              Set uf = Workbooks("formtest.xlam").Frm
              uf.Showform Frmmdless
           Else
              If Not uf.chkUF Then
                 uf.Showform False
              Else
                 uf.Unloadform
                 Set uf = Nothing
              End If
           End If
        End If
 End Sub

 このようにして、セルA1を選択して 試してみてください。

(ichinose) 2015/04/17(金) 07:02


 テストで、できました!
 たぶんアドイン側のThisWorkBookモジュールに、
 Function Frm() As Class1
     Set Frm = New Class1
 End Function
 を入れれば動くかなと思って、入れてみました。

 Set uf = Workbooks("formtest.xlam").Frm
 この部分、参照設定されていることが明らかでも、ブック名は入れておいたほうがいいのでしょうか?

 あと、列挙型のところ、Publicで宣言すれば外部の引数に使えたんですね・・・。

 頂いたコードを参考に、カレンダーフォームに合わせて書き換えてみます。
 フォームごとにラップするクラスを作らなければいけないのが難点ですね・・・。
(稲葉) 2015/04/17(金) 08:54

 >Function Frm() As Class1
 >    Set Frm = New Class1
 >End Function

 申し訳ありません、記述忘れです。

 >ブック名は入れておいたほうがいいのでしょうか?
 Thsiworkbookのモジュールですから、付けいないとエラーになりますが、これを標準モジュールにおいて

 Set uf = Frm

 とするよりは、どこのモジュールという事がわかりやすいと 思いませんか?

 参照設定がなくても アクセスが可能な方法という事もありますが、わかりやすいという意味もあります。

 クラスを介さないとならないのは、参照設定の必要性ですよね。今回の場合は、イベントを利用したい

 が原因です。参照設定の必要がなければ、ユーザーフォームだけでもなんとかなる場合もありそうですよ。

 ユーザーフォームの仕様にもよるでしょうね!!

(ichinose) 2015/04/17(金) 12:57


 >どこのモジュールという事がわかりやすいと 思いませんか?
 確かにその通りですね!
 エラーの内容も、アドインを開いていないということが一目でわかるようになりますし!

 >クラスを介さないとならないのは、参照設定の必要性ですよね。今回の場合は、イベントを利用したい
 >が原因です。参照設定の必要がなければ、ユーザーフォームだけでもなんとかなる場合もありそうですよ。
 >ユーザーフォームの仕様にもよるでしょうね!!
 やっぱりイベントが原因ですよね・・・
 脱却とこちらで教えてもらったことを思い出しながらカレンダーフォームを作っていて、
 結合度として、どのように結果を返せばよいか考えた結果が、イベントでした。

 フォーム側のイベントに、CallByNameで参照元へ返すことも考えましたが、
 本当に利用者が使いやすいのか? と考えたときに、
 オブジェクト名、プロシージャ名、引数と、設定を失敗したときにアドイン側のコードにデバッグが
 移ってしまうと考え、Eventなら書かなければ、実行されないだけですのでそのような仕様にしました。

 イベントが必要なければ、ThisWorkBookモジュールにユーザーフォームのインスタンスを作って
 Showするだけで十分ですよね。

 アドインの開発をしてて思ったことが、コンパイルエラーがあると、アドインの関係ないメソッドまで
 実行時エラー40036
 アプリケーション定義またはオブジェクト定義エラーです。
 が表示されるんですね・・・

 せめて、
 アプリケーション定義またはオブジェクト定義の「コンパイル」エラーです。
 くらい表示してもらえたらわかりやすいのに!!

(稲葉) 2015/04/17(金) 13:44


 色々、教えてもらって実運用までたどり着いたのですが
 運用後に元に戻す結果になりましたorz

 下記の手順(実際使用しているコードが長くなり、再現できるコードを再構築する気力がないので、
 状況説明だけです)のときに、
 コンパイルエラー オブジェクトまたはライブラリが見つかりません
 となります。

 アドインブック  (A)
   誰でもアクセスできるネットワークドライブに置く
   Class1でClickのEvent実装
   Class1はフォームをラッピングしたもので、参照元にイベントを渡すだけの役割

 テンプレートブック(T)
   xlsmで保存し、テンプレートとして使用。 名前を付けて保存で記録としている。
   Aを参照設定して、シートモジュールに「Private WithEvents cls1 As Class1」
   シートモジュールに、SelectionChangeとcls1_Clickイベントを作成
   Selection_Changeイベントでフォームを表示し、ボタンのクリックイベントで
   データを入力する

【再現手順】

 1)Tを開いてデータを入力する(コンパイルエラー出ず、SelectionChangeイベントも問題なし)
 2)名前を付けて保存で、1階層下のディレクトリに保存する
   この段階でもSelectionChangeイベント問題なし
 3)閉じる
 4)HYPERLINK関数で自分が開く →問題なし
 5)HYPERLINK関数で別PCで開き、別セルをクリックする
   「Private WithEvents cls1 As Class1」
   「コンパイルエラー オブジェクトまたはライブラリが見つかりません」
   参照設定のダイヤログが開くが、Aは「参照不可」にもならず、レ点も付いた状態。
   OKで確定する
 6)SelectionChangeイベントが正常に動く
   

 どうにも理由がわかりません・・・。
 これはアドインからイベントを拾うのは間違っている、そういうことなのでしょうか・・・。
 解決策があれば教えてください。

 参照設定しないでも動く仕様に変更も考えますが、できればここまで来たので
 最後までイベント仕様のままアドインを完成させたいです。
(稲葉) 2015/04/21(火) 15:00

 まだ、諸事情により試してはいませんが・・・。

 >HYPERLINK関数で別PCで開き

 HYPERLINKが問題なのか?
 別PCから対象ブックそのものを開いた場合は、どうなのか?

 HYPERLINK関数が問題なのか? HYPERLINK設定(右クリック---ハイパーリンク)では どうなのか?

 この辺りが気になるところです。

 アドインをプログラムツールとして共有
 私は、参照設定での運用は、あまりありませんが、アドインからのクラスの共有は、結構あります。

 運用方法があまりポピュラーでないと、 バグが潜んでいても気が付かない 

 これを一つでも克服したい とは思っていますから、頑張ってください。

 それとスレッド変えた方が良いかもしれませんね

(ichinose) 2015/04/24(金) 07:02


 HYPERLINK設定は検証していません・・・
 エクスプローラから開いた場合と、HYPERLINK関数で開いた場合の違いは
 前者はWorkbookOpenイベントより、参照先ブックが先に開き
 HYPERLINK関数から開いた場合は、WorkbookOpenイベントが先に開きました。

 ↑も含めて落ち着きましたら、再現手順をまとめて、新しくスレッドたてたいと思います。
 ありがとうございます。
(稲葉) 2015/04/24(金) 09:14

コメント返信:

[ 一覧(最新更新順) ]


YukiWiki 1.6.7 Copyright (C) 2000,2001 by Hiroshi Yuki. Modified by kazu.