えくせるちゅんちゅん

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

VBAで快適にレジストリ操作ができるクラスを作ってみた

今回は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では「キー」が分からなければ読み込めませんでした。

  • バージョンアップ前後の互換性に対応しようとすると、キーが分からないということがあるかも

  • 別アプリとの連携で、管理外のキーが増えていることもあるかも

  • 配列のような形でKey1Key2Key3・・・という不特定多数のキーを作る場面もあるかも

そんなときに使います。


取得した結果は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セットと決まっているので、すっ飛ばして参照できるよう実名を付けています。(SecApp

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を使うには、其々をオブジェクトにしなければなりません。

そこで、RegAppRegSecRegKeyクラスを作成し、各クラスには子のオブジェクトを選択するメソッドを追加することで実現しました。

最終的にアプリ名.セクション.キーというツリー構造でのアクセスが可能となるように構築します。

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のみですが、読み込みではGetValueValueの2つを実装しています。

何故かと言うと、デフォルト値を引数に与えるための止む終えない事情です。

プロパティプロシージャは、GetとLet,Setで同じ引数を持たせなければならないというルールがあります。

従ってValueの方にはdefault_valueが付けられず、別途GetValue(default_value)を実装することになりました。

最初はGet Valueを実装せず、書き込み専用プロパティにしていたのですが、実際に使用中に混乱を起こしたので読み込みも実装しました。

自分自身が開発したにも関わらず、開発した翌日にもかかわらず、です(笑)


RegKeyではValueAttribute 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より入手して頂ければ幸いです。

Gist - VBAのレジストリ操作の拡張クラス


まとめ

設定の保存は、汎用的なツール開発においては無くてはならない存在です。

このクラスを使えば、億劫な設定保存もストレスフリーになります!?

ツールを使っていて前に使ったパラメータが最初から入力されていたら、嬉しくなるとは思いませんか?

他所のツールとの格の違いを見せつけてやりましょう。


とは言え、これだけではマダマダ実用性には足りません。次の課題は設定項目(キー)そのものの自動決定です。

ユーザーフォームのオブジェクトをまるごと設定値に変換して保存・復元できれば、プロシージャ1本呼ぶだけで設定保存ができちゃいます。

今のところ良い感じで開発が進んでおり、このクラスがとても役に立ってくれています。この話は追々気が向いたら紹介するとしましょう。

以上


何か御座いましたらコメント欄、またはTwitterからどうぞ♪

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