それわVBA案件ですね

エクセルVBAネタを書いています

表データを扱うクラスを作ってみた

超久しぶりな更新です。

こんちくわ 壁|ω・)ノ


さて久々な記事は、VBAを使った表データの集計用になかなかイケてる(当社比)ツールができちゃったんじゃね?~なオハナシです。


でわいきます ̄▽ ̄)ノ


目次


背景

私は仕事柄実験データの解析をよくやっているのですが、1日の実験で得られるデータ数が500を超える(合計約100サンプル分のソースデータ)ことも少なくなくって、それぞれについて平均、標準偏差、データサンプル数計算とそれをグラフ化するだけでも


こんなんやってられっか~(ノ ゚Д゚)ノ ==== ┻━━┻



ってなるわけで、当然VBAワンポチ解析に持ち込んでいます。


解析プログラムは構造化したソースデータを取り扱う形で作成していきますが、対象表データについて以下の情報を取得する機能単位をよく作成します


  1. 特定の列のデータ範囲 (列タイトルセルを除いた残り)(Rangeオブジェクト)
  2. 各列タイトル文字列を格納した配列
  3. 各列タイトルのセル(Rangeオブジェクト)
  4. 特定の条件を満たすデータ範囲


f:id:FukuCyndiP:20200808170154p:plain

f:id:FukuCyndiP:20200808171410p:plain

これまでは過去に作ったFunctionプロシージャを都度修正しながら使いまわししてはいるのですが、なんかコードが全体的に散らかるんですよね。


そんなもん、ListObject使えばなんかうまいこと書けるやろ~



というご意見もあるかと思いますが、以下の理由からクラスモジュールに各機能単位を押し込めることにしました。


理由①:表を取り扱うツールをまとめておけば管理や持ち運び(?)が楽
理由②:表から別の表への転記など1つのマクロで同時に複数の表を扱うケースがある
理由③:例えば表全体のRange()だけクラスに通せば、必要な情報をプロパティとして含んだ表オブジェクトを作成するみたいなことができる



クラスTableTools


今回作ったクラスでは以下のようなプロパティ、メソッドを実装しました

1. DefineTableメソッド(対象の表が含まれるワークシート名 as String, 表範囲 as Range)
インスタンスとして取り扱う表を定義するメソッド

​ 2. tableHeaderCellsプロパティ as Range
表のタイトル列全体を示す

3. tableDataBodyRng プロパティ as Range
表範囲全体からタイトル行を除いた表データ全体を示す

​ 4. ColumnDataArea プロパティ(列タイトル as String) as Range
表の列タイトル文字列を引数として対応するデータ範囲を返す

​ 5. ColumnTitleCellプロパティ(列タイトル as String) as Range
表の列タイトル文字列を引数として対応するデータ範囲を返す

​ 6. FindCommonAreaメソッド(列タイトル as String, Param 参照範囲 as Range) as Range
特定の列データ範囲から特定の条件(Rangeオブジェクトで指定、複数対応可)に対応するデータ範囲を返す


クラスTableToolsコード

以下クラスモジュールTableToolに記述したコードです。


最初にフィールド部分とDefineTableメソッドですが、これがこのクラスのご本尊的なコードになります。表の必要な範囲の指定するために引数として取得した範囲をListObjectオブジェクト化して取り扱いました。

Option Explicit

Public tableHeaderCells As Range
Public tableDataBodyRng As Range
Private tableColumns As Collection
Private tableHeaders As Collection
Private targetWorksheet As Worksheet

Public Sub DefineTable(ByVal wsName As String, ByVal rng As Range)

    '引数 rngで指定するデータ対象領域をListObject化して以下の領域を特定して配列化する
    '  Title行:tableColumnItems
    '  1列目(各データのTitle列): tableColumnItems
    
    Set targetWorksheet = Worksheets(wsName)
    With targetWorksheet
    '対象シート内のListObjectを全て解除する
          If .ListObjects.count <> 0 Then
                Dim temp As Variant
                For Each temp In .ListObjects
                     temp.Unlist
                Next
          End If
          
          With .ListObjects.Add(Source:=.Range(rng.Address), xllistobjectHasheaders:=xlYes)
                .Name = "tempTable"
                .TableStyle = ""
          End With
    
          With .ListObjects("temptable")
                Set tableColumns = New Collection
                Dim tempArea As Variant
                For Each tempArea In .ListColumns
                    Call tableColumns.Add(tempArea.DataBodyRange, tempArea.Range(1).Value)
                Next
                
                'カラムTitleが格納されているセル(Rangeオブジェクト)を集積する
                '   Item: Rangeオブジェクト
                '   Key: 各カラムTitle
                Set tableHeaderCells = .HeaderRowRange
                Set tableHeaders = New Collection
                For Each tempArea In tableHeaderCells
                    Call tableHeaders.Add(tempArea, tempArea.Value)
                Next

                Set tableDataBodyRng = .DataBodyRange
                
                .Unlist
          End With
  End With

End Sub



このコードではクラスでメンバーを定義するための下準備として以下の2つのコレクション、tableColumns, tableHeaders を作成しています

それぞれのコレクションのイメージとしては以下のような感じです


f:id:FukuCyndiP:20200808170045p:plain



f:id:FukuCyndiP:20200808170116p:plain



ColumnDataAreaプロパティは tableColumns コレクションのItem (列データ範囲)を Key (列タイトル文字列) で呼び出します

Public Property Get ColumnDataArea(ByVal columnTitle As String) As Range
'指定のカラムタイトル列のデータ範囲を返す
    Set ColumnDataArea = targetWorksheet.Range(tableColumns.item(columnTitle).Address)
End Property



ColumnTitleCellプロパティも同様にtableHeaders コレクションのItem (セルRange オブジェクト)を Key (列タイトル文字列) で呼び出します

Public Property Get ColumnTitleCell(ByVal columnTitle As String) As Range
'表のカラムタイトル群(1行目)に含まれる指定の文字列を含むセルを返す
    Set ColumnTitleCell = targetWorksheet.Range(tableHeaders.item(columnTitle))
End Property



FindCommonAreaメソッドはソースコードは載せておきますが、説明が少々ややこしいので次の記事で書こうと思いますw

Public Function FindCommonArea(ByVal targetColumnTitle As String, ParamArray refAreas() As Variant) As Range
'表内の複数の範囲に共通する範囲を返す

        Dim targetColumnArea As Range
        Set targetColumnArea = ColumnDataArea(targetColumnTitle)
        
        Dim refInitialRows() As Variant
        Dim refLastRows() As Variant
        Dim refRows As Variant
      
        
        ReDim refInitialRows(UBound(refAreas))
        ReDim refLastRows(UBound(refAreas))
        ReDim refRows(UBound(refAreas))
        Dim commonRow As Range
        With targetWorksheet
            Dim i As Long
            For i = LBound(refAreas) To UBound(refAreas)
                refInitialRows(i) = refAreas(i).Cells(1).Row
                refLastRows(i) = refAreas(i).Cells(1).Row + (refAreas(i).Cells.count - 1)
                Set refRows(i) = .Rows(refInitialRows(i) & ":" & refLastRows(i))
                Set commonRow = refRows(LBound(refAreas))
                Set commonRow = .Range(Intersect(commonRow, refRows(i)).Address)
            Next
              
            Set FindCommonArea = .Range(Intersect(targetColumnArea, commonRow).Address)
        End With
