VBAには、環境に応じてソースコードの使用する箇所を変える「条件付きコンパイル」という仕組みがある。
この記事では、独自のコンパイラ定数を定義し全てのモジュールのコンパイルを一括で制御する方法を解説する。
- よく見かけるWindowsAPIのコンパイル制御
- コンパイラ定数とは
- ディレクティブとは
- #ConstにPublicを書けない問題
- パブリックなコンパイラ定数の作り方
- 余談: If VBA7 Then は省略表記かもしれない
- 複数のコンパイラ定数の書き方
- 条件付きコンパイル引数の限界
- まとめ:コンパイラ定数の種類
- おまけ:条件付きコンパイル引数の活用事例
- 最後に
よく見かける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
文で定義される普通の定数とは全くの別物である。
例えば、オブジェクトブラウザでVBA7
や IsExcel
を検索しても、メンバーとして表示されることはないし、変数や定数のように使おうとすると定義されていませんとエラーが出る。
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
このままでは全てのモジュールで同じコードを記述して、変更するときは何箇所も修正しなければならなくなる。
パブリックなコンパイラ定数の作り方
というわけで本題である。
ソースコード上でパブリックなコンパイラ定数を定義することは出来ない。だが、使えないとは一言も言っていないのである。
ちゃんと「やり方」は存在する。
見たことがあるはずだが、大多数の人がスルーしている。
「プロジェクトのプロパティ」の「条件付きコンパイル引数」である。
通常、条件付きコンパイル引数は空欄だが、 DEBUG_MODE = 1
と書くことによって、コンパイラ定数 DEBUG_MODE
に 1
が格納された状態になる。(未定義のコンパイラ定数は 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
で十分だと言いたいのだと思うが、以下の理由から自作コンパイラ定数においては注意が必要である。
#Const
では、特にTrue/False
を強制されておらず、整数値を指定することができる。- 条件付きコンパイル引数には
True/False
と書くとエラーが表示され、整数で指定しなければ設定が保存できない。 - 現に
#If
の分岐で、True (1)
とFalse (0)
以外の数値を使った分岐が可能である。
VBA7が定義された環境において #If VBA7 Then
という記述は、即ち #If 1 Then
と記述したということに等しく、普通の If
の条件式と同じ 0
なら 偽
、 0以外
なら 真
という挙動によって成り立っていると考えられる。
つまり、省略表記だと1以外も真としてしまう危険性がある。
とは言え、組み込みコンパイラ定数である Vba7 Win64 Mac に変な値が入ることは考えにくいので無視して全く問題ないと言える。
しかし、独自で定義しているコンパイラ定数の場合は、0,1以外の数値を使いたい場合があるので、 自作のコンパイラ定数の条件式は明示的に書いたほうが間違いない と言えるだろう。
複数のコンパイラ定数の書き方
さて、この条件付きコンパイル引数だが、一つ懸念がある。
1行のテキストボックスなので、複数の定義が書けないのだ。
この懸念は、 マルチステートメント記法(コロン)によって解決できる。
ここで、無茶な書き方をすると、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
は定義済みとなったので、内部的にはまだまだイケルということになると思う。
まあ、こんなに定義することはないので、気にしなくても良いくらい潤沢な枠があるという解釈で良いだろう。
まとめ:コンパイラ定数の種類
最後にスコープ毎のコンパイラ定数の違いをまとめる。
- グローバルなコンパイラ定数
- Office等のアプリケーションが用意している組み込み定数
- 全てのプロジェクトで使用できる
- バージョンを表す
Vba6
、Vba7
- 環境を表す
Win16
、Win32
、Win64
、Mac
- パブリックなコンパイラ定数
- プロジェクトのプロパティの条件付きコンパイル引数で設定しファイル毎に保存される
- 全てのモジュールで使用できる
- 好きな名前の定数を整数値で定義できる
- コロンで連結して複数定義できる
- プライベートなコンパイラ定数
- モジュールの宣言領域へ
#Const
ステートメントで記述する - モジュール内でのみ使用できる
- 好きな名前の定数を整数値で定義できる(
True/False
も設定できるが、内部的には-1/0
として記憶される)
- モジュールの宣言領域へ
おまけ:条件付きコンパイル引数の活用事例
個人的な活用事例を紹介する。
DEBUG_MODE = 1
: 開発中とリリース後でDebug.Print
等のログ出力や、警告時のStop
の有無を変化させる- ユーザー環境で、ワークブックの保存確認メッセージを抑制する。
Saved = True
- 開発中に、廃止予定の関数の使用の可否を切り替える。
- レイトバインディング(CreateObject)方式とアーリーバインディング(参照設定)方式の切り替え
IsExcel
IsAccess
IsWord
: Officeアプリによって異なるプロパティの取得(例えばThisWorkbook.Path
とCurrentProject.Path
)- ネタ:超長文のコメントアウトの代わり(差分管理するときにチョット便利)
モジュールコード上でコンパイラ定数を定義しないことで、ソースコードを差分管理をするときに余分な差異として検知されないとか、モジュールをアップデートしたときに挙動が変わらないという恩恵を受けることが出来ます。
最後に
これまでに条件付きコンパイル引数の解説をしている記事を読んだことが無かったので、勢いで書いてみました。
改めて公式を読んで挙動を確認しましたが、意外と書かれていないことがあって勉強になりました。
条件付きコンパイルを必要とする人はあまりいないかもしれませんが、お役に立てれば幸いです。
それでは、また来週?ちゅんちゅん(・8・)