えくせるちゅんちゅん

ことりがエクセルをちゅんちゅんするブログ

VBA関数のライブラリ名やモジュール名の省略を検知するアイディア

こんばんわ。最近自作のVBAモジュールのライブラリ化を夢見てアレコレしていることりちゅんです。

ライブラリ化を進めるに当たって、今まで適当に省略して済ませてきたモジュール名の明示が重要な課題となってきました。

そんな中、良い感じのアイディアが浮かんだので紹介しておきます。


一般VBAerとツヨツヨVBAerの違い?

本記事では暫定的に、

VBAユーザーの9割以上を占めると思われる普通の人・・・一般人

ごく一部の本格的にプログラミングをしている人・・・ツヨツヨ

と呼びます。

分かりやすいように省略しただけで、深い意味はありません。


VBAでは予約語を除く関数等は、全てライブラリ(プロジェクト)名.クラス(モジュール)名.メンバという構成になっています。(たぶん)

でもVBAでプログラミングする時は、いきなりメンバから書き始めても使えるように出来ています。


従って、普通の人は以下の様にコーディングします。

Sub Test1_1()
    Const TXT = "あいうえお"
    Dim s As String
    s = Left(TXT, 2)
    Debug.Print s    'あい と出力
End Sub


しかし、ツヨツヨな方の書いたソースコードを見ると、十中八九、以下のようにコーディングされています。

Sub Test1_2()
    Const TXT = "あいうえお"
    Dim s As String
    s = VBA.Strings.Left$(TXT, 2)
    Debug.Print s    'あい と出力
End Sub


違うのはs = VBA.Strings.Left$(TXT, 2)の部分。

この場合、VBAがライブラリ名でStringsがモジュール名になります。

LeftLeft$になっているのは、実は戻り値型を文字列型と明示することで高速化できるからだと思われます。

どの関数が何のモジュールに属しているかは、オブジェクトブラウザを見ればすぐに分かります。

f:id:Kotori-ChunChun:20191001233903p:plain


尚、先日の記事でもやりましたが、ライブラリ名.関数名(VBA.Left)や、モジュール名.関数名(Strings.Left)なんて表記もできます。

www.excel-chunchun.com


それにしても、毎回VBA.Strings.なんて入力するのは面倒です。

なんでこんなことをするんでしょう?


ライブラリ名.モジュール名を省略したことによる問題点

何故ツヨツヨな人は省略をしないのか、私には心の内は分からないのですが、ある程度想像は付きます。

  • 組み込み関数と同名の関数が作成されると、そちらを使用するようになるため。
  • 省略表記だと「参照設定の破損。プロジェクトの破損。VBAのバグ」などにより、環境に依存したコンパイルエラーが起こりやすいため。
  • 開発環境等の都合により省略できないようになっているため?
  • 私の知らない理由

(詳しい方、他に理由があれば教えて下さい)


なにはともあれ、私もツヨツヨなコーディングに挑戦することにしました。


省略された事を検知する方法

ツヨツヨコーディングするぜ!!!と宣言したは良いものの、膨大なソースコードから省略されている場所を探し出して置き換えていくのは困難を極めます。

一件づつ置換していくという方法もありますが、将来を考えるといつか絶対忘れます。


なんとかして、自動的に検知する方法はないだろうかと考えたところ、いつもお世話になっている方法が浮かびました。

下記の記事では、偽のプロパティで上書きするモジュールを作成する事で、Excel固有のRangeWorksheetsCellsなどを省略表記をするとコンパイルエラーが起こるようにしています。

qiita.com


そう、上書きすれば良いのです!!!

リライトです!


実は異なるプロジェクト(ライブラリ)で宣言された関数と、自身のプロジェクトで宣言された関数が同名の時、自身のプロジェクトの関数を優先する。という暗黙の了解があるんですよね。

この性質を生かして、VBAの組み込み関数も上書きしてしまえば良いわけです。


VBA組み込み関数の省略を機械的に検知する

というわけで、早速こんなプロシージャを作ってみました。

Sub IsArray(): End Sub

f:id:Kotori-ChunChun:20191002001335p:plain

大成功!

我ながら天才的な発想である

が、実は「予約語」となっている関数が多くて、完全にはブロックできていない。


自作関数の省略を検知する

私のアイディアは、組み込み関数だけでは終わらない。

自作したモジュールの省略も、この方法で検知できることに気がついてしまいました。


全ての関数を別のモジュールに複製するです。

そんな中、関数だけ別途定義しなおさなくても、モジュール丸ごと複製すれば良いのではないかと返事を頂き、ごもっともだと思いました。


再現プログラムは以下のような感じになります。

