えくせるちゅんちゅん

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

MENU

VBAでパブリックなコンパイラ定数を定義して条件付きコンパイル制御を行う

VBAには、環境に応じてソースコードの使用する箇所を変える「条件付きコンパイル」という仕組みがある。

この記事では、独自のコンパイラ定数を定義し全てのモジュールのコンパイルを一括で制御する方法を解説する。

よく見かけるWindowsAPIのコンパイル制御

もっともよく見かける条件付きコンパイルは、Officeのバージョンを判断して WindowsAPIの32bit//64bit宣言文を切り替える次のようなDeclare文 だと思われる。

#If VBA7 Then
    '32bit / 64bit対応 ※Excel2010以降限定
    Declare PtrSafe Function SetForegroundWindow Lib "User32" (ByVal hWnd As LongPtr) As Long
#Else
    '32bit限定
    Declare Function SetForegroundWindow Lib "User32" (ByVal hWnd As Long) As Long
#End If

この例で登場する VBA7 というのは、グローバル(全てのプロジェクト/全てのモジュール)で使用可能な コンパイラ定数 である。

コンパイラ定数とは

コンパイラ定数とは、コンパイラソースコードコンパイルする時に使用するための、普通のConstとは違った役割の定数である。

先の VBA7 はアプリケーションに組み込み済みのコンパイラ定数だが、 #Const 文によって自作することもできる。

' このソースコードはExcelであると定義
' (Excel以外のVBAでは動かしたくないプログラムがある)
#Const IsExcel = 1

コンパイラ定数は、 Const 文で定義される普通の定数とは全くの別物である。

例えば、オブジェクトブラウザでVBA7IsExcel を検索しても、メンバーとして表示されることはないし、変数や定数のように使おうとすると定義されていませんとエラーが出る。

Option Explicit

' このソースコードはExcelであると定義
' (Excelではないとき動かしたくないプログラムがある)
#Const IsExcel = 1

Sub TestIsExcel()
       'コンパイル エラー:変数が定義されていません。
    Debug.Print IsExcel
End Sub

では、コンパイラ定数は何のために存在するのか。

コンパイラ定数は #If...Then...#Else...#End If ディレクティブで使用するためにある。

ディレクティブとは

ディレクティブとは、コンパイラソースコードコンパイル(PCが読んで意味を理解して実行できる状態に翻訳する)ときに、ソースコードを読み飛ばすための仕組みである。

#If VBA7 Then#If IsExcel=1 Then のように使用する。

では、if#if で何が違うのか?と思うかもしれない。その疑問は、以下のコードを見れば理解いただけると思う。

Option Explicit

' このソースコードはExcelであると定義
#Const IsExcel = 1

#If IsExcel = 0 Then
    IsExcelが未定義か0ならこの文字でコンパイルエラーが出ます
#End If

Sub Test()
    MsgBox "Hello World"
End Sub

このコードでTestを実行した場合は、普通に実行されてメッセージボックスが表示される。

ところが、 #Const IsExcel = 1コメントアウトしたり 0 に書き換えて実行すると何らかのコンパイルエラーが表示される。

---------------------------
Microsoft Visual Basic for Applications
---------------------------
コンパイル エラー:

プロシージャの外では無効です。
---------------------------
OK   ヘルプ   
---------------------------

試すまでもないことだが、これを普通の Const と If で記述するとコンパイル段階で プロシージャの外では無効です。 が表示される。(余分な言葉がどのようなエラーになるかは、構文次第なので決められたコンパイルエラーが出るわけではない。)

Option Explicit

' このソースコードはExcelであると定義
Const IsExcel = 1

'↓Subの外でIfは使えない
If IsExcel = 0 Then
    コンパイルがここまでたどり着かない
End If

Sub Test()
    MsgBox "Hello World"
End Sub

条件付きコンパイル制御というのは、条件付きのコメントアウトと同義なのである。

If...Then..Else...End If がプログラムの実行時に分岐させるのに対して、#If...Then...#Else...#End If ディレクティブはプログラムの翻訳を分岐させるのに使用するモノだというわけだ。

#ConstにPublicを書けない問題

ここまで説明して、ようやく本題の序章に入ることが出来る。

既に説明したように、独自のコンパイラ定数で分岐を行いたいときは #Const を使用すれば良い。

' このソースコードはExcelであると定義
' (Excelではないとき動かしたくないプログラムがある)
#Const IsExcel = 1

しかし、 #Const で定義した IsExcelは、定義したモジュール内でしか使用することができない。

しかも、 Const のように、 Public をつけて、プロジェクト内の全てのモジュールで使い回せるようにすることができないのある。

