えくせるちゅんちゅん

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

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

VBAには、環境に応じてプログラムの動きを変える「条件付きコンパイル」という仕組みがある。

この記事では、条件付きコンパイル(#Const / #If)とは何か説明し、その上で複数のモジュールで使用可能な独自のコンパイラ定数を定義しコンパイル制御(ソースコードの読み込まれる箇所)を簡単に切り替える方法を解説する。

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

一般的に最もよく見かける条件付きコンパイル制御と言えば、Officeのバージョンを識別して WindowsAPIのDeclare文を32bitと64bitの両方をサポートするために記述する次のような例だろう。

#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 を組み込みコンパイラ定数などと呼ぶ。

コンパイラ定数とは

コンパイラ定数とは、コンパイラソースコードコンパイルする対象を変えるために使用する特殊な値である。 (訳:コンパイラ定数とは、人間が読み書きしやすいVBAのようなソースコードを、コンピュータが実行可能なアセンブラ機械語といった形に翻訳する段階で、読み込べきソースコードを仕分けするための目印です)

ソースコードの大半を占める関数、定数、変数、制御文は翻訳される対象そのものであるのに対して、コメントアウトと同じように翻訳自体をするかどうかを識別するために上位の次元に存在するコードである。

コンパイラ定数にはVBAが元々用意している組み込みコンパイラ定数と、開発者が記述するユーザー定義コンパイラ定数がある。

組み込みコンパイラ定数には、先の VBA7 の他にも複数存在する。

  1. アプリケーションのビット数を識別する Win16 Win32 Win64 ※OSのビット数ではない点に注意
  2. VBAのバージョンを識別する Vba6 Vba7 ※VBA6はOffice2007以前、VBA7はOffice2010以降であることを示す
  3. OSを識別する MacMac OSであることを示す

詳しくは、docs - コンパイラ定数 に目を通してほしい。

一方で、ユーザー定義コンパイラ定数はモジュールの冒頭(宣言領域)に #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 ディレクティブで使用するためにある。

ディレクティブとは

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

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

※なぜ「1」なのか。なぜ型を指定しないのかは後述する。

では、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 ディレクティブはプログラムの翻訳を分岐させるのに使用するモノだというわけだ。

ようやくコンパイル制御の解説が終わったので、ようやく本記事の本体に入る。

#ConstをPublicで宣言できない問題

既に説明したように、独自のコンパイラ定数を宣言したいときは #Const を使えば良い。

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

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

普通の定数宣言 Const であれば Public をつけてプロジェクト内の全てのモジュールで使い回すことができるが、コンパイル定数 #Const では以下のように書いたプログラムはいずれも実行することはできない。

Public #Const IsExcel = 1
#Const Public IsExcel = 1
#PublicConst IsExcel = 1
#Public Const IsExcel = 1

したがって、全てのモジュールで条件付きコンパイル制御(#if)を使用するには、 各モジュールの冒頭に #Const を記述しなけばならず、条件値を変更するときは何箇所も修正しなければならない という、集中管理が中途半端にしかできない致命的な問題が存在する。

全てのモジュールで使えるパブリックなコンパイラ定数の作り方

前述のように、 #Const ではソースコード上でパブリックなコンパイラ定数を定義することは出来ない。

だが、組み込みコンパイラ定数の VBA7Win64 はモジュール関係なくグローバルに使用できているので、何らかの手法があるのではないかと考え、見つけ出すのに数年かかったがついに解決策を発見した。

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

この設定画面を開いた事がある人は少なくないはずだが、使っているプロジェクトも、解説している人も見たことがない。おそらく見過ごされているのだろう。

通常、条件付きコンパイル引数の欄には何も書かれていないが、例えば DEBUG_MODE = 1 と書くことによって、全てのモジュールに #Const DEBUG_MODE = 1 が記述されているのと同じ状態になる。

これにより #If DEBUG_MODE = 1 Then をどこのモジュールに書いても、モードに応じた切替が実現できるようになる。

(なお、未定義のコンパイラ定数は 0 が定義されているものとして扱われる。Option Explicitを書いても変数ではないので未定義の検知はされない)

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

メリットとデメリット

このように、VBAで条件付きコンパイル制御を行いたいとき、プロジェクトのプロパティに記述するため、ソースコード上にはコンパイラ定数が出現しない。

そのため、ソースコードの差分管理をする人にとっては、余計なノイズが減るためメリットとなる。

一方で、逆に機能を知らない人からすると、ソースコードをコピーしても動かないとか、開発環境のプログラムを本番環境に持って行ってもずっと開発モードで動いて原因不明の怪奇現象となりデメリットとなる。

世の中には「条件付きコンパイル引数」を利用している人がいるかもしれないため、もし #if を見かけた場合はプロジェクトのプロパティを確認することをおすすめする。

なぜ 0 や 1を代入するのか。なぜ型を指定しないのか

途中で紹介したコード #Const IsExcel=1 において、数字の1で型指定 As Integer も必要ないと書いたが、その理由について説明する。

そもそも、よく見かける #If VBA7 Then という書き方は、比較対象となる値を省略していると考えられる。

Microsoft公式Docsのコンパイラ定数 によると、VBA7にはOfficeが2010以降の場合にTrueが格納されると書かれているので、 #If VBA7=True Then と書いても良いはずである。ところがこの書き方だとVBA7が定義された環境でもコンパイル対象にはならない。

Sub TestVBA7()
    #If VBA7 = True Then
        VBA7がTrueのときコンパイルに含まれる・・・と思いきや含まれない
    #End If
End Sub

また、VBAのTrueを整数化すると -1 なので、If VBA7=-1 Then と書けば良いのかと思いきやそれもダメ。

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

実は、#If VBA7 = 1 Then と書いたらコンパイルに含まれるようになる

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

加えて、プロジェクトのプロパティの条件付きコンパイル引数に至っては、そもそもTrueやFalseを書くことすらできない。(ついでに "A" などの文字列も書けない)

また、 #ConstAs Integer などを書いてもコンパイルエラーになる。

従って、もし「真」を意味する値を定義するならば「1」にすべきと判断した。

それと、#If における右辺の省略については少し注意が必要である。

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

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

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

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


以上のコンパイラ定数の定義と制御についてまとめるとこなる。

  1. 組み込みコンパイル定数にはTrueが1、Falseが0として定義されている。
  2. #Const では真偽、整数、文字列が定義できるが型指定はできないし必要ない。
  3. 条件付きコンパイル引数には整数しか設定できない。
  4. #If による分岐では、普通のifと同じように右辺の値を省略でき、省略時は0ならfalse、0以外ならtrueと判断される。

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

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

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 は定義済みと検知していることが確認できたので、内部的にはまだまだイケルということになると思う。(方法は不明だが)

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

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

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

  1. グローバルなコンパイラ定数
    1. Office等のアプリケーションが用意している組み込み定数
    2. 全てのプロジェクトの全てのモジュールで使用できる
    3. VBAのバージョンを表す 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・)


コンパイラ定数を活用した分岐に関しては、こんな記事を書いてるので良ければ見ていってください。

www.excel-chunchun.com

www.excel-chunchun.com

www.excel-chunchun.com

プライバシーポリシー