'kccLib_Stringモジュール
Function LeftStr(s, d)
    LeftStr = Split(s, d)(0)
End Function
'省略検知用モジュール(再定義。またはモジュールの複製)
Sub LeftStr(): End Sub
'利用者モジュール
Sub Test2_1()
    Const TXT = "あいうえお"
    Dim s As String
    s = LeftStr(TXT, "う")
    Debug.Print s
End Sub
'あい が出力


f:id:Kotori-ChunChun:20191002003627p:plain

大成功!


問題点1

自作関数の省略検知では、新しいプロシージャを作るたびにダミーのモジュールを更新しなければならないので、若干の手間がかかります。

本体のプロシージャが名変更されたり削除された場合に、変更内容が複製しなおさないとダミーのプログラムを呼んでしまうためコンパイルエラーが出ないという危険性があります。

従って、普段は記載せず、必要な時に複製して確認する。という使い方をすべきでしょう。

常駐させてもインテリセンスからは除外されないので、使えるかのように見えてしまいます。

f:id:Kotori-ChunChun:20191003064308p:plain

リアルタイムに常時保護するような使い方には向いていないと思われます。


また、ダミーの関数が呼ばれてしまう恐れもあるため、常駐させるなら停止するように以下の様にしておくと良さそうです。

Private Const ERRMSG = "このプロシージャは廃止されました"
Sub MyFunc(): Err.Raise 9999, , ERRMSG: End Sub
Sub Test_Subブロック()
    'Err.Raiseで実行時エラーが限界
    Call MyFunc
    
    'Sub によりブロックできる記述
'    Call MyFunc(1)
'    Dim v: v = MyFunc(1)
'     Set MyFunc = 1
'    Set MyFunc = Nothing
End Sub


追記:よく考えたら、Property Setで上書きしたほうが良さそうです。

Property Set MyFunc(dummy): End Property

この記法なら、元々関数として使われていた記法を完全にブロックできます。

Sub Test_Setブロック()

    'Property Set によりブロックできる記述
'    Call MyFunc
'    Call MyFunc(1)
'    Dim v: v = MyFunc(1)
'     Set MyFunc = 1

    'Err.Raiseで実行時エラーが限界
    'だが、元々が関数であればこの記法はあり得ない。
    Set MyFunc = Nothing
    
End Sub

ただ、これを追記するのは手間がかかりすぎるため、ソースコードを解析してこのコードを発生させるプログラムを用意したほうが良さそうです。

(気が向いたら作るかも?)


問題点2

実は異なるモジュールで宣言された関数と、自身のモジュールで宣言された関数が同名の時、自身のモジュールを優先する。という、これまた暗黙の了解があります。


だから以下のように同名関数を宣言した状態で利用すると、意図せぬ関数を呼んでしまかもしれないのです。

'利用者モジュール

'上書き
Function LeftStr(s, d)
    LeftStr = s
End Function

'利用者モジュール
Sub Test2_1()
    Const TXT = "あいうえお"
    Dim s As String
    s = LeftStr(TXT, "う")
    Debug.Print s
End Sub
'あい が出力

残念ながら、良い解決方法は浮かびませんでした。


が、ですね。これはこれで使えることに気が付きました。(問題3へ)


問題点3

現状の方法では、全てのモジュールで省略が禁止されてしまいます。

これは以下の全ての状況に言えることです。

  • Excelの組み込み関数の無効化
  • VBAの組み込み関数の無効化
  • 自作関数の無効化

ここまでの主張と逆行するみたいですが、省略を認めたい時に面倒という問題が発生します。


具体的には、

  • WEBから取得したソースコードを確認の為に実行したい場合
  • テスト用のプログラムで手軽に書きたい場合
  • Twitter投稿用に少しでも短く書きたい場合

なんかが上げられます。


そのためには先ほどあげた問題2の方法が使えます。

今度は使用を認めたいモジュールにPrivateラッパー関数やプロパティを記述すれば良いのです。

Private Property Get Rows(Optional v1, Optional v2) As Range
    Set Rows = Excel.Rows(v1, v2)
End Property

面倒ですが、省略を認めたいものだけ必要なテストモジュールの先頭に記載しておきましょう。


省略を認めない方法

実は自作のモジュール名の記述を強制するには、もう一つ良い方法があります。というか、こっちが正攻法です。


標準モジュールではなくクラスモジュールを作成し、オプションで「既定のインスタンス」にしておくことで、やりたいことが実現できると思います。


作り方を簡単に言うと、クラスモジュールを一旦ファイルにエクスポートして、テキストエディタで開き、

Attribute VB_PredeclaredId = False

Attribute VB_PredeclaredId = True

に変更してインポートし直せば完成です。


詳しい話は省略しますが、この方法でモジュール名の記載を強制することができるので便利です。