'以下 記述不可
Public #Const IsExcel = 1
#Const Public IsExcel = 1
#PublicConst IsExcel = 1
#Public Const IsExcel = 1

このままでは全てのモジュールで同じコードを記述して、変更するときは何箇所も修正しなければならなくなる。

パブリックなコンパイラ定数の作り方

というわけで本題である。

ソースコード上でパブリックなコンパイラ定数を定義することは出来ない。だが、使えないとは一言も言っていないのである。

ちゃんと「やり方」は存在する。

見たことがあるはずだが、大多数の人がスルーしている。

「プロジェクトのプロパティ」の「条件付きコンパイル引数」である。

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

通常、条件付きコンパイル引数は空欄だが、 DEBUG_MODE = 1 と書くことによって、コンパイラ定数 DEBUG_MODE1 が格納された状態になる。(未定義のコンパイラ定数は 0 である)

あとは #Const 同様 #If に使用するだけで、全てのモジュールでモード切替が実現できる。というわけである。

Sub Test()
    #If DEBUG_MODE = 1 Then
        Debug.Print "デバッグモード"
    #Else
        Debug.Print "本番モード"
    #End If
End Sub

余談: If VBA7 Then は省略表記かもしれない

これは余談だが、よく見かける #If VBA7 Then という書き方は、 #If の暗黙的な挙動に任せた書き方であり、厳密な書き方ではないという可能性が浮上した。

たとえば、VBA7が定義された環境において、 #If VBA7=1 と書いたら真として実行される。(※VBAのTrueは-1ですが、組み込みコンパイラ定数に入っているTrueの値は1のようです)

Sub TestVBA7()
    #If VBA7 = 1 Then
        VBA7が1のときコンパイルに含まれる
    #End If
End Sub

コンパイラ定数 のページに記載されているとおり、Microsoft公式の言い分としては True/False が代入されているらしいので If 真偽 Then で十分だと言いたいのだと思うが、以下の理由から自作コンパイラ定数においては注意が必要である。

  1. #Const では、特に True/False を強制されておらず、整数値を指定することができる。
  2. 条件付きコンパイル引数には True/False と書くとエラーが表示され、整数で指定しなければ設定が保存できない。
  3. 現に #If の分岐で、 True (1)False (0) 以外の数値を使った分岐が可能である。

VBA7が定義された環境において #If VBA7 Thenという記述は、即ち #If 1 Then と記述したということに等しく、普通の If の条件式と同じ 0 なら 0以外 なら という挙動によって成り立っていると考えられる。

つまり、省略表記だと1以外も真としてしまう危険性がある。

とは言え、組み込みコンパイラ定数である Vba7 Win64 Mac に変な値が入ることは考えにくいので無視して全く問題ないと言える。

しかし、独自で定義しているコンパイラ定数の場合は、0,1以外の数値を使いたい場合があるので、 自作のコンパイラ定数の条件式は明示的に書いたほうが間違いない と言えるだろう。

複数のコンパイラ定数の書き方

さて、この条件付きコンパイル引数だが、一つ懸念がある。

1行のテキストボックスなので、複数の定義が書けないのだ。

この懸念は、 マルチステートメント記法(コロン)によって解決できる。

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

ここで、無茶な書き方をすると、OKを押したときに 定数宣言の構文が正しくありません。 が発生する。何も出なければ設定成功というわけである。

---------------------------
Microsoft Visual Basic for Applications
---------------------------
定数宣言の構文が正しくありません。
---------------------------
OK   ヘルプ   
---------------------------

条件付きコンパイル引数の限界

ところで、条件付きコンパイル引数の限界が気になったので試してみたところ、文字数は500文字がテキストボックスの上限だと言うことが分かった。

AA=1:AB=1:AC=1:AD=1:AE=1:AF=1:AG=1:AH=1:AI=1:AJ=1:AK=1:AL=1:AM=1:AN=1:AO=1:AP=1:AQ=1:AR=1:AS=1:AT=1:AU=1:AV=1:AW=1:AX=1:AY=1:AZ=1:BA=1:BB=1:BC=1:BD=1:BE=1:BF=1:BG=1:BH=1:BI=1:BJ=1:BK=1:BL=1:BM=1:BN=1:BO=1:BP=1:BQ=1:BR=1:BS=1:BT=1:BU=1:BV=1:BW=1:BX=1:BY=1:BZ=1:CA=1:CB=1:CC=1:CD=1:CE=1:CF=1:CG=1:CH=1:CI=1:CJ=1:CK=1:CL=1:CM=1:CN=1:CO=1:CP=1:CQ=1:CR=1:CS=1:CT=1:CU=1:CV=1:CW=1:CX=1:CY=1:CZ=1:DA=1:DB=1:DC=1:DD=1:DE=1:DF=1:DG=1:DH=1:DI=1:DJ=1:DK=1:DL=1:DM=1:DN=1:DO=1:DP=1:DQ=1:DR=1:DS=1:DT=1:DU=1:DV=1:

一度、末尾のコロンを取り除いて確定した後、再び開くと AA = 1 : AB = 1のようにスペースが追加されたので、実際には897文字を押し込むことに成功した。

AA = 1 : AB = 1 : AC = 1 : AD = 1 : AE = 1 : AF = 1 : AG = 1 : AH = 1 : AI = 1 : AJ = 1 : AK = 1 : AL = 1 : AM = 1 : AN = 1 : AO = 1 : AP = 1 : AQ = 1 : AR = 1 : AS = 1 : AT = 1 : AU = 1 : AV = 1 : AW = 1 : AX = 1 : AY = 1 : AZ = 1 : BA = 1 : BB = 1 : BC = 1 : BD = 1 : BE = 1 : BF = 1 : BG = 1 : BH = 1 : BI = 1 : BJ = 1 : BK = 1 : BL = 1 : BM = 1 : BN = 1 : BO = 1 : BP = 1 : BQ = 1 : BR = 1 : BS = 1 : BT = 1 : BU = 1 : BV = 1 : BW = 1 : BX = 1 : BY = 1 : BZ = 1 : CA = 1 : CB = 1 : CC = 1 : CD = 1 : CE = 1 : CF = 1 : CG = 1 : CH = 1 : CI = 1 : CJ = 1 : CK = 1 : CL = 1 : CM = 1 : CN = 1 : CO = 1 : CP = 1 : CQ = 1 : CR = 1 : CS = 1 : CT = 1 : CU = 1 : CV = 1 : CW = 1 : CX = 1 : CY = 1 : CZ = 1 : DA = 1 : DB = 1 : DC = 1 : DD = 1 : DE = 1 : DF = 1 : DG = 1 : DH = 1 : DI = 1 : DJ = 1 : DK = 1 : DL = 1 : DM = 1 : DN = 1 : DO = 1 : DP = 1 : DQ = 1 : DR = 1 : DS = 1 : DT = 1 : DU = 1 : DV = 1

この状態で DV は定義済みとなったので、内部的にはまだまだイケルということになると思う。

まあ、こんなに定義することはないので、気にしなくても良いくらい潤沢な枠があるという解釈で良いだろう。

まとめ:コンパイラ定数の種類

最後にスコープ毎のコンパイラ定数の違いをまとめる。

  1. グローバルなコンパイラ定数
    1. Office等のアプリケーションが用意している組み込み定数
    2. 全てのプロジェクトで使用できる
    3. バージョンを表す Vba6Vba7
    4. 環境を表す Win16Win32Win64Mac
  2. パブリックなコンパイラ定数
    1. プロジェクトのプロパティの条件付きコンパイル引数で設定しファイル毎に保存される
    2. 全てのモジュールで使用できる
    3. 好きな名前の定数を整数値で定義できる
    4. コロンで連結して複数定義できる
  3. プライベートなコンパイラ定数
    1. モジュールの宣言領域へ #Const ステートメントで記述する
    2. モジュール内でのみ使用できる
    3. 好きな名前の定数を整数値で定義できる( True/False も設定できるが、内部的には -1/0 として記憶される)

おまけ:条件付きコンパイル引数の活用事例

個人的な活用事例を紹介する。

  • DEBUG_MODE = 1 : 開発中とリリース後で Debug.Print 等のログ出力や、警告時の Stop の有無を変化させる
  • ユーザー環境で、ワークブックの保存確認メッセージを抑制する。 Saved = True
  • 開発中に、廃止予定の関数の使用の可否を切り替える。
  • レイトバインディング(CreateObject)方式とアーリーバインディング(参照設定)方式の切り替え
  • IsExcel IsAccess IsWord : Officeアプリによって異なるプロパティの取得(例えば ThisWorkbook.PathCurrentProject.Path
  • ネタ:超長文のコメントアウトの代わり(差分管理するときにチョット便利)

モジュールコード上でコンパイラ定数を定義しないことで、ソースコードを差分管理をするときに余分な差異として検知されないとか、モジュールをアップデートしたときに挙動が変わらないという恩恵を受けることが出来ます。

最後に

これまでに条件付きコンパイル引数の解説をしている記事を読んだことが無かったので、勢いで書いてみました。

改めて公式を読んで挙動を確認しましたが、意外と書かれていないことがあって勉強になりました。

条件付きコンパイルを必要とする人はあまりいないかもしれませんが、お役に立てれば幸いです。

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

プライバシーポリシー