End Function


まとめ

今回の記事では表データ集計解析マクロ作成時にこりゃ使い勝手がいいぞい(当社比)なプロパティ、メソッドを詰め込んだクラスを紹介しました。表データの集計や解析用マクロでは表全体のデータ範囲や特定の列に含まれるデータ範囲やその列タイトルセルを取得する場面がたくさんあります。これまでは対応するメソッドを毎回標準モジュールに用意していたのですが、コードが整理できなくっていまいちな感じでした。今回その辺をクラスとしてまとめたわけですが、実際に使ってみてコード全体を整理しつつシンプルで読みやすく記述できるようになったなぁと思います。

TLを見ていると、独自のライブラリを作成して運用されている方が多いように思えますが、今回は小さな処理ではありますが、目的別に専用のライブラリを準備しておくと後々資産になるんだなぁと実感しました。

因みに

今回のコーディングでは、表タイトル文字列から対応するデータ範囲を呼び出すために、1対1の対応関係を集積するCollectionの仕組みを利用することを思いついたことが個人的には大きな発見でした。同様に1対1を集積するDictionaryももちろん利用できます思います。事前バインディングまたは事後バインディングが必要な点でひと手間がかかりますが、登録したKeyを配列として呼び出すことができるなどCollectionではできないことも多いので、目的に応じて使い分けるといいかなぁと思います。



次は積み残しているFindCommonAreaメソッドについて書こうと思います

でわまた~  ̄▽ ̄)ノシ

Application.OnTime で設定した無限実行を正しく制御する

昼ごはんにカレーうどんを食べました。
メチャおいしかったけど、胸やけがコワかったあてくしです

こんちくわ 壁|ω・)ノ


さて、今日の記事はApplication.OnTimeメソッドで作った無限実行は止める仕組みが必要。でも 止めるなら正しく止めましょう~なオハナシです。

でわいきます ̄▽ ̄)ノ



目次


背景

前回の記事では、指定した時間にマクロを起動させるApplication.OnTimeメソッドを使って、学習時間を管理するマクロを紹介しました。

fukucyndip.hatenablog.com

アルゴリズムのポイントはApplication.OnTimeで Nowから指定の時間後に自分自身を起動する というものでした。ただし永久機関になってしまうので、ちゃんと止める仕組みを用意しましょうね~と書きました。

ところが、この止める仕組みとして前回紹介したプロシージャでは誤作動が起こることがわかりました。なぜだろうかと頭をひねり続けて土曜日の時間を延々と過ごしたのですが、一つの結論と対策にたどり着いたので、修正コードとともに紹介します。



Application.OnTime を使った時間更新アルゴリズム

例えば、10分間隔で表示時間を更新するなら、以下の様なコードが書けます。

f:id:FukuCyndiP:20200126215858p:plain:w550

 


このコードは以下のように動作して、A1セルの時間表示を更新していきます、

f:id:FukuCyndiP:20200126220007p:plain:w400



ただしコレはきちんと制御しないと、いつまでも時間更新が止まらないことになります。ところが、下の図のように、間に停止処理をただ挟み込むだけでは止めることができません。


f:id:FukuCyndiP:20200126220059p:plain:w450


Application.OnTimeメソッドが実行された瞬間に、次の実行予約が確定するためですね。 




永久機関を制御するために

そこで、Application.OnTimeメソッドを実行するフラグを使った以下のような仕組みを考えました(前回の記事で紹介した仕組み)。

f:id:FukuCyndiP:20200126220316p:plain:w450


このコードでは以下のような動作を期待しました。

f:id:FukuCyndiP:20200126220339p:plain:w450


結果、これでうまく動作したので、前回はこのまま記事を公開したのでした。




不具合発見

作成したコードで本格運用を始めたのですが、ふとカウンターが変な動作をするに気が付きました。

まずは、下記Gifアニメのカウンターの動きをご覧ください。


f:id:FukuCyndiP:20200126220410g:plain:w450

なんか、なんかカウンターが不規則な動きをしてます。

しかも、どう見ても設定の時間間隔ではない。。

原因を特定するのに時間がかかりましたが(土曜日の日中の時間をほぼ丸ごと使いましたw)、結論としてはカウンターのON/OFFを短時間の間に繰り返したことが原因で起こる不具合だということがわかりました。


では、何が起こっていたのか。

図にすると以下のような感じです。


f:id:FukuCyndiP:20200126220630p:plain:w500


先のコードでは、停止ボタンポチー後も発生するClockCounterの不要な予約起動そのものはスルーして、次の予約を発生させないという仕組みでしたが、結果的にそれが不具合の原因になっていたんですね。



不具合の対策

実いうと停止ボタンポチー後に発生するClockCounterの不要な予約起動は避けられないと思っていました。でも、ちゃーんと制御するための仕組みが準備されていました。

先日の記事を思い出してみましょう。

f:id:FukuCyndiP:20200126220806p:plain:w550
(Applicationは省略できません)

引数 省略 説明
EarliestTime プロシージャを実行したい時刻 Date
Procedure 実行するプロシージャ名(ダブルクオーテーションで囲みます)
(文字列を引数として渡す形での設定は可能だが、変数を渡す形での設定はできないよもよう)
String
LatestTime (指定のプロシージャが実行できない状態のとき)実行できる状態になるまで待つ時刻
省略した場合は指定のプロシージャが実行できるまで待ちます
Date
Schedule 設定済みのOnTimeメソッドの実行を取りやめるか否か Boolean


ということが書いてありましたね。

このうちの引数 [Schedule] のところを見ると、設定済みのOnTimeメソッドの実行を取りやめるか否かと書いてあります(ハイ。私が書きました)。これをきちんと使わなければいけなかったんですね。

でわどのように記述するのか。


f:id:FukuCyndiP:20200126220831p:plain:w500


御覧のように、引数 Schedule=False を記述します。ただし、ここで気を付けないといけないのは、実行をスキャンセルする予約起動を正確に特定しないといけないことです。つまり、起動するプロシージャ名だけでなく、起動時間の記述もそろえないといけないのですね。

というわけで, ClockCounterのコードの群は以下のように作りました。


f:id:FukuCyndiP:20200126221010p:plain:w500