リアルタイムに保護したい場合は、この方法を使うべきでしょう。


組み込み関数の省略表記を無効化するモジュール

最後に今回作成した組み込み関数の省略禁止モジュールを記載する。

ドキュメントとオブジェクトブラウザを使って一通り網羅したつもりですが、足りないものがあれば教えて頂けると幸いです。

尚、VBAの「予約語」で上書きできないものが多数見つかったため、完全にはブロックできていない。

コメントアウトされているものが、予約後だったもの)


また、常時発動しないように条件付きコンパイルDISABLE_GLOBAL = 1を採用しているので注意されたし。

f:id:Kotori-ChunChun:20191002010536p:plain

'DisableVBAGlobal
' 組み込み関数の省略表記を禁止するモジュール
'
'えくせるちゅんちゅん
'
'2019/10/02 : 公開
'2019/10/03 : 訂正
'
'本モジュールを有効化したい場合は True 使いたくない場合は False を記載
#Const DISABLE_GLOBAL = True

#If DISABLE_GLOBAL Then
    
Option Explicit

'数学関数 VBA.Mathメンバ
'Public Sub Abs(): End Sub
Public Sub Atn(): End Sub
Public Sub Cos(): End Sub
Public Sub Exp(): End Sub
Public Sub Log(): End Sub
Public Sub Rnd(): End Sub
'Public Sub Sgn(): End Sub
Public Sub Sin(): End Sub
Public Sub Sqr(): End Sub
Public Sub Tan(): End Sub
Public Sub Round(): End Sub 'その他から移動

'データ型変換関数 VBA.Conversion
'Public Sub CBool(): End Sub
'Public Sub CByte(): End Sub
'Public Sub CCur(): End Sub
'Public Sub CDate(): End Sub
'Public Sub CDbl(): End Sub
'Public Sub CDec(): End Sub
'Public Sub CInt(): End Sub
'Public Sub CLng(): End Sub
'Public Sub CLngPtr(): End Sub   '追加
'Public Sub CSng(): End Sub
'Public Sub CStr(): End Sub
'Public Sub CVar(): End Sub
Public Sub CVDate(): End Sub
'Public Sub CVErr(): End Sub     '変換関数から移動
Public Sub Error(): End Sub     '変換関数から移動
'Public Sub Fix(): End Sub       '数学関数から移動
Public Sub Hex(): End Sub       '変換関数から移動
'Public Sub Int(): End Sub       '数学関数から移動
Public Sub MacID(): End Sub     'その他から移動
Public Sub Oct(): End Sub       '変換関数から移動
Public Sub Str(): End Sub       '変換関数から移動
Public Sub Val(): End Sub       '変換関数から移動

'文字列関数 VBA.Strings
Public Sub Asc(): End Sub       '変換関数から移動
Public Sub AscB(): End Sub      '変換関数から移動
Public Sub AscW(): End Sub      '変換関数から移動
Public Sub Chr(): End Sub       '変換関数から移動
Public Sub ChrB(): End Sub      '変換関数から移動
Public Sub ChrW(): End Sub      '変換関数から移動
Public Sub Filter(): End Sub    'その他から移動
Public Sub Format(): End Sub    '変換関数から移動
Public Sub FormatCurrency(): End Sub    'その他から移動
Public Sub FormatDateTime(): End Sub    'その他から移動
Public Sub FormatNumber(): End Sub      'その他から移動
Public Sub FormatPercent(): End Sub     'その他から移動
Public Sub InStr(): End Sub
Public Sub InStrB(): End Sub
Public Sub InStrRev(): End Sub
Public Sub Join(): End Sub
Public Sub LCase(): End Sub
Public Sub Left(): End Sub
Public Sub LeftB(): End Sub
'Public Sub Len(): End Sub
'Public Sub LenB(): End Sub
Public Sub Ltrim(): End Sub
Public Sub Mid(): End Sub
Public Sub MidB(): End Sub
Public Sub MonthName(): End Sub
Public Sub Replace(): End Sub
Public Sub Right(): End Sub
Public Sub RightB(): End Sub
Public Sub Rtrim(): End Sub
Public Sub Space(): End Sub
Public Sub Split(): End Sub
Public Sub StrComp(): End Sub
Public Sub StrConv(): End Sub
'Public Sub String(): End Sub
Public Sub StrReverse(): End Sub
Public Sub Trim(): End Sub
Public Sub UCase(): End Sub
Public Sub WeekdayName(): End Sub

'VBA.[_HiddenModule]
'Public Sub Array(): End Sub
'Public Sub Input(): End Sub
'Public Sub InputB(): End Sub
Public Sub ObjPtr(): End Sub    '追加
Public Sub StrPtr(): End Sub    '追加
Public Sub VarPtr(): End Sub    '追加
Public Sub Width(): End Sub     '追加

