ExcelVBAでクラスモジュールを活用してCSVファイルをコレクションに格納する
先日CSVファイルを2次元配列に格納するコードを書いたのですが、クラスを使うととっても便利だということがわかったのでご紹介します。
はじめに
この記事は、
こちらの内容を活用してクラスモジュールを作り、
こちらの記事でやっている「CSV→2次元配列へ格納」の処理を、「CSV→クラスを使ったコレクションへ格納」に置き換えてみよう、という内容です。コードの効率や可読性がこんなに違うんだー、というところが書けたらなと。
クラスモジュールの使い方は前述の記事でガッッツリ書いていてるので(めっちゃ長い)、まずはそちらからご覧ください。
クラスの概念はデータベースのテーブルに似ている
前述のクラスモジュールの記事で、説明にこんな図を描きました。
Class1さんの運営する配車センターがあって、複数の荷台のついた車の手配をしてくれます。最初は看板を持って「この形の車を手配しますよー」って言っているだけなので、車はまだありません。
車を使いたいときは、Class1さんにお願いして、実際に手配してもらいます。この車を「インスタンス(実体)」と呼び、荷物を載せることができます。Class1さんに言えば、同じ形の車を何台も手配してもらえます。
Name | Price | Number |
---|---|---|
いちご | 150 | 5 |
りんご | 200 | 3 |
みかん | 180 | 10 |
車に載せた荷物は、表にするとこんな感じで表せます。これを考えていて、思いました。何かに似ていないか…?
データベースのテーブル…
似てる!!! (( ゚д゚)ガタッ
と、業務でデータベースを扱うことの多いわたしは、自分にとってのクラスの使いどころはココなんじゃ!? と思い、早速CSVファイルをクラスを使って処理してみようと思いました。
CSVファイルをクラスを使ったCollectionへ
それではまず、こんな形のCSVファイルがあったとします。
いちご,150,5 りんご,200,3 みかん,180,10
クラスモジュール(Class1)に、プロパティ設定/取得のための記述をしましょう。
Public Name As String '名称(Name)プロパティ Public Price As Integer '値段(Price)プロパティ Public Number As Integer '個数(Number)プロパティ '売上(Sale)プロパティは取得のみ Property Get Sale() As Integer Sale = Price * Number '値段×個数の値を使う End Property
今回は特にプロパティに読み書き制限は設けませんので、Public変数で簡単に書いてしまいます。CSVを格納するだけなら上の3つだけでOKなのですが、勉強も兼ねて、取得した値を計算して使う「売上」プロパティも作ってみましょう。Getプロシージャだけ(読み取り専用)のプロパティにしておきます。
SQLでも、SELECTのときに計算フィールドつくれたりしますよね。やっぱりちょっと似ている…。
次に標準モジュール(Module1)に、実際の処理を書きましょう。
Sub Sample() Dim file As String: file = "C:\test.csv" 'CSVファイル指定 Dim Items As New Collection 'コレクションを生成 Open file For Input As #1 'CSVファイルを開く Do Until EOF(1) '最終行までループ Dim buf As String: Line Input #1, buf '読み込んだデータを1行ずつみていく Dim tmp As Variant: tmp = Split(buf, ",") 'カンマで分割 Dim obj As New Class1 'インスタンスの生成 obj.Name = CStr(tmp(0)) '名称 obj.Price = CInt(tmp(1)) '値段 obj.Number = CInt(tmp(2)) '個数 Items.Add obj 'コレクションに追加 Set obj = Nothing 'インスタンスの破棄 Loop Close #1 'CSVファイルを閉じる Dim item As Class1 'ループ用の変数 For Each item In Items 'コレクション内をループ Debug.Print item.Name, item.Price, item.Number, item.Sale 'プロパティを取得 Next Set Items = New Collection 'コレクションの初期化(この場合は省略可) End Sub
CSVの中身を1行ずつ見ていって要素を分割し、インスタンスを作成してそれぞれプロパティへ値を入れ、そのオブジェクトをコレクションに追加、というのをCSVの行数ぶん繰り返しています。
一応これでも動くんですが、ループ内のメインとなる10~15行目だけ見てみましょう。
Dim obj As New Class1 'インスタンスの生成 obj.Name = CStr(tmp(0)) '名称 obj.Price = CInt(tmp(1)) '値段 obj.Number = CInt(tmp(2)) '個数 Items.Add obj 'コレクションに追加 Set obj = Nothing 'インスタンスの破棄
obj
という変数でインスタンスを作って、それぞれプロパティに値を入れた後、5行目でコレクションに追加しています。このコードは同じプロシージャ内で何度もループするので、その都度きちんと破棄しないと不具合が出ますので、6行目は省略できません。
ここで、何度も出てくるobj
は省略できないのかなー、と思って調べてみたら、いつもお世話になっているthomさんのブログで、With文でコレクションに追加する方法が紹介されていました。
なにこれすてき!!!
Public Name As String '名称(Name)プロパティ Public Price As Integer '値段(Price)プロパティ Public Number As Integer '個数(Number)プロパティ '売上(Sale)プロパティは取得のみ Property Get Sale() As Integer Sale = Price * Number '値段×個数の値を使う End Property Public Property Get Self() As Class1 Set Self = Me End Property
このように、クラスモジュールへ自己参照を返すプロパティを追加してやれば、
With New Class1 'インスタンスの生成 .Name = CStr(tmp(0)) '名称 .Price = CInt(tmp(1)) '値段 .Number = CInt(tmp(2)) '個数 Items.Add .Self 'コレクションに追加 End With
こんなふうに、インスタンス変数を作らなくてもコレクションに追加できちゃうのです。しかもこの書き方ならWithを抜けるときにインスタンスが破棄されるので、Nothingしなくていい。すごい。
追記:正しくは、「インスタンスへの参照が破棄される」でした。これに関してめちゃくちゃわかりやすい記事をthomさんが書いてくださいました!
というわけで、最終的には標準モジュールではこのようなコードに。
Sub Sample() Dim file As String: file = "C:\test.csv" 'CSVファイル指定 Dim Items As New Collection 'コレクションを生成 Open file For Input As #1 'CSVファイルを開く Do Until EOF(1) '最終行までループ Dim buf As String: Line Input #1, buf '読み込んだデータを1行ずつみていく Dim tmp As Variant: tmp = Split(buf, ",") 'カンマで分割 With New Class1 'インスタンスの生成 .Name = CStr(tmp(0)) '名称 .Price = CInt(tmp(1)) '値段 .Number = CInt(tmp(2)) '個数 Items.Add .Self 'コレクションに追加 End With Loop Close #1 'CSVファイルを閉じる Dim item As Class1 'ループ用の変数 For Each item In Items 'コレクション内をループ Debug.Print item.Name, item.Price, item.Number, item.Sale 'プロパティを取得 Next End Sub
実行するとイミディエイトウィンドウにこのように出ます。
2次元配列に格納していたときは、CSVファイルを開く前に中の行数を調べてその数で配列を宣言したり、取得するときには数値で指定しなければならなかったりと、比較するとやや難解です。
クラスを使ってコレクションに入れてしまえば、名称を取得したいときにはitem.Name
、値段ならitem.Price
と、とってもわかりやすいですね! コードも短いですし。
以上です! 積極的にクラス使ってみたいですね。
ExcelVBAに興味をお持ちの方は、こちらの記事もどうぞ!
- これからExcelのマクロを始めたいという方に!簡単な練習問題作りました。
- 私がExcelVBAでよく使う便利なコード・スニペットまとめ
- プログラム初心者さんへ贈る、エラーが起きたら試してみて欲しいこと
- ExcelVBAのクラスモジュールって何?という人向けの使い方まとめ
書籍を執筆しています。
6件のコメント
解説ありがとうございます。
クラスモジュールについて理解できていない部分があり、ご教授頂ければと思います。
===== 質問 =====
コレクションに追加されたものをある検索条件で抽出して、その平均値を求めたいとします。
標準モジュールのコードに動的配列を用意して、for eachで抽出したものを格納し、WorksheetFunction.Average(arr)とすればいいかと思いますが、この操作をクラスモジュールにメソッドとして作り、標準モジュールからは、item.平均値(抽出条件)のように呼び出す事はできるのでしょうか?
抽出したいプロパティが多数の場合、その分だけ動的配列も用意する必要があり、コードの可読性が悪くなります。
item.平均値(プロパティ, 抽出条件)
のようにできればすっきりしたコードになっていいと思うのですが。
よろしくお願いします。
こんにちは、コメントありがとうございます! できると思います。ちょっとコメント欄だと長くなっちゃうので、新たに記事を書きますね。少々お待ち下さい。
追記:こちらに書きました!
csvファイルの取り扱いに悩んでいた折、こちらのサイトにたどり着きました。
とても分かりやすい解説でとても参考になりました。
ありがとうございます。
また、ご教授いただきたいことがございます。
最終的にはユーザーが任意にプロパティを選択して
データの加工ができるような設計にしたいのですが、
なかなか思い通りにいかず悩んでおります。
たとえばプロパティ名を変数で指定できたら、
上の例でいうと、変数xの中身をユーザーが任意に選べ、
x = Name のときは item.x でNameを拾ってくる・・・
というような運用ができればベストなのですが。
何かいい解決方法があれば、ぜひご教授いただければ幸いです。
こんにちは、コメントありがとうございます。プロパティ名を変数で指定するのは
CallByName
を使うとできますよ! 本文中のModule1のコードを変更した例がこちらです。17行目までは同じです。20行目で指定のプロパティ名を変数に格納しています。今回はかんたんにInputBoxにしちゃいましたが、間違い防止でコンボボックスとかのほうが親切だと思います。27行目で
CallByName
を使って指定のプロパティの値を取得しています。なお、このコードでは Name, Price, Number, Sale の4つのプロパティが有効で、それ以外の文字列が入った場合は27行目でエラーになりますので、32行目以降のエラートラップで処理します。無効なプロパティが入った場合のエラー番号は438なので、そこでメッセージボックスで注意を促す形です。
なお、上のコメントで質問を受けて書いた回答記事では、プロパティを変数で2つ指定して、プロパティ1に対して条件を設け、それを満たすプロパティ2の平均値を出すという、なかなかにマニアックなことをしてますので、よろしければそちらもなにか参考になるかもしれません。
コメントありがとうございます。
CallByName というものがあるんですね。
まさに思い描いていた機能で、動作させてうれしくなりました。
リンク先の内容もとても興味深いです。
想定していたデータ処理にとても役立ちそうです。
ありがとうございました。
こんにちは、お返事ありがとうございます! お役に立てたようで光栄です(*゚ω゚*)
コメントは承認制ですので、反映までしばらくお待ち下さい。(稀にスパムの誤判定にて届かないこともあるようですので、必要な際はお問い合わせからお願い致します。)
YouTubeでQ&Aコンテンツを企画しています
運営しているYouTubeチャンネルで、ご相談やご質問を募集しています。動画のコメントやお問い合わせページからお気軽にご相談をお寄せください。