VBAのオブジェクトのコピーは参照渡ししか無い
VBAでいろんなオブジェクト変数があります。そしてオブジェクト変数のコピーはSetステートメントで行います。
Setステートメントによるコピーは参照渡しになります。
しかし処理の中でそれでは困る場合があります。
参照渡しではなく値渡し(ディープコピー)をしてほしい場合です。
VBAのコピー方式やディープコピー方法について説明します。
なお、オブジェクト変数には親子関係を持つExcel.Range.Fontのようなオブジェクトと、親子関係がない独立したDictionaryのようなオブジェクトの2種類がありますが、いずれの場合もクラスのProperty SetのByValで値渡しをしようとしても残念ながら参照渡しになってしまい、クラス+ByVal方式でのディープコピーは出来ません。
通常のコピーによる参照渡し(シャローコピー)の例
VBAで使うオブジェクト変数として代表的なものを挙げるとするとセルを表すRangeオブジェクトです。
Rangeオブジェクトを使ってVBAのオブジェクト変数のコピーを説明します。
1. 元のオブジェクトを変更しない場合
VBAでコーディングを行う際に、セルを参照するRangeオブジェクトを一時的に別の変数に保持しておいて、あとで再設定したい、と考えることがあります。
例えば、元々選択していたセルを事前に保持しておいて、あとで再度設定する、とかですね。
A1セルを選択して、次にB2セルを選択して、再度A1セルを選択するコードとしてはこんな感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Sub RangeSample() Dim rBefore As Range '// A1セルを選択 Range("A1").Select '// アクティブセルを保持(A1セルを保持) Set rBefore = ActiveCell '// B2セルを選択(アクティブセルがB2になる) Range("B2").Select '// B2セル値を出力 Debug.Print ActiveCell.Value '// 元のセルを再選択(A1セルがアクティブセル) rBefore.Select End Sub |
2. 元のオブジェクトを変更する場合
上のコードの場合は元のセルを再度選択するだけなので、特に問題は発生しません。
ところが以下のコードのようにオブジェクト変数のコピー元のセルの文字列を変更すると問題が発生します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Sub RangeEditSample() Dim rBefore As Range '// A1セルに"AAA"を設定 Range("A1").Value = "AAA" '// A1セルを選択 Range("A1").Select '// アクティブセルを保持(A1セルを保持) Set rBefore = ActiveCell Debug.Print rBefore.Value '// A1セルの値を変更("aaa"を追加") Range("A1").Value = Range("A1").Value & "aaa" Debug.Print rBefore.Value End Sub |
実行結果は以下のようになります。
AAA
AAAaaa
12行目と16行目はどちらも同じオブジェクト変数のrBeforeの値を出力しており、全く同じコードです。
処理途中でrBeforeの値を変更していません。
にも関わらず実行結果は異なる結果が出力されています。
これは、rBeforeというオブジェクト変数が常にA1セルのRangeオブジェクトを指しているためです。
そのためA1セルの内容が変わるとA1をSetステートメントでコピーしたrBeforeの変数の内容も変わってしまいます。
これがVBAのオブジェクト変数のコピーになります。他の言語では参照渡しとかシャローコピーとかの言い方をしますが、Windowsのショートカットみたいな感じで考えてもらうと分かりやすいと思います。
ディープコピーの方法(元のオブジェクトが変わっても影響がない方法)
Setステートメントでのコピーは必ず参照渡しになります。
それでは値渡しをしたい場合はオブジェクト変数ではなく別の方法を利用しなければなりません。
考えられる方法は以下のようにいくつかあります。
- オブジェクト変数と同じプロパティの変数を持つユーザー定義型(構造体)を用意する。
- オブジェクト変数と同じプロパティの変数を持つクラスを用意する。
- オブジェクト変数と同じプロパティの数だけローカル変数を用意する。
- オブジェクト変数の内容を一時的にワークシートに設定しておく。
- オブジェクト変数の内容を一時的にテキストファイル等に出力しておく。
上から順に分かりやすい方法で並べていますが、どの方法も一長一短があります。
1.のユーザー定義型を使う方法が一番無難かと思います。同じプロパティ名を利用できるためコーディングのしやすさもあります。
2.の自作クラスは変数を用意するだけならユーザー定義型とほとんど変わりません。ただメソッドなどを拡張できる利点もあります。
上でも書きましたが、Property SetでのByValで以下のような感じでクラスを書いて値渡しをしても参照渡しになります。
1 2 3 4 5 6 7 8 9 10 11 12 |
'// ディープコピーにならないクラス Option Explicit Private c As Font Public Property Set Copy(ByVal a As Font) Set c = a End Property Public Property Get Font() As Font Set Font = c End Property |
3.のローカル変数を用意する方法は思いつきやすいとは思いますがプロパティの数に比例してコードが長くなります。さらにオブジェクト変数が配列化している場合にプロパティごとに配列を用意しなければならずコードが冗長化するため可読性が落ちる点があります。
4.の一時的にワークシートに保持する場合もシートの書式設定を気にしなければならない点や不要になった後始末などが面倒な点があります。
5.の一時的にテキストファイルを使う場合もワークシートと同様に出力先や後始末の考慮が必要になります。
そこで、以下にユーザー定義型を使った値渡し(ディープコピー)の方法を紹介します。
オブジェクト変数の値渡し(ディープコピー)のサンプル
Rangeオブジェクトのプロパティは数がちょっと多くてサンプルとして使いにくいので、セルのフォントのRange.Fontオブジェクトでのサンプルとします。
オブジェクト変数のプロパティと同じユーザー定義型の作成
Fontオブジェクトには以下のプロパティがあります。
そのうち実際にフォントの設定に利用するもののみをユーザー定義型として定義します。
ユーザー定義型は標準モジュールなどの先頭あたりに定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Type ST_FONT Background As Long Bold As Boolean Color As Double ColorIndex As Long FontStyle As String Italic As Boolean Name As String OutlineFont As Boolean Shadow As Boolean Size As Double Strikethrough As Boolean Subscript As Boolean Superscript As Boolean ThemeColor As Variant ThemeFont As XlThemeFont TintAndShade As Double Underline As Long End Type |
ディープコピーのコード
上で作成したユーザー定義型の変数を使って、コピー先の値保持用の領域とします。
このコードは以下の処理を行っています。
- A1セルに”abc”と書いて文字色に赤、そして取り消し線を設定
- A1セルのフォント情報をユーザー定義型の領域に保持
- A1セルの文字色を青、取り消し線を解除に設定
- A1セル(青+取り消し線無し)をA2セルにコピー
- A2セルにユーザー定義型のフォント情報(赤+取り消し線あり)を設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
Sub DeepCopyTest() Dim stFont As ST_FONT '// Font用ユーザー定義型 Dim r As Range '// セル参照用オブジェクト '// A1セルをオブジェクト変数にコピー Set r = Range("A1") '// セル値を設定 r.Value = "abc" '// 文字色と取り消し線を設定 r.Font.Color = RGB(255, 0, 120) r.Font.Strikethrough = True '// ユーザー定義型にフォント情報を保持 stFont.Color = r.Font.Color stFont.Strikethrough = r.Font.Strikethrough '// 文字色と取り消し線を再設定 r.Font.Color = RGB(0, 0, 255) r.Font.Strikethrough = False '// A1セルの内容をA2セルにコピー Call r.Copy(Range("A2")) '// A2セルにユーザー定義型のフォント情報を設定 Range("A2").Font.Color = stFont.Color Range("A2").Font.Strikethrough = stFont.Strikethrough End Sub |
実行結果
A2セルに変更前のA1セルのフォント情報が設定されています。
ワークシートやテキストファイルなどに保持する場合も考え方は同じで、各プロパティを保持しておく、という点にあります。
ユーザー定義型の場合はプロパティと同じ変数名を用意できますが、ワークシートやテキストファイルの場合はどこに保存されているのかは設計しなければなりません。
ただ、そちらの方が都合がいい場合もありますので、用途によって使い分けしてください。