'VBA.Information
Public Sub Erl(): End Sub       '追加
Public Sub Err(): End Sub       '追加
Public Sub IMEStatus(): End Sub
Public Sub IsArray(): End Sub
Public Sub IsDate(): End Sub
Public Sub IsEmpty(): End Sub
Public Sub IsError(): End Sub
Public Sub IsMissing(): End Sub
Public Sub IsNull(): End Sub
Public Sub IsNumeric(): End Sub
Public Sub IsObject(): End Sub
Public Sub QBColor(): End Sub
Public Sub RGB(): End Sub
Public Sub TypeName(): End Sub
Public Sub VarType(): End Sub

'VBA.Interaction
Public Sub AppActivate(): End Sub     '追加
Public Sub Beep(): End Sub     '追加
Public Sub CallByName(): End Sub
Public Sub Choose(): End Sub
Public Sub Command(): End Sub
Public Sub CreateObject(): End Sub
Public Sub DeleteSetting(): End Sub     '追加
'Public Sub DoEvents(): End Sub
Public Sub Environ(): End Sub
Public Sub GetAllSettings(): End Sub
Public Sub GetObject(): End Sub
Public Sub GetSetting(): End Sub
Public Sub IIf(): End Sub
Public Sub InputBox(): End Sub
Public Sub MacScript(): End Sub
Public Sub MsgBox(): End Sub
Public Sub Partition(): End Sub
Public Sub SaveSetting(): End Sub   '追加
Public Sub SendKeys(): End Sub      '追加
Public Sub Shell(): End Sub
Public Sub Switch(): End Sub

'VBA.FileSystem
Public Sub ChDir(): End Sub      '追加
Public Sub ChDrive(): End Sub      '追加
Public Sub CurDir(): End Sub
Public Sub Dir(): End Sub
Public Sub EOF(): End Sub
Public Sub FileAttr(): End Sub
Public Sub FileCopy(): End Sub      '追加
Public Sub FileDateTime(): End Sub
Public Sub FileLen(): End Sub
Public Sub FreeFile(): End Sub
Public Sub GetAttr(): End Sub
Public Sub Kill(): End Sub      '追加
Public Sub Loc(): End Sub
Public Sub LOF(): End Sub
Public Sub MkDir(): End Sub      '追加
Public Sub Reset(): End Sub      '追加
Public Sub RmDir(): End Sub      '追加
'Public Sub Seek(): End Sub
Public Sub SetAttr(): End Sub      '追加

'VBA.DateTime
Public Sub Calendar(): End Sub      '追加
'Public Sub Date(): End Sub
Public Sub DateAdd(): End Sub
Public Sub DateDiff(): End Sub
Public Sub DatePart(): End Sub
Public Sub DateSerial(): End Sub
Public Sub DateValue(): End Sub
Public Sub Day(): End Sub
Public Sub Hour(): End Sub
Public Sub Minute(): End Sub
Public Sub Month(): End Sub
Public Sub Now(): End Sub
Public Sub Second(): End Sub
Public Sub Time(): End Sub
Public Sub Timer(): End Sub
Public Sub TimeSerial(): End Sub
Public Sub TimeValue(): End Sub
Public Sub Weekday(): End Sub
Public Sub Year(): End Sub

'VBA.Financial
Public Sub DDB(): End Sub
Public Sub FV(): End Sub
Public Sub IPmt(): End Sub
Public Sub IRR(): End Sub
Public Sub MIRR(): End Sub
Public Sub NPer(): End Sub
Public Sub NPV(): End Sub
Public Sub Pmt(): End Sub
Public Sub PPmt(): End Sub
Public Sub PV(): End Sub
Public Sub Rate(): End Sub
Public Sub SLN(): End Sub
Public Sub SYD(): End Sub

'該当ライブラリ無し
Public Sub LoadPicture(): End Sub
'Public Sub Spc(): End Sub
'Public Sub Tab(): End Sub
'Public Sub LBound(): End Sub
'Public Sub UBound(): End Sub
    
#End If


参考資料

docs.microsoft.com

software.opensquare.net

qiita.com

kazusa-pg.com

自作関数については、既出のアイディアであったことが判明した。


まとめ

ソースコードを極力省略しない記法に揃えることで、バグの起こりにくい強靭なプログラムを作ることができる。

思わぬ所で時間を食ってしまったが、これでまた一歩、私の野望に近づいたと信じたい。

以上


何か御座いましたらコメント欄、またはTwitterからどうぞ♪

それではまた来週♪ ちゅんちゅん(・8・)