今回はVBAのレジストリ操作を快適にするクラスを作ってみたので紹介します。
設定値をレジストリで記憶する
自作したアドインやマクロの設置値を記憶する方法の一つとして、レジストリを使用する方法があります。
ここで言うレジストリとは、Windowsのシステムやその上で動いている様々なアプリケーションの設定を記憶しているデータベースのことです。
キーの保存先
レジストリは、普通のファイルと同じようにツリー構造で管理されています。
例えばデータを保存しているファイルのパスは、こんな感じですね。
C:\Users\USERNAME\blog\レジストリ操作を使いやすくしてみた.md
レジストリも同じようなモノで、例えばVBA設定のパスはココと決まっています。
HKEY_CURRENT_USER\Software\VB and VBA Program Settings\
レジストリではフォルダ毎にアクセス権が定められておりますが、このフォルダはExcelVBAでも編集できるようになっています。
ここではないフォルダのレジストリキーを変えるためには、大抵の場合は管理者権限が求められるため、VBAの特性上変更は困難になります。
※Excelを管理者権限で起動しなければならないため、PowerShellやvbsに任せたほうが良い。
一般的に、レジストリを変更するとOSが立ち上がらなくなるとか、自己責任で操作するようにとか脅し文句を書くのが通例ですが、知らない項目を適当に触らなければそんなことはあり得ません。
そっくりな名前があれば間違えるかもしれませんが、そんな状況見たことがありません。
結構、気軽に触っても大丈夫です。(って言うと叱られちゃう?)
ただ、手軽にバックアップ(エクスポート)が出来るので、触る前にはそれくらいはやっておきましょう。
PCが壊れたりはしなくとも、設定を変えたアプリの調子が悪くなるくらいなら有り得ますから。
VBAのレジストリ操作では、アプリケーション名、セクション、キーという3項目が要求されます。
アプリケーション名とセクションはGUI的に見れば単なるフォルダで、キーというのが値を格納しているファイルみたいなものです。
つまり、任意のキーを読み出すときのフルパスは
HKEY_CURRENT_USER\Software\VB and VBA Program Settings\アプリ名\セクション\キー
という感じの構成となります。
レジストリで設定を保存する理由
メリットは、
- 別途、設定ファイルが出力されない。
- 作業中のエクセルブック、VBAを汚さない。
- PC・条件が変わったら設定値が初期値になる。
- アドイン・マクロブックをPC内のどこに保存しても、コピーして増殖させても、同一の設定を使える
デメリットは、
- (開発者の心理的に)普段は触らないものなので怖い
- 一般ユーザーが設定値の削除ができないため、削除機能の実装が必要となる(アンインストーラの必要性)
- 単一のPCでアドインを複製したときに、異なる設定値をもたせられない
- 設定値を別のPCと共有できない(共有フォルダにツールを置いて共同利用する時の話。スキルがある人ならエクスポートして移動させることは可能)
という感じで、メリット・デメリットは同じくらいあります。
設定を保存する方法は色々ありますが、どれが一番ということはありません。
用途によって使い分ければ良いのです。しかし用途というのはユーザーの使い方次第で変わるため、開発者の意図で決まるとは限りません。
フリーソフトの設定で「レジストリに保存する」と「iniファイルに保存する」が選べる物が多いのは、こういった理由からです。
開発に至った経緯
さて、ここで白状しておくと、私はVBAの設定をレジストリに書き込んだことがありませんでした。
今までは設定値をiniやxmlへ書き込み、或いはブック埋め込みをしていました。
下記のアドインを開発した際に、レジストリの活用を思いついたので挑戦してみました。
Excelの入力規則のリスト選択を簡単にするアドインを作ってみた
一般的なVBAでのレジストリ操作
VBAでレジストリの設定を読み書きするステートメントは、たったの4種類だけです。
ステートメント | 効果 |
---|---|
SaveSetting | キーを書き込み |
GetSetting | キーを読み込み |
GetAllSettings | キーの一覧を取得 |
DeleteSetting | アプリ名、セクション、キーを削除 |
どのステートメントでも登場するのが、アプリ名、セクション、キーです。
指定項目 | 記事での呼び名 | レジストリエディタで見た時 |
---|---|---|
appname | アプリ名 | ツリー上位のフォルダ |
section | セクション | ツリー下位のフォルダ |
key | キー | 右ペインの項目の名前 |
キーを書き込み(SaveSetting)
SaveSetting appname, section, key, setting
指定した名前のキーへsetting
の値を書き込みます。
書き込みは基本的に上書きです。
下図は後ほど紹介するコードのTest_SaveSetting
を実行した結果をレジストリエディタで開いてみた様子です。
※レジストリエディタの起動コマンドは、Win → regedit → Enter
です。
キーを読み込み(GetSetting)
GetSetting(appname, section, key[, default])
指定したキーの値を読むだけです。
キーそのものが存在しない時のために、default
を指定できます。
省略できますが、アドインを初めて実行した時の値は重要であるため、大抵の場合は省略せずに使います。
注意しなければならないのは「キー」の名前が分からなければ読み込めないということで、そういうときは次のGetAllSettings
を使います。
キーの一覧の取得(GetAllSettings)
GetAllSettings(appname, section)
GetSettingでは「キー」が分からなければ読み込めませんでした。
バージョンアップ前後の互換性に対応しようとすると、キーが分からないということがあるかも
別アプリとの連携で、管理外のキーが増えていることもあるかも
配列のような形で
Key1
、Key2
、Key3
・・・という不特定多数のキーを作る場面もあるかも
そんなときに使います。
取得した結果はKey Valueの二次元配列となります。
arr(n, 1) | arr(n, 2) | |
---|---|---|
arr(1, m) | キー1 | 書き込む値1 |
arr(2, m) | キー2 | 書き込む値2 |
arr(3, m) | キー3 | 書き込む値3 |
削除(DeleteSetting)
DeleteSetting appname [, section] [, key]
レジストリのキーなどを削除します。
引数の有無によって、アプリ全体、セクション全体、特定のキーのみから選べます。
ところでこのステートメント、他所の記事やMicrosoft公式ではセクションが省略不能と記載されています。
DeleteSetting appname, section [, key]
でもVBAのオブジェクトブラウザでは下図のようになっており、実際にセクションは省略可能です。何故でしょうね?
もし公式資料が正しいのであれば、VBAからは完全なアンインストールができないという事になってしまいます。
変更前のVBAソースコード
4種類のステートメントを普通に使うソースコードは以下のようになります。
Sub Test_SaveSetting() SaveSetting "アプリ名", "セクション", "キー1", "書き込む値1" SaveSetting "アプリ名", "セクション", "キー2", "書き込む値2" SaveSetting "アプリ名", "セクション", "キー3", "書き込む値3" End Sub Sub Test_GetAllSettings() Debug.Print Join2(GetAllSettings("アプリ名", "セクション")) End Sub Sub Test_GetSetting() Debug.Print GetSetting("アプリ名", "セクション", "キー1", "存在しないときの値") Debug.Print GetSetting("アプリ名", "セクション", "キー2", "存在しないときの値") Debug.Print GetSetting("アプリ名", "セクション", "キー3", "存在しないときの値") End Sub Sub Test_DeleteKey() DeleteSetting "アプリ名", "セクション", "キー1" DeleteSetting "アプリ名", "セクション", "キー2" DeleteSetting "アプリ名", "セクション", "キー3" End Sub Sub Test_DeleteSec() DeleteSetting "アプリ名", "セクション" End Sub Sub Test_DeleteApp() DeleteSetting "アプリ名" End Sub
ご覧の通り、同じアプリ名やセクションを何度も記載する必要があり、メチャクチャめんどくさいです。
もちろん、変数を付けてやるというのが一般的だと思われますが、同じ変数と分かりきってるのに何度も入力しなければならず、やっぱりめんどくさいです。
そこで、ようやく本題です。
VBAでの美しいレジストリ操作
どうすれば使いやすくなるのか?
答えは簡単です。
VBAからのアクセスも、ツリー構造にすれば良いのです!
さらにVBAにはツリー構造に特化しているステートメントがあります。
Withステートメントです!
後ほど紹介する自作クラスを活用し、Withステートメントを使うことで以下のソースコードのように書けます。
キーを書き込み(SaveSetting)
'Before Sub Test_SaveSetting() SaveSetting "アプリ名", "セクション", "キー1", "書き込む値1" SaveSetting "アプリ名", "セクション", "キー2", "書き込む値2" SaveSetting "アプリ名", "セクション", "キー3", "書き込む値3" End Sub 'After Sub Test_With_SaveSetting() With RegApp("アプリ名") With .Section("セクション") .Key("キー1").Value = "書き込む値1" .Key("キー2").Value = "書き込む値2" .Key("キー3").Value = "書き込む値3" End With End With End Sub
キーを読み込み(GetSetting)
'Before Sub Test_GetSetting() Debug.Print GetSetting("アプリ名", "セクション", "キー1", "存在しないときの値") Debug.Print GetSetting("アプリ名", "セクション", "キー2", "存在しないときの値") Debug.Print GetSetting("アプリ名", "セクション", "キー3", "存在しないときの値") End Sub 'After Sub Test_With_GetSetting() With RegApp("アプリ名") With .Section("セクション") Debug.Print .Key("キー1").Value Debug.Print .Key("キー2").Value Debug.Print .Key("キー3").Value End With End With End Sub
キーの一覧の取得(GetAllSettings)
二次元配列が返るので、自作関数Join2
により単一の文字列に連結して出力しています。
'Before Sub Test_GetAllSettings() Debug.Print Join2(GetAllSettings("アプリ名", "セクション")) End Sub 'After Sub Test_With_GetAllSettings() With RegApp("アプリ名") With .Section("セクション") Debug.Print Join2(.AllSettings) End With End With End Sub
さらに、これを拡張してキーのコレクションを取得してFo Each
で回すこともできます。
Sub Test_With_GetAllsettings_RegKey() With RegApp("アプリ名") With .section("セクション") 'KeyValue出力 Dim rk As RegKey For Each rk In .Keys Debug.Print rk.Key, rk.Value Next End With End With End Sub
削除(DeleteSetting)
この例では見本のために下位から消してますが、フォルダ配下のキーは残っていても同時に消えますので、アンインストーラではアプリ名をいきなり消去してしまえばOKです。
'Before Sub Test_DeleteKey() DeleteSetting "アプリ名", "セクション", "キー1" DeleteSetting "アプリ名", "セクション", "キー2" DeleteSetting "アプリ名", "セクション", "キー3" DeleteSetting "アプリ名", "セクション" DeleteSetting "アプリ名" End Sub 'After Sub Test_With_Delete() With RegApp("アプリ名") With .Section("セクション") 'キー削除 .Key("キー1").Delete .Key("キー2").Delete .Key("キー3").Delete 'セクション削除 .Delete End With 'アプリ名削除 .Delete End With End Sub
この書き方のメリット
各名称はWithに対しての1回しか書かないので、名称をいちいち変数に入れる必要がなくなります。(もちろん読み・と書きで2回登場するのは避けられないので、変数・定数に入れても構いませんが)
Withの通例として一段回インデントさせますから、どのセクションがどこまで続いているのか、ひと目でわかるようになります。
「Withを書くのは嫌だ」と思うならメソッドチェーン風に一行でも書けます。
Sub Test_With_Single_SaveSetting() RegApp("アプリ名").Section("セクション").Key("キー1").Value = "書き込む値1" End Sub
コードが読みやすくなります。
「何をしたいか。それはどのオブジェクトか」が、「どのオブジェクトに対して何をしたいか」と日本人にとって馴染み深い順番で書けるようになります。
'Before : 設定を削除する。アプリ名 の セクション の キー3 を。 DeleteSetting "アプリ名", "セクション", "キー3" 'After : アプリ名のセクションのキー1を削除する。 RegApp("アプリ名").Section("セクション").Key("キー1").Delete
「With中のオブジェクトをプロシージャの引数で渡す」ということも可能です。
Sub Test_With_Main() '入れ子で呼び出し Call Test_With_Func(RegApp("アプリ名").section("セクション")) End Sub 'サブルーチン Sub Test_With_Func(section As RegSec) section.Key("キー2").Value = 2 End Sub
自己参照を行うための.Self
プロパティを実装しています。
多くのオブジェクトをWithで扱う際の弱点が解消されています。
Sub Test_With_Main() 'Withからの自己参照(Self)で呼び出し With RegApp("アプリ名").section("セクション") Call Test_With_Func(.Self) End With End Sub 'サブルーチン Sub Test_With_Func(section As RegSec) section.Key("キー2").Value = 2 End Sub
尚、使うことは少ないと思いますが、上位オブジェクトを参照するプロパティも実装しています。
普通は.Parent
だと思うんですが、アプリ名、セクション、キーは3セットと決まっているので、すっ飛ばして参照できるよう実名を付けています。(Sec
、App
)
Sub Test_With_Parent() With RegApp("アプリ名") With .section("セクション") 'セクション → アプリ名 Debug.Print .App.AppName 'キー → アプリ名 Debug.Print .Key("キー1").App.AppName 'キー → セクション名 Debug.Print .Key("キー1").Sec.section End With End With End Sub
どうやってクラスを作ったのか
Withを使うには、其々をオブジェクトにしなければなりません。
そこで、RegApp
、RegSec
、RegKey
クラスを作成し、各クラスには子のオブジェクトを選択するメソッドを追加することで実現しました。
最終的にアプリ名.セクション.キー
というツリー構造でのアクセスが可能となるように構築します。
Set intRegApp = RegApp.Name(アプリ名) 'デフォルトプロシージャ Set instRegSec = intRegApp.Section(セクション) Set instRegKey = instRegSec.Key(キー) Value = instRegKey.Value
RegApp.cls
※このソースコードは、直接VBEにコピペしても使えません。
※メモ帳等へコピペして、拡張子をcls
にしたものをVBEにインポートしてください。
VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "RegApp" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = True Attribute VB_Exposed = False 'RegApp 1.レジストリアプリケーション管理 'HKEY_CURRENT_USER\Software\VB and VBA Program Settings\[AppName] Option Explicit Public AppName As String Public Function section(Section_ As String) As RegSec Set section = New RegSec Set section.App = Me section.section = Section_ End Function Public Function Delete() DeleteSetting Me.AppName End Function Property Get Name(AppName) As RegApp Attribute Name.VB_UserMemId = 0 Me.AppName = AppName Set Name = Me End Property Property Get Self() As RegApp Set Self = Me End Property
RegAppの解説
事前準備として、RegAppクラスは事前にAttribute VB_PredeclaredId = True
を指定することで、クラス名と同一のインスタンスの自動生成を有効化します。
VBE上では表示されない隠しパラメータなので、外部のテキストエディタにて編集する必要があります。他の隠しパラメータの意味や使い方は検索してください。
普通のクラスであれば、新規インスタンスの生成と初期値の設定はこのように書かざる終えません。
Dim instRegApp As RegApp Set instRegApp = New RegApp Set instRegApp = instRegApp.Name("アプリ名")
VBAでコンストラクタに引数が持たせられないのが諸悪の根源です。
ところが、Attribute VB_PredeclaredId = True
によりこう書けます。
Dim instRegApp As RegApp Set instRegApp = RegApp.Name("アプリ名")
でも、まだなんか冗長ですね。設定項目のうち該当アプリ名のアイテムを選択したいのです。つまり.Name
が邪魔です。
こんなときは、デフォルトプロシージャ Attribute Name.VB_UserMemId = 0
を使用します。
こちらも、外部エディタ上のみで編集可能な秘密のパラメータです。
これでNameプロシージャの宣言部はこのようになり
Property Get Name(AppName) As RegApp Attribute Name.VB_UserMemId = 0 Me.AppName = AppName Set Name = Me End Property
利用時はこう書けます。
Set instRegApp = New RegApp Set instRegApp = RegApp("アプリ名")
With
するならこう書けますね。
With RegApp("アプリ名") ~~~~ End With
其々の段階のフォルダを削除するのに対応しているので、Delete
メソッドも実装しておきます。
Public Function Delete() DeleteSetting Me.AppName End Function
更にWith句の中で関数を呼ぶときの引数に、With句に指定した自分自身を渡せるようにSelf
プロパティも実装します。
Property Get Self() As RegApp Set Self = Me End Property
RegSec.cls
VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "RegSec" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = False Attribute VB_Exposed = False 'RegSec 2.レジストリセクション管理 Option Explicit Public App As RegApp Public section As String Public Function Key(Key_ As String) As RegKey Set Key = New RegKey Set Key.App = Me.App Set Key.Sec = Me Key.Key = Key_ End Function Property Get AllSettings() AllSettings = GetAllSettings(Me.App.AppName, Me.section) End Property Property Get Keys() As Collection Set Keys = New Collection Dim KeyValue: KeyValue = GetAllSettings(Me.App.AppName, Me.section) Dim i As Long For i = LBound(KeyValue) To UBound(KeyValue) Dim k As RegKey Set k = New RegKey Set k.App = Me.App Set k.Sec = Me k.Key = KeyValue(i, 0) Keys.Add k Next End Property Public Function Delete() DeleteSetting Me.App.AppName, Me.section End Function Property Get Self() As RegSec Set Self = Me End Property
RegSecの解説
特徴的なのは、AllSettingsとKeysです。
実際に使ってみた結果、GetAllSettings
を実行しただけの純粋な二次元配列が欲しい場合と、RegKey
オブジェクトのコレクションが欲しい場合がある事に気が付きました。
だから、このような使い方が可能になってます。
Sub Test_With_GetAllsettings_RegKey() With RegApp("アプリ名") With .section("セクション") 'KeyValue出力 Dim rk As RegKey For Each rk In .Keys Debug.Print rk.Key, rk.Value Next End With End With End Sub
RegKey.cls
VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "RegKey" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = False Attribute VB_Exposed = False 'RegSec 3.レジストリキー管理 Option Explicit Public App As RegApp Public Sec As RegSec Public Key As String 'デフォルト値を指定して値を取得する Public Property Get GetValue(default_value) GetValue = GetSetting(Me.App.AppName, Me.Sec.section, Me.Key, default_value) If GetValue = "Null" Then Value = Null End Property Public Property Get Value() Value = GetSetting(Me.App.AppName, Me.Sec.section, Me.Key, Empty) If Value = "Null" Then Value = Null End Property Public Property Let Value(ByVal saveValue) Attribute Value.VB_UserMemId = 0 If IsNull(saveValue) Then saveValue = "Null" SaveSetting Me.App.AppName, Me.Sec.section, Me.Key, saveValue End Property Public Function Delete() DeleteSetting Me.App.AppName, Me.Sec.section, Me.Key End Function Property Get Self() As RegKey Set Self = Me End Property
RegKeyの解説
書き込みはLet Value
のみですが、読み込みではGetValue
とValue
の2つを実装しています。
何故かと言うと、デフォルト値を引数に与えるための止む終えない事情です。
プロパティプロシージャは、GetとLet,Setで同じ引数を持たせなければならないというルールがあります。
従ってValue
の方にはdefault_value
が付けられず、別途GetValue(default_value)
を実装することになりました。
最初はGet Value
を実装せず、書き込み専用プロパティにしていたのですが、実際に使用中に混乱を起こしたので読み込みも実装しました。
自分自身が開発したにも関わらず、開発した翌日にもかかわらず、です(笑)
RegKeyではValue
にAttribute Value.VB_UserMemId = 0
を記述して、デフォルトプロシージャとしているので、単なる読み書きではValue
を省略できます。(これの良し悪しは分かりませんが・・・)
RegApp("アプリ名").section("セクション").Key("キー1").Value = "書き込む値1"
は
RegApp("アプリ名").section("セクション").Key("キー1") = "書き込む値1"
と書けますし、
Debug.Print RegApp("アプリ名").section("セクション").Key("キー1").Value
は
Debug.Print RegApp("アプリ名").section("セクション").Key("キー1")
と書けます。
デフォルト値がある場合は、
Debug.Print RegApp("アプリ名").section("セクション").Key("キー1").GetValue("キーが存在しません")
のように書けます。
ダウンロード
この記事で記載しているソースコードは執筆時点のものです。
最新版はGistより入手して頂ければ幸いです。
まとめ
設定の保存は、汎用的なツール開発においては無くてはならない存在です。
このクラスを使えば、億劫な設定保存もストレスフリーになります!?
ツールを使っていて前に使ったパラメータが最初から入力されていたら、嬉しくなるとは思いませんか?
他所のツールとの格の違いを見せつけてやりましょう。
とは言え、これだけではマダマダ実用性には足りません。次の課題は設定項目(キー)そのものの自動決定です。
ユーザーフォームのオブジェクトをまるごと設定値に変換して保存・復元できれば、プロシージャ1本呼ぶだけで設定保存ができちゃいます。
今のところ良い感じで開発が進んでおり、このクラスがとても役に立ってくれています。この話は追々気が向いたら紹介するとしましょう。
以上
何か御座いましたらコメント欄、またはTwitterからどうぞ♪
それではまた来週♪ ちゅんちゅん(・8・)