Now()+時間間隔内容を変数reservedTimeに格納しました。そして、その中身を停止用プロシージャでも利用できるよう`モジュールレベル変数として宣言しています。

これで、停止ボタンポチー後に発生していた不要な予約起動も完全にキャンセルすることができるようになりました。

と、いうわけで前回掲載しましたコードは以下のように修正されました

Option Explicit

Private Const timeCount = "0:00:10"  '時間カウントの間隔を設定する
Private reservedTime As Date
Private isInitial As Boolean


Public Sub TimerStart()
  isInitial = True
  MsgBox "始めます", vbInformation, "がんばれ~"
  Call ClockCounter
End Sub


Private Sub ClockCounter()

  With ws時間記録
    '開始ボタンポチーした最初は時間の表示を更新しない
    If Not isInitial = True Then
      .Range("B2").Value = .Range("B2").Value + TimeValue(timeCount)
      .Range("C2").Value = .Range("C2").Value + TimeValue(timeCount)
    End If
    
    'ClockCounterご本尊
    reservedTime = Now() + TimeValue(timeCount)
    Application.OnTime reservedTime, "ClockCounter"
    isInitial = False
  End With

End Sub


Public Sub stopTimer()
  'タイマーを止めるときは "今回の学習時間" 列のみ時間をリセットする
  Application.OnTime reservedTime, "ClockCounter", , False
  
  MsgBox "記録終了します", vbInformation, "終わったのかな? ん?"
  ws時間記録.Range("B2").Value = "0:00:00"
End Sub



まとめ

今回の記事ではApplication.OnTimeメソッドの 自分予定呼び出しで作った無限実行を止める方法について書きました。前回の記事でもコードとしては掲載していたのですが、不具合が発生する不完全なものだということがわかりました。

Application.OnTimeメソッドには設定された予約実行を停止するための仕組みとして、引数 Schedule がちゃんと準備されており、それを使うことで不具合なくコードを実行することができるようになりました。


つまるところ、きちんと理解してから使えってことですねw

でわまた~  ̄▽ ̄)ノシ

学習時間管理ツールを作る

オリンピックイヤー最初の記事更新です。

このように書くと、年明けすぐに記事を更新したかのようですが、違います。

こんちくわ 壁|ω・)ノ


さて今日の記事は、日ごろのコソ勉の累積時間をカウントするツールを作ろう~なオハナシです。


でわいきます ̄▽ ̄)ノ



目次




背景

私が所属させていただいております#ノンプロ研の御大、通称タカーシさんが英語の学習に1000時間を捻出すると宣言されてました。その後学習を継続しつつ、累積時間を頻繁にツイされているのを見てふと思いました。

塵も積もれば山となるを見える化すれば、継続のチカラになりそう

ならば、

  • コソ勉始めたら開始ボタンポチー(時計スタート)
  • 終わったら終了ボタンポチー(時計止まる)
  • 勉強時間と累積時間の表示と記録


みたいなツールがあればいいなぁ~
と、なりまして、早速作ることにしました。


VBAで時間をカウントする

時間のカウント機能は言うまでもなくこのツールの御本尊様であるわけですけれども、コレをVBAでどう作るかが案外悩むところかと思います。
myコソ勉にはVBAも含まれるワケでして、時間記録マクロの実行中は他のVBAのコーディングができないというのもイマイチなので。。。

もちろん、

  1. 開始ボタンポチーでその時の時刻を変数 a に代入
  2. 終了ボタンポチーしたときの時刻から変数aの時刻を引いて計算した時間を表示

みたいな仕掛けもいいんですけど・・
それではなんだかつまらないし、折角ならリアルタイムでの継続時間が見たいってのが世の情けですよね



で、どうやるの?

指定の時刻にマクロの予約実行を行う OnTime メソッドを利用します。
このメソッドは指定の時刻だけでなく、例えば○○分xx秒後に実行ということも設定できますのでこの機能を利用します。

まずはこのメソッドの概要は以下の通りです

Application.OnTime(EarliestTime, Procedure, [LatestTime], [Schedule])
(Applicationは省略できません)

引数 省略 説明
EarliestTime プロシージャを実行したい時刻 Date
Procedure 実行するプロシージャ名(ダブルクオーテーションで囲みます)
(文字列を引数として渡す形での設定は可能だが、変数を渡す形での設定はできないよもよう)
String
LatestTime (指定のプロシージャが実行できない状態のとき)実行できる状態になるまで待つ時刻
省略した場合は指定のプロシージャが実行できるまで待ちます
Date
Schedule 設定済みのOnTimeメソッドの実行を取りやめるか否か Boolean



それで○○分xx秒後に実行は、Now + TimeValue(指定の間隔)で設定します。

例えば以下の"十秒後に挨拶" マクロを実行すると、


Sub 十秒後に挨拶()
  Application.OnTime Now + TimeValue("0:00:10"), "Greet"
End Sub

Sub Greet()
  MsgBox "こんちくわ~"
End Sub



実行から10秒後(Now + TimeValue("0:00:10"))に

f:id:FukuCyndiP:20200119171431p:plain:w200

が出力されます。


このメソッドを利用すれば目的のカウントアップタイマーが作れそうな気がしますよね。




カウントアップタイマー

1項目分ですが、下記の様なカウントアップタイマーのコードを紹介します

f:id:FukuCyndiP:20200119171648p:plain

(フォームコントロールで作成したボタンにタイマー開始(TimerStart)と終了(StopTimer⦆を割り当てます


Option Explicit

Private Const timeCount = "0:00:02"  '時間カウントの間隔を設定する
Private flagTimerRegulation As Boolean
Private isInitial As Boolean


Public Sub TimerStart()
  isInitial = True
  flagTimerRegulation = True
  MsgBox "始めます", vbInformation, "がんばれ~"
  Call ClockCounter
End Sub

Public Sub stopTimer()
  'タイマーを止めるときは "今回の学習時間" 列のみ時間をリセットする
  flagTimerRegulation = False
  MsgBox "記録終了します", vbInformation, "終わったのかな? ん?"
  ws時間記録.Range("B2").Value = "0:00:00"
  Call ClockCounter
End Sub


Private Sub ClockCounter()

  'モジュールレベル変数として宣言した flagTimerRegulation を利用して
  '時計を動かすかどうかを決定する
  If flagTimerRegulation Then
    With ws時間記録  
      If Not isInitial = True Then
        .Range("B2").Value = .Range("B2").Value + TimeValue(timeCount)
        .Range("C2").Value = .Range("C2").Value + TimeValue(timeCount)
      End If
      'ClockCounterご本尊
      Application.OnTime Now() + TimeValue(timeCount), "ClockCounter"
      isInitial = False
    End With
  End If

End Sub

(サンプルコードなので、時間表示部分はアドレス決め打ちで書きました)

プロシージャ "ClockCounter" は 設定した時間が経過したら自分自身を呼び出して時計を進める 仕組みになっています。これは放っておくと永久に時間を刻み続けることになりますので、経過時間記録と時計部分の実行はブール型変数flagTimerRegulation で制御 しています。


実行するとこんな感じで動きます

f:id:FukuCyndiP:20200119191148g:plain:w450
(2秒間隔設定です)

ちゃんと思惑通りの動きになっていますね

  • 開始ボタンポチー(時計スタート)
  • 終了ボタンポチー(時計止まる)
  • その間の活動時間の表示と累積時間を記録




時間カウントしながら他のVBAコードは実行できるの?

コレメッチャ大事ですよね。

確認した範囲では概ね大丈夫でした。

他のコードの実行:ほとんど影響されない

上に書いたように、Application.OnTimeメソッドは、引数 LatestTime を設定しない場合は指定のプロシージャが実行可能になるまで待ちます。逆に言うと、実行可能状態になれば自動で再開しますので、他のコードを実行したとしてもその間は止まりますが、終われば再び時を刻み始めます。

したがって、別のコードの実行中はその間時計は止まりますが、よほど実行時間が大きなコードでない限りは、時間計測に影響はほとんどないと思います。


VBEでのコード編集:可能だけれども、テストランなどでエラーが発生すると時計も止まる

時計はVBE画面で他のコード編集中でも律義に動いてくれます。時計と同じプロジェクト上でのコーディングも可能です。時計が動いた瞬間に入力中コードが強制的に入力確定されしまいますが、時計を動かす間隔を広げておけば、ほとんど気になることはないと思います。

ただし、テストランなどでコード実行後に何らかの'エラーが発生したときには、時計も一緒に止まってしまう`点に注意してください(時計を動かすために再度開始ボタンポチが必要)。



まとめ

今回の記事ではコソ勉時間を見える化するためのツール作成を紹介しました。

リアルタイムで経過時間を表示するためのポイントは

  • マクロ予約実行メソッドOnTimeを利用する
  • 設定時間後に自分自身を呼び出す

でした。

ただし、自分自身を呼び出す仕組みとしましたので、停止のための仕組みを必ず準備することが注意点です。

時計を動作させながらVBAコーディングを含めた他の作業ができるので、目的通りのツールを作ることができました。

現在使っていますが、実際にかかった時間、これまでの累積時間の見える化は、思った通りコツコツコソ勉のモチベ維持に良いと思いました。




因みに

現在私が使っているのは以下の様なものです。

f:id:FukuCyndiP:20200119173604p:plain:w500

ActiveXコントロールを利用した5項目ものです(ゼロが多いのはご愛敬ということでw)。

今回紹介したのは1項目分のコードでしたが、以下の仕組みを追加すれば複数項目にも対応できるかと思います

  • ​各項目にセットしたボタンに時計動作開始/終了のプロシージャを仕込む
  • それぞれの項目行に経過時間を表示させる


当初はクラスを使って、それぞれの項目での経過時間を同時にカウントできるようにしようと考えたのですが、Application.Ontimeメソッドを使ったプロシージャの呼び出しはオブジェクトモジュール上では実行できないようです。
複数の学習を同時進行することはないので、そもそも必要ないですけどね。


でわまた~  ̄▽ ̄)ノシ


追記

この記事で紹介したコードでは時間のカウントに不具合が出てしまうことがわかりました。
原因を特定してコードを修正したので、コードはこちらの記事を参考にしてください

fukucyndip.hatenablog.com


InputBoxメソッドを使ってワークシート名を特定する

年末のイベント年賀状のデザインを考えるのがたるいあてくしです。

こんちくわ 壁|ω・)ノ


さて今日の記事は、他所から送られて来るワークブックに含まれる数も名前もわからないワークシートの中から、目的の情報が詰まっているワークシート名を特定しよう~ なオハナシです。



でわいきます ̄▽ ̄)ノ



目次

背景

ある外部事業者さんとの仕事で、送られてきた情報をもとに解析した結果を同じファイルに書き込んで先方に返すということを定期的にやっています。ルーチンワークですので、ここぞとばかりマクロ化して作業効率化しています。


f:id:FukuCyndiP:20191222001934p:plain:w350


ところが! ところがですよ!!

今月の初め頃から、先方から送られてくるエクセルファイルに変化が・・・

なんと、オートフィルターやら、条件付き書式などを付けてくるようになったのです。さらに、ワークシートの数や名前、表の列位置やTitle表記も変わってて..orz..。

つまり、情報の書き方がガラリと変わってしまったのですね -ω-)



これまで3年ほどはただの味気ない一覧で扱いやすかったのに(・д・)チッ

いあ、見た目きれいなんだけど、前より見やすくなったんだけど、こちとら自動で情報を抜き差しするだけなので、むしろ味気ない一覧の方がよかったわけで。。


てなことで、手元のマクロの修正を迫られている今日この頃なのです。

それで、いろいろある要対応事項の中で少々手強いと思ったことについて、コレいけるんじゃね 的ひらめきがあったので、恥ずかしげもなく紹介したいと思います。



手強い要対応事項とは

現在手元の解析マクロの処理の流れとしては

  1. 送られてきたブックを選択して開く
  2. ワークシートから情報を読み込む
  3. 解析
  4. 新しい情報を開いたブックに書き込む


となっておりますです。

で、上の 2. の処理なのですが、今後も起こりえる変化を考えると コード上でワークシートが特定できなくなるコトに気が付きました。またその対処に普段から使っている王道的な方法が使えないことにも同時に気が付いたのですね。


  • ワークシート名を使う: 名前が何度も変更されると ✖
  • インデックス番号を使う: 数がムダに増減すると ✖
  • オブジェクト名を使う: ブックを新しく作り直されると △~✖

(すでにワークシート名が "格納情報の名称" → "Sheet1" (ブックを作り直したようだw)になっていて、処理が絶賛止まっておりますw)


というわけで

手強い対応事項:
数や名前に依存せずに目的のワークシートを特定する


思いついたアイデアとは

先日開催されたノンプロ研もくもく会でコードの修正作業をしていたのですが、良さげなアルゴリズムとして以下のようなことを思いつきました。


  1. ワークシートをカウント
  2. <条件分岐>
    1. シート数が1つ → インデックス番号で特定
    2. シートが複数 → マクロを使うユーザーに特定させる

(数には若干依存したw)


ブックに含まれるワークシートが複数あった場合に(名前は突然変更される可能性あり)どうやって必要なワークシートを特定するかが特に問題だったのですが、マクロを使うユーザーに特定してもらえばいいのではないか。さらに、それを実現する手段として InputBoxメソッドが使えそうなことに気が付いたわけですね。



InputBoxメソッドってナニ?

ユーザーに入力してもらった文字列を変数に代入するInputBox関数は使ったことがある方も多いかと思います。
ココで紹介するInputBoxメソッドでは戻り値として数式やRangeオブジェクトなどを取得できる点で大きく異なります。


InputBoxメソッドの記述

Application.InputBox(Prompt as string, [Title], [default], [Left]. [Top], [HelpFile], [HelpContextID], [Type])

[引数]は省略可能

なお、Applicationを省略すると自動的にInputBox関数と認識されるので、ご注意ください。

たくさんの引数がありますが、このメソッドを使う上で重要なのは [Type] で、数値で戻り値の種類を指定します。


Type引数値とその対応関係

説明
数式
文字列(引数省略時の規定値)
ブール値(True/False)
セル参照(Rangeオブジェクト)
16 エラー値(#N/Aなど)
64 値の配列



そう、ワークシートをユーザーに特定させるとわ、

  • 必要な情報が格納されたワークシートの任意のセルをクリックするようユーザーを促す
  • ダイアログの"OK"ボタンを押した瞬間にクリックしたRangeオブジェクトを取得
  • それに紐づいたワークシート名を引っぱり出す


という魂胆なのです(`ω´)グフフ

 

 

コード例

Option Explicit
Sub クリックしたセルのワークシート名を特定()
  Dim target As Range
  Set target = Application.InputBox(" ""こんちくわ"" を選択してください", Type:=8)
  
  Dim targetWsName As String
  targetWsName = target.Worksheet.Name
  
  MsgBox "アナタが選んだ ""こんちくわ"" は、" & targetWsName & "シートにあります"

End Sub

変数targetにユーザーが選択したRangeオブジェクト情報が格納されます。
TypeはInputBoxメソッドの最終引数ですが、ここでは第1引数(Prompt)から間の引数を省略しているので、名前付き引数とする点にご注意ください。
名前付きにしないと、"8"が第2引数とみなされてコンパイルエラーとなります。

そしてtarget.Worksheet.Name で、targetに格納されているワークシート名を引っぱり出しています。


まとめて下のように記述するのもアリですね。

Option Explicit
Sub クリックしたセルのワークシート名を特定()

  Dim targetWsName As String
  targetWsName = Application.InputBox(" ""こんちくわ"" を選択してください", Type:=8).Worksheet.Name
  
  MsgBox "アナタが選んだ ""こんちくわ"" は、" & targetWsName & "シートにあります"

End Sub


尚、InputBoxメソッドの戻り値を格納する変数宣言は理解しやすさを重視して Range または String としましたが、実用を考えるなならば、Variant型として宣言する方が良いかと思います(ユーザーがダイアログのOKではなくキャンセルボタンをクリックした場合も想定)。

 
動作検証
f:id:FukuCyndiP:20191222000714g:plain:w350

というわけでうまくいきましたねw

まとめ

今回の記事ではInputBoxメソッドと、その特徴を利用した事例としてワークシートの内容が固定されないブックを対象にしたワークシート特定方法について紹介しました。

このような事例が皆様のお役に立てるかどうかはわかりませんが、InputBoxメソッドの使い方を学ぶ上で少しでも参考になればと思います。

InputBoxメソッドは数値や文字列、オブジェクトだけでなく、ワークシート上の数値(文字列)群を配列として取得することができることも併せて覚えておくとアルゴリズムを考える上での引き出しが増えて良いと思いますね(ってか配列取り込みは使ってみたい)。

 


でわまた~  ̄▽ ̄)ノシ

 

ActiveX コントロールはコワくない

仕事の忙しさがひと段落してクリスマス、年末ムードが一気に押し寄せてきているあてくしです。

こんちくわ |ω・)ノ

さて今回はノンプロ研のアドベントカレンダーイベント(なんかイベントプロシージャ的な響きw)で、13日目を担当することになりましたので、久々の更新に合わせて記事を書こうと思います。

本日はなんだか謎めいていて、なんとなく扱うのがコワかったActiveXコントロールを必要に迫られて恐る恐るいじってみたら・・

あれ? なにこれそういうことだったのね・ω・)

と、あっさり理解できたので、皆さんにも共有しましょう!

な、お話です。



でわいきます ̄▽ ̄)




目次


突然ですが!

ActiveXコントロールって謎めいていると思いませんか?

私はエクセルのリボンの開発タブにひっついている、なんか訳の分からない謎のコントロールだと思っていました。


だって、"ActiveX" って、"" ですよ!!

なんかコワくないですか?


だいたい普通っぽいコントロールもちゃーんと準備されているのに、なんでわざわざ Ⅹなモノがすぐ下にひっついているのか・・・

f:id:FukuCyndiP:20191208234254p:plain:w200



VBAの神様も

一生触らなくて良い

って、言ってるんですよ。


bookmeter.com

なんかコワくないですか?(2度目)



だったのですが、データ解析用のワークブックに使える気がしたので、恐る恐るいじってみたら・・


あれ?なんだ簡単ぢゃん!

とあっさり、理解してしまったのでした。


ActiveXコントロールってナニ?

ハイ、一言でいうと、

ユーザーフォームで使う各種コントロールをワークシート上に埋め込むためのものです

ActiveX コントロールをいじってみたら、プロパティの設定、コードの書き方や内容がユーザーフォームと同じなことにすぐに気がついちゃったんですねw 

ユーザーフォーム:
コントロールが埋め込まれた専用の枠(?)を呼び出して使う

f:id:FukuCyndiP:20191212211339g:plain:w300
f:id:FukuCyndiP:20191212212646p:plain:w400
コードはフォームモジュールに書きます


ActiveXコントロール
コントロールをワークシートに埋め込んで使う

f:id:FukuCyndiP:20191212211907g:plain:w300

f:id:FukuCyndiP:20191212212903p:plain:w400
コードはシートモジュールに書きます

ね。どこに作るか以外はおんなじでしょw




フォームコントロールとの違いはナニ?

フォームコントロールが簡易的なもので、ActixeX コントロールは発展応用版ですね


以下両社の違いなどを簡単にまとめてみました

フォームコントロール ActiveXコントロール
機能 クリックに合わせて指定のマクロを発動する(コマンドボタン)
ワークシートの指定の場所に数値などを表示する(チェックボックス、コンボボックスなど)
クリックに合わせて指定のマクロを発動する(コマンドボタン)
チェック状態や、選択された値などをメモリに格納する(他のコントロールに付随したマクロと自由に受け渡し可能)
見た目のコントロール フォント、色など、ピクチャ系のコントロール フォント、色など、ピクチャ系のコントロール
数値入力時やマウスオーバーなどアクションに応じた見た目の変化
ピクチャの設定や、背景の透明化など
他のコントロールオブジェクトとの連携 ワークシートへの出力値を通じて連携 ボタンの状態、入力値やイベントトリガーなどを通じて自由に連携
イベントマクロの設定 コントロールのクリックに合わせて発動するのみ クリック、ダブルクリック、テキスト入力、マウスオーバー等の複数のイベントに合わせてマクロを実行可能
コードを書く場所 標準モジュール コントロールを埋め込んだシートモジュール
その他 テキストボックスは選択できない




どうやって使うの?

ActiveXコントロールの存在を知っている方はすでにご存じのことかと思いますが、リボンの開発タブ→挿入をクリックすると出てきます。

f:id:FukuCyndiP:20191208235619p:plain:w250


そこで埋め込みたいコントロールをクリックして、ワークシート上の設置したい場所をクリックして、好きな大きさにびろーんとすれば作れます。
見た目の変更など、各種プロパティを変更したいときは、デザインモードをONにした状態で、プロパティ部分クリックして設定します。 ONにしておかないと、移動も消去もできないのでご注意ください。

f:id:FukuCyndiP:20191208235956p:plain:w250


因みにあてくしはこのことを知らなかったため、一度くっつけると剥がせなくなる恐ろしいモノ と勝手に思い込んでおりました -ω-)


で、肝心のコーディングはどうするかというと・・・、
ここでは説明しきれないので、また次の機会にしますね。




ぶっちゃけフォームコントロールとどうなの?

例えば、ワークシートに埋め込むコントロールの使いどころとしては


  1. コマンドボタンをクリック → 設定したマクロを動かす
  2. チェックボックスやコンボボックスでパラメータを設定 → 設定に合わせてワークシート上の関数の引数を変更
  3. コマンドボタンクリック → チェックボックスやコンボボックスで設定したパラメータを使ったマクロを動かす

といったことでしょうか。



このような操作ならばどちらを使っても実現可能ですので、複数のプロシージャを組みわせることになるActiveX コントロールをあえて使わなくてもいいかなと思います。

もしフォームコントロールで使いにくい部分があるとすると、2.、 3.のケースかなと思います。何が気になるのかというと、コントロールで設定するパラメータを利用するためには、初期値や変更した後のパラメータ値をワークシートのどこかに書いておかなければいけないということですね。それはつまり、例えば何も知らないユーザーがうっかり初期値を記入しているセルをいじってしまうなど、ユーザーによる干渉が原因となって、プログラムが正常に動作しないリスクを残すということになるのです。

ワークシートを作って使ってもらう側に立つと、エラーによるプログラム停止でユーザーに不都合がないようにしたいですからねw

 


ActiveXコントロールならではのことってナニ?

先ほど書いた観点に立つと、ActiveXコントロールは初期値や設定値は変数に代入してメモリ上でプロフグラムを動かすことができるので、より安定的にプログラムを動かすことができます(いわゆる堅牢性ですね)

また、下記の様な様々な操作をきっかけにしたマクロ発動ができることでしょうか。

  • (ダブル)クリック
  • 内容の変更
  • ドロップダウン表示/非表示
  • キーボードのキーを押す/離す
  • コントロール(テキストボックスなど)を選択する

    など


このような機能を使うと、例えば、


  • テキストボックスにデフォルト値を設定 → 書き換えたいときにクリックするとデフォルト値を消去して、入力Readyにする(GetFocusイベント)
  • あるコントロール都道府県名を入力したら、それに応じた名産品一覧を別のコントロールに表示(Chaneイベント)
  • 選択しているボックスの色を変える(GetFocusイベント)
  • ユーザーによる特定のキーの使用を制限する(KeyPress, KeyUp, KeyDownイベント)
  • 必須項目の入力が終わるまで次の操作を制限する(LostFocusイベント)


などなど、ユーザーフレンドリーや安定的運用(プログラム製作者の意図から外れないようにユーザーの操作を導くといった)のための工夫を存分に設定できるかと思います。




ActiveX Controlで気を付けること

私も体験したことですが、ボタンやテキストボックスの大きさがでっかくなったりちっちゃくなったりすることがあります。

エクセル2016とWindows10との組み合わせによるバグだそうですが、外部モニターやプロジェクタに接続した際の画面解像度の変化と関係しているようです。

回避策として、コントロールを埋め込んだワークシートを起動とするのと同時に、埋め込んでいる全てのコントロールの大きさをちょっとだけいじる(Worksheet_Activateイベント案件)。ということがブログで紹介されていましたので(https://shimpokikaku.at.webry.info/201707/article_1.html)、絶賛利用させていただいています。

いまのところ不具合は発生していませんが、バグを確実に再現する方法がわからないため、十分に検証できていない状況です。なので、エクセル2016+Windows10の方はご注意くださいませ(Office365ではバグの報告はなさそうです)。




まとめ

今回の記事はActiveX コントロールについて、

  • ただのユーザーフォームのコントロールをワークシートに埋め込むためのものだった。
  • 大まかにはフォームコントロールとできることは同じ
  • ワークシートをよりユーザーフレンドリーに、より安定的に使うための細かい設定ができる発展版

ですよーということをお伝えしました。
実際に使うための細かい設定やコーディングについては別の機会とさせていただきましたが、今後ちょこちょこと紹介していこうかなと思います。

また、われらがノンプロ研のブログにも紹介されていますので、ご興味がある方は一度ご覧になっていただけるといいかと思います。  

tonari-it.com


それにしてもなんだか謎だったので、ムリに使わんでもいいや~と突っ込むのを 遠慮 避けていたActiveXコントロールでしたが、分かってしまえば何のことはなく、ワークシートの便利な使い方がわーっと目の前に広がった感じがしますね。

しかしたまたま勉強する気になったから良かったものの、もしその気になっていなかったなら、この知識を得る機会が果たしてあったのかと思うと・・・食わず嫌いは良くないを改めて実感ました。
徹底的にやることで、苦手を得意に転じてきた経験は過去にもありますが、苦手だから、なんだかよくわからないから避けるのではなく、逆にどんどん突っ込んでいった方が確実に得するんですよね。このことを忘れず年末年始、さらには2020年に凸入していこうと思います。

でわまた~ ̄▽ ̄)ノシ

過去のコードを見て進歩を実感する

天気がいい土曜の午前です。 我が家のワンコも気持ちよさげに寝ています。
f:id:FukuCyndiP:20191109102957j:plain:w250

つい添い寝したくなりますが、実行したら露骨に迷惑そうなカオして去っていくので、我慢なあてくしです。

こんちくわ|ω・)ノ


さて本日の記事ですが、昔書いたコードを見たらあまりのできていなさっぷりにビックリした! なお話です。

でわいきます ̄▽ ̄)ノ


目次

  1. 事の経緯と話題のコード
  2. 過去のコードを見て思ったこと
  3. このコードを書いた過去の自分を振り返る
  4. まとめ
  5. 因みに・・



事の経緯と話題のコード

管理人はExcelを使った実験データの解析にVBAを利用しています。

それで、2年前に作って使っていたデータ集計用ツールを同僚に提供することになったため、いま一つ使いにくかった操作を修正しようとコードを見たのが事の始まりです。

まずは惜しげもなくその全貌を全宇宙に、しかもノーカットで晒します。

Private Sub Worksheet_BeforeRightClick(ByVal Target As Range, Cancel As Boolean)
  P = Selection.Column
  r = Selection.Row
  PAdd = Selection.Address
  c = 3
  ws = ActiveSheet.Name
  Application.CommandBars("Cell").Enabled = False
  
  With Sheets(ws)
    Select Case r
      Case Is = 9
        N = Application.WorksheetFunction.CountA(.Rows("9:11"))
        CN = N - 4
        .Range(PAdd) = "O"
        If .Range(PAdd).Offset(0, -2) <> "O" Then
          If CN > 1 Then
            .Cells(12, .Range(PAdd).Column) = "O"
          End If
        End If
      
      Case Is = 10
        N = Application.WorksheetFunction.CountA(.Rows("9:11"))
        CN = N - 4
        .Range(PAdd) = "O"
        If .Range(PAdd).Offset(0, -2) <> "O" Then
          If CN > 1 Then
            .Cells(12, .Range(PAdd).Column) = "O"
          End If
        End If
      
      Case Is = 11
        N = Application.WorksheetFunction.CountA(.Rows("9:11"))
        CN = N - 4
        .Range(PAdd) = "O"
        If .Range(PAdd).Offset(0, -2) <> "O" Then
          If CN > 1 Then
            .Cells(12, .Range(PAdd).Column) = "O"
          End If
        End If
      
      Case Is = 22
        N = Application.WorksheetFunction.CountA(.Rows("22:24"))
        CN = N - 4
        .Range(PAdd) = "O"
        If .Range(PAdd).Offset(0, -2) <> "O" Then
          If CN > 1 Then
            .Cells(25, .Range(PAdd).Column) = "O"
          End If
        End If
                
      Case Is = 23
        N = Application.WorksheetFunction.CountA(.Rows("22:24"))
        CN = N - 4
        .Range(PAdd) = "O"
        If .Range(PAdd).Offset(0, -2) <> "O" Then
          If CN > 1 Then
            .Cells(25, .Range(PAdd).Column) = "O"
          End If
        End If
                
      Case Is = 24
        N = Application.WorksheetFunction.CountA(.Rows("22:24"))
        CN = N - 4
        .Range(PAdd) = "O"
        If .Range(PAdd).Offset(0, -2) <> "O" Then
          If CN > 1 Then
            .Cells(25, .Range(PAdd).Column) = "O"
          End If
        End If
            
      Case Else
      Application.CommandBars("Cell").Enabled = True
    
    End Select
    
    EN = Application.WorksheetFunction.CountA(.Rows("9:11")) - 4
    AN = Application.WorksheetFunction.CountA(.Rows("12:12")) - 1
    .Range("C15") = EN
    .Range("C16") = AN
    .Range("C17") = "=(C16/(C15-2))*100"
    
    EN = Application.WorksheetFunction.CountA(.Rows("22:24")) - 4
    AN = Application.WorksheetFunction.CountA(.Rows("25:25")) - 1
    .Range("C28") = EN
    .Range("C29") = AN
    .Range("C30") = "=(C29/(C28-2))*100"
  
  End With
End Sub



右クリックイベントを利用してワークシートにチェックを入れるコードなのですが、コレをみた管理人は思わず目をパチパチしてしまいましたw



過去のコードを見て思ったこと

で、これを見た瞬間の感想は、
同じコードの塊が何個も見える・・・

気が付いた方もおられるかと思いますが、

N = Application.WorksheetFunction.CountA(.Rows("9:11"))
CN = N - 4
.Range(PAdd) = "O"
If .Range(PAdd).Offset(0, -2) <> "O" Then
    If CN > 1 Then
        .Cells(12, .Range(PAdd).Column) = "O"
    End If
End If

の塊が山盛りありますね。


また、

EN = Application.WorksheetFunction.CountA(.Rows("9:11")) - 4
AN = Application.WorksheetFunction.CountA(.Rows("12:12")) - 1
.Range("C15") = EN
.Range("C16") = AN
.Range("C17") = "=(C16/(C15-2))*100"

も、なんで2回も書いているのか。。


次に思ったことは、
変数名がテキトーで何を示しているのかわからない。


ロジックなどはすでに忘却の彼方にある2年前に書いたコード。。
変数が何を意味してどう使われているのかを理解するのに時間がかかってしまいました。


さらに、びっくりしたのは
変数宣言がない


これはちょっと修正してテストランしたときにVBEが、

f:id:FukuCyndiP:20191109122458p:plain:w350

と叫んできて初めて気が付いたのですが、

変数はなーんにもいぢってなかったので、想定外のエラー過ぎて、数秒間固まってしまいましたw


その他にも、

  • 無駄に変数が多い
  • シートモジュールにコードを書いているのにワークシート名を指定している
  • イベントプロシージャ内でデフォルトでTargetとして宣言されている変数を使っていない
  • 右クリックイベントメニュー発生防止の用のコマンドを使わず、わざわざApplication.CommandBars("Cell").Enabled = False なコマンドを書いている


など、コードの書き方の下手っぷりが目に余りましたw



このコードを書いた過去の自分を振り返る

VBAのプログラミング自体はかなり長いことやっていますが、きちんと体系的に勉強し始めたのは去年の夏ごろでした。


それまではとりあえず動けばいい 俺様コード を量産していたわけですが、特にコードの読みやすさなんかは全く意識したことがありませんでした。


改めて勉強をしていく中で読みやすくシンプルに書くことが最も大事なことに気が付いて、それまではやってこなかった共通処理の分割や、型を意識した変数宣言、命名などを意識してやるようになりました。


その結果、変数宣言の強制(Option Explicit)やプロシージャ内の繰り返し処理の共通化、それを独立した機能単位を外に切り出して、メインプロシージャを読みやすくするすことなどがこの1年ほどで習慣化されてきました。

なので今回紹介したコードでは同じ処理のコードセットが必要な数だけ書かれているところが最初に目について、うひゃ! なんじゃこりゃ~ になりました。


変数宣言に至っては、現在はOption Explicitがデフォルトで記述されるようVBEを設定しているため、変数宣言せずにコードが動くことはまずありえません。それだけに、それまでワークしていたハズのコードが変数宣言が理由でエラーになるということが理解できず一瞬思考が停止するという事態に陥るということになったのです。


またこの1年でイベントプロシージャの理解もかなり進みました。当時のコードではプロシージャの引数にわざわざデフォルトで準備されているTargetを使っていないのを見るとイベントコードをきちんと理解していなかったのがまるわかりですね(当時はイベントプロシージャの引数なんて民間人には理解不能な聖域だと思っていましたw)。



これまで学習してきた経緯を考えると、当時はまだ知らないこともたくさんあって、コードの読みやすさなど全く意識していなかったんだから、無理もないのかなと思います。ただ、当時の知識レベルを最大限に活用して書いて結構満足していたものが、今見ると甚だ残念な出来上がりだったというのはなんだか赤面しそうですね。



まとめ

今回はたまたま過去に書いたコードを見なおす機会があったわけですが、当時と今ではあまりにも書き方に違いがありすぎて、誰が書いたんだ?いや俺じゃねぇ位のギャップを感じました。一方でたった2年前だけれども、今では当たり前のようにできていることを当時は全くできていなかったんだなぁと、過去の自分を具体的に振り返ることができて新鮮でした。

コードは今の俺流に修正されましたが、2年後に見直したらまた、 "誰が書いたんだ?いや俺じゃねぇ" ってことになっているかもしれませんね。まぁ、それも貴重な成長記録ってことでw


このように、過去に書いたコードをロジックを変えずに見直すことをリファクタリングと言うそうです(3か月くらい前に初めて知った単語w)。皆さんのお手元にも黒歴史的コードがある(ハズ)かと思いますが、一度勇気を出して見直してみるのもいいと思います。自分の成長が手に取るように分かって良いと思います。

 

因みに

今回のコードは以下のように修正されました。

Option Explicit
'----------------------------------------------------------'
Private Sub Worksheet_BeforeRightClick(ByVal Target As Range, Cancel As Boolean)

  Select Case Target.Row
    Case 9 To 11
      Call EntryCheck(Target, Range("C9:BM11"))
    Case 22 To 24
      Call EntryCheck(Target, Range("C22:BM24"))
    Case Else
  End Select
  Cancel = True
  
End Sub

'----------------------------------------------------------'
Sub EntryCheck(ByVal Target As Range, ByVal recordArea As Range)

  Dim entryNumber As Long
  entryNumber = Application.WorksheetFunction.CountA(recordArea)
  
  Dim entryCheckArea As Range
  Set entryCheckArea = Intersect(recordArea, Columns(Target.Column))
  
  Dim alterationCheck As Range
  Set alterationCheck = entryCheckArea.Cells(3).Offset(1, 0)

  With Target
    .Value = "O"
    If .Offset(0, -2) <> "O" Then
      If entryNumber > 1 Then
        Cells(alterationCheck.Row, .Column) = "O"
      End If
    End If
  End With
  
End Sub

かなーりシンプルになりましたねw
 

さて、2年後の管理人がどう思うのか、楽しみですねw

でわまた~ ̄▽ ̄)ノシ

インターフェイスをかじってみた

今回も書きかけのシリーズ記事とは違うネタです。
んが、決して飽きたわけではありませんのです。
こんちくわ|ω・)ノ

さて、ノンプロ研の方がノンプロSlackVBAインターフェイスを使ったコードについて何か良い方法はないかなぁといった書き込みがありました。
インターフェイスについてはその書き込みをした方から以前いろいろと教えていただいていたのですが、実際にコードを書いたことがなかったので、勉強がてらチャレンジしてみることにしました。
それで、一定の理解を得ることができたので備忘録的な意味も含めて記事に残そうと思います。

でわいきます ̄▽ ̄)

目次

VBAインターフェイスって...

  • ナニ?
  • 何ができるの?
  • どうやって書くの?

  • まとめ



インターフェイスってナニ?

改めてググってみたら、thomさんの記事がHitしました。

thom.hateblo.jp

もうここにすべてが書かれているので私が改めて記事にする必要などないような気がしますが、あくまで自分自身の学習のためですので、お付き合いくださいましまし。

で、私なりのインターフェイス捉え方ですが、
VBAでいうインターフェイスとは、1本でいろんな家電を操作できる共通リモコンのようなもの だなぁと思いました。

つまり共通リモコンの各種操作ボタンは1組しか用意されていないけれども、リモコン上の切り替えスイッチを切り替えることで、テレビが操作できたり、ビデオが操作できたりするようになる。

もう少し突っ込むと、
テレビやビデオが電源ON/OFFを認識する信号は異なるけれども、1つしかないリモコンの電源ON/OFFボタンを押したときに発射される信号は、リモコンの手元の切り替えスイッチ操作によって、テレビやビデオに合わせて変化させることができる。

そう、インターフェイスとわ
入り口は1つだけど出口を複数に変化させる仕組み

なのだなと思いました。



VBAインターフェイスで何ができるの?

コレ重要ですよね。

入り口は1つだけど出口を複数に変化させる仕組み という概念から、いまのところ思いつくのは、

例えば、

  • エクセルでワークブックを開くコマンドは、Workbooks.Open を使いますが、ワードVBAでは、Documents.Open、パワーポイントではPresentations.Openとなるように、同じ操作でも言語によってコマンドは変わるものです。
    コレを一括して同じ(例えば)FileOpenみたいな共通コマンド化してつかうことができる

  • 異なるクラスモジュールに設定したそれぞれ機能が異なるメソッドセットを共通のメソッド名で使い分けるようになる。

っていう感じでしょうか。
他にもありそうですが、経験不足なのでちょっと思いつかないですね^^;



どうやって書くの?

インターフェイスを実装するには、クラスモジュールを使った記述が必要になります。

クラスってナニ?については前回の記事で触れましたが、概念のみ(しかも個人的な)で、具体的な書き方については一切触れていませんので、そこのあたりはコチラのシリーズ記事 tonari-it.com

を参考にしていただけると良いかと思います。。
 


と、いうわけでどう書くかですが、
ココでは例として、異なるクラスモジュールに設定したそれぞれ機能が異なるメソッドセットを共通のメソッド名で使い分ける方法(上に書いた使用例の2番目ですね)について書きます。

別々のクラスモジュールにそれぞれ定義した3つのメソッドを使い分ける

クラスモジュールを合計4つを挿入して以下のようにオブジェクト名を設定します。

[f:id:FukuCyndiP:20191022161819p:plain:w350

上からそれぞれ異なるメソッドセットを入れるFuncSetA, FuncSetB, FuncSetCと、共通リモコンインターフェイスとして共通のメソッド名を定義するInterFaceAです。

通化したいメソッド名をInterFaceAに定義します

(クラスモジュールInterFaceAに記述)

Option Explicit
'インターフェイスで共通化したいメソッドを記述します
'名前のみでおk

Public Sub Greet()
End Sub

Public Sub Response()
End Sub

ここでは名前だけで、中身は必要ありません。

メソッドセットを作ります

ココでは各メソッドの内容も含めてコードを記述していきますが、以下の3つのお約束に従ってコードを書かなくてはいけません

(クラスモジュール: FuncSetAの内容)

Option Explicit

Implements InterfaceA

Public Sub InterfaceA_Greet()
  MsgBox "おじゃまします`"
End Sub

Public Sub InterfaceA_Response()
  MsgBox "どうぞ~`"
End Sub


  1. モジュールの宣言部に”Implements"+実装するインターフェイス名を記述する( Implements InterfaceA)
  2. 各メソッド名は実装したいインターフェイス名と(クラスモジュールInterfaceAで)宣言したメソッド名をアンダースコア( _ ) でつないだものにする
    ( InterfaceA_Greet(), InterfaceA_Response()にします)
  3. メソッドはInterfaceA で宣言した共通化したいメソッドをすべて使います(足りなくても、多すぎてもエラーになります)



上記お約束に従って残りのモジュール(FuncSetC, FuncSetCにもメソッドを記載します)

(クラスモジュール: FuncSetbBの内容)

Option Explicit

Implements InterfaceA

Public Sub InterfaceA_Greet()
  MsgBox "じゃまするでぇ"
End Sub

Public Sub InterfaceA_Response()
  MsgBox "じゃまするんやったら、帰って~
End Sub



(クラスモジュール: FuncSetCの内容)

Option Explicit

Implements InterfaceA

Public Sub InterfaceA_Greet()
  MsgBox "おじゃまします・・・か?"
End Sub

Public SubInterfaceA_Response()
  MsgBox "なんで聞くねん! 「か」いらんやろ!"
End Sub



これでインターフェイスを使う準備は完了ですので、これを使うコードを書いていきましょう

インターフェイスを使うコードを書く

これは標準モジュールに書きます。

Option Explicit

Sub Ippann()
'クラスモジュール名(FuncSetA)を変えることで
'Greet, Response メソッドの処理内容が変化します

  Dim c As InterfaceA
  Set c = New FuncSetA
  Call c.Greet
  Call c.Response

End Sub



ココではInterfaceA(型?)で宣言した変数cを準備して、クラスモジュールFuncSetAから生成したオブジェクト(インスタンス)を代入しています。

このプロシージャを実行すると、

f:id:FukuCyndiP:20191022185408p:plain:w200

f:id:FukuCyndiP:20191022185422p:plain:w200

と、クラスモジュールFuncSetAに準備した Greet(), Response()メソッドが無事実行されました。

じゃぁ、残りのメソッドに切り変えるにはどうするかというと、Set c = New FuncSetAのクラスモジュール名FuncSetAの部分を書き換えればおkです。


Set c = New FuncSetBと書き換えたなら、

f:id:FukuCyndiP:20191022190256p:plain:w200

f:id:FukuCyndiP:20191022190309p:plain:w250



Set c = New FuncSetBと書き換えたなら、

f:id:FukuCyndiP:20191022190502p:plain:w200

f:id:FukuCyndiP:20191022190518p:plain:w300

となります。
このようにインターフェイスを使うことで、入り口は1つだけど出口を複数に変化させる仕組み を作ることができましたね

まとめ

今回は入り口は1つだけど出口を複数に変化させる仕組みとしてVBAで作成するインターフェイスについて書きました。
この仕組みをどのように応用するか?ということについては、私にはまだまだアイデア不足なところもあります。ただ、入り口は1つだけど出口を複数に変化させる仕組みがVBAで作れるということだけアタマの隅に置いておけば、コードのロジックを考える際の引き出しの一つとして利用できる場面もあるかと思います。
すでに他のブログでもいくつかの使い方が紹介されていますので、参考にしてみてはいかがでしょうか?

thom.hateblo.jp

akashi-keirin.hatenablog.com



でわまた~ ̄▽ ̄)ノシ