VBAには、環境に応じてソースコードの使用する箇所を変える「条件付きコンパイル」という仕組みがある。
この記事では、独自のコンパイラ定数を新たに定義し、複数のモジュールのコンパイル制御を一括で行う方法を解説する。
- よく見かけるWindowsAPIのコンパイル制御
- コンパイラ定数とは
- ディレクティブとは
- #ConstにPublicを書けない問題
- 全てのモジュールで使えるパブリックなコンパイラ定数の作り方
- 余談: If VBA7 Then は #If VBA7=1 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
を組み込みコンパイラ定数などと呼ぶ。
コンパイラ定数とは
コンパイラ定数とは、コンパイラがソースコードをコンパイル(構文解析や翻訳)する動作に変化を与えるために使われる特殊な定数である。
コメントアウトと同じように、ソースコードの大半を占める関数、定数、変数と比較して上位の次元に存在する強い存在である。
先のソースコードで紹介したように、組み込みコンパイラ定数には、アプリケーションのビット数を識別する Win16
Win32
Win64
と、VBAのバージョンを識別する Vba6
Vba7
と、Mac OSであることを識別する Mac
などがある。
詳しくは、docs - コンパイラ定数 に目を通してほしい。
一方で、#Const
文によってコンパイラ定数を自作することもできる。
' このソースコードはExcel向けであると定義 ' (Excel以外のVBAでは動かせないプログラムがあるため) #Const IsExcel = 1
これだけ見ると Constと同じように見えるが、 #Const で定義されるコンパイラ定数は 普通の定数とは全くの別物である。
例えば、オブジェクトブラウザでVBA7
や IsExcel
を検索しても、メンバーとして表示されることはないし、変数や定数のように使おうとすると定義されていませんとエラーが出る。
Option Explicit ' このソースコードはExcelであると定義 ' (Excelではないとき動かしたくないプログラムがある) #Const IsExcel = 1 Sub TestIsExcel() 'コンパイル エラー:変数が定義されていません。 Debug.Print IsExcel End Sub
では、コンパイラ定数は何のために存在するのか。
コンパイラ定数は #If...Then...#Else...#End If
ディレクティブで使用するためにある。
ディレクティブとは
ディレクティブとは、コンパイラがソースコードをコンパイル(コンピュータがコードを読んで意味を理解して実行できる状態に翻訳する)する前に、ソースコードを読み飛ばすための仕組みである。
#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を実行しようとしたとき、実際には次のソースコードだという認識でコンパイルして実行される。
Option Explicit Sub Test() MsgBox "Hello World" End Sub
結果としては、正常にTestが実行されてメッセージボックスが表示される。
一方で、#Const IsExcel = 1
を コメントアウトしたり 0
に書き換えて実行すると、次のソースコードが書かれているという認識で実行される。
Option Explicit IsExcelが未定義か0ならこの文章でコンパイルエラーが起きます Sub Test() MsgBox "Hello World" End Sub
結果としては、以下のコンパイルエラーが表示される。
--------------------------- Microsoft Visual Basic for Applications --------------------------- コンパイル エラー: プロシージャの外では無効です。 --------------------------- OK ヘルプ ---------------------------
行数は明記されないが、言われるまでもなく IsExcelが未定義か0ならこの文章でコンパイルエラーが起きます
という文章が原因である。この文章はVBAの文法に合っていないのだからエラーが出て当然だ。
いまさら試すまでもないことだが、上記のソースコードを普通の 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
ディレクティブはプログラムの翻訳を分岐させるのに使用するモノだというわけだ。
余談だが、VBA以外の言語(C言語など)でも同様の手法でコンパイル制御を行われていることが多いので、知っておくと役に立つかもしれない。
#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
したがって、 全てのモジュールの冒頭に全く同一の #Const を記述して、値を変更するときは何箇所も修正しなければならない という致命的な問題が発生する。
全てのモジュールで使えるパブリックなコンパイラ定数の作り方
というわけで本題である。
前述のように、 #Const
ではソースコード上でパブリックなコンパイラ定数を定義することは出来ない。
だが、組み込みコンパイラ定数の VBA7
や Win64
はモジュール関係なくグローバルに使用できているので、何らかの手法があるのではないかと私は考えた。
色々試した結果、数年がかりでついに解決策を発見した。
ちゃんと「やり方」が存在したのである。
「プロジェクトのプロパティ」の「条件付きコンパイル引数」である。
この設定項目は、多くの人が目にしたことがあるはずだが、使っているプロジェクトを見たことがない。おそらく見過ごされているのだろう。
通常、条件付きコンパイル引数は空欄だが、 DEBUG_MODE = 1
と書くことによって、コンパイラ定数 DEBUG_MODE
に 1
が格納された状態になる。(未定義のコンパイラ定数は常に 0
である)
あとは #If
に定数名をどこのモジュールに書いてもモード切替が実現できる。というわけである。
Sub Test() #If DEBUG_MODE = 1 Then Debug.Print "デバッグモード" #Else Debug.Print "本番モード" #End If End Sub
つまり、ソースコード上には挙動を変化させるためのコンパイラ定数が出現しない。 そのためソースコードの差分管理をする人にとっては大きなメリットとなる場合がある。(逆に機能を知らない人からすると、原因不明の怪奇現象となる可能性もあるが)
余談: If VBA7 Then
は #If VBA7=1 Then
の省略表記かもしれない
これは余談だが、よく見かける #If VBA7 Then
という書き方は、 #If
の暗黙的な挙動に任せた書き方であり、厳密な書き方ではないという可能性が浮上した。
たとえば、VBA7が定義された環境において、 #If VBA7 = 1 Then
と書いたら真として実行される。(※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等のアプリケーションが用意している組み込み定数
- 全てのプロジェクトの全てのモジュールで使用できる
- VBAのバージョンを表す
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・)
コンパイラ定数を活用した分岐に関しては、こんな記事を書いてるので良ければ見ていってください。