TextFileクラスの改良
TextFileクラスの改良
「改良」なのかどうかはわからんが。
ちなみに、元祖TextFile
クラスについては、
コチラをどうぞ。
改良後のTextFileクラス
リスト1 クラスモジュール
'オブジェクト名はTextFile' Option Explicit 'Constants' Private Enum ErrorTypes '……(**)' etFileNotFound = 1 etLineNotExists etNotInitialized etErrorOccurred End Enum 'Module Level Variables' Private isInitialized As Boolean Private fileFullName As String Private line_() As String Private fsObj As FileSystemObject 'Properties' Public Property Get Line(ByVal numberOf As Long) As String Const ERR_SOURCE As String = _ "TextFile Class, Property Get Line" 'initメソッド未実行ならエラー。' If Not isInitialized Then _ Call raiseError(etNotInitialized, ERR_SOURCE) Dim ret As String '存在しない行番号を指定していたらエラー。' If UBound(line_) + 1 < numberOf Or _ numberOf < 0 Then _ Call raiseError(etLineNotExists, ERR_SOURCE) On Error GoTo ErrorHandler ret = line_(numberOf - 1) Line = ret Exit Property ErrorHandler: '何かしらエラーが出たら、イミディエイトに情報を表示して中断。' Debug.Print "Number : " & Err.Number Debug.Print "Description : " & Err.Description Call raiseError(etErrorOccurred, ERR_SOURCE) End Property Public Property Get LineCount() As Long Const ERR_SOURCE As String = _ "TextFile Class, Property Get LineCount" 'initメソッド未実行ならエラー。' If Not isInitialized Then _ Call raiseError(etNotInitialized, ERR_SOURCE) On Error GoTo ErrorHandler Dim ret As Long If IsEmpty(line_) Then ret = 0: GoTo Finalizer ret = UBound(line_) + 1 Finalizer: LineCount = ret Exit Property ErrorHandler: Debug.Print "Number : " & Err.Number Debug.Print "Description : " & Err.Description Call raiseError(etErrorOccurred, ERR_SOURCE) End Property 'Constructor' Private Sub Class_Initialize() isInitialized = False Set fsObj = New FileSystemObject End Sub Public Sub init(ByVal targetFullName As String) Const ERR_SOURCE As String = _ "TextFile class,init Method" '対象ファイルの存否確認。なければエラー。' If Not fsObj.FileExists(targetFullName) Then _ Call raiseError(etFileNotFound, "TextFile Class, init Method") On Error GoTo ErrorHandler 'モジュールレベル変数に対象ファイルのフルパスを保存' fileFullName = targetFullName 'テキストファイルからデータを取得' line_ = getLines(targetFullName) isInitialized = True Exit Sub ErrorHandler: Debug.Print "Number : " & Err.Number Debug.Print "Description : " & Err.Description Call raiseError(etErrorOccurred, ERR_SOURCE) End Sub 'テキストファイル読み込み' Private Function getLines( _ ByVal targetFullName As String) As String() Const ERR_SOURCE As String = _ "TextFile class, getLines Method" On Error GoTo ErrorHandler Dim ret() As String Dim n As Long n = 0 ReDim ret(n) Dim txtStream As Scripting.TextStream Set txtStream = fsObj.OpenTextFile( _ FileName:=targetFullName, _ IOMode:=ForReading, _ Create:=False) Do ret(n) = txtStream.ReadLine '最終行まで読み込んだらExit' If txtStream.AtEndOfLine Then Exit Do n = n + 1 ReDim Preserve ret(n) Loop Call txtStream.Close Set txtStream = Nothing line_ = ret getLines = line_ Exit Function ErrorHandler: Debug.Print "Number : " & Err.Number Debug.Print "Description : " & Err.Description Call raiseError(etErrorOccurred, ERR_SOURCE) End Function 'Destructor' Private Sub Class_Terminate() Set fsObj = Nothing End Sub 'Methods' Public Sub regetData(Optional ByVal targetFullName As String) Const ERR_SOURCE As String = _ "TextFile Class, regetData Method" 'initメソッド未実行ならエラー。' If Not isInitialized Then _ Call raiseError(etNotInitialized, ERR_SOURCE) If targetFullName = "" Then GoTo MainProcess If Not fsObj.FileExists(targetFullName) Then _ Call raiseError(etFileNotFound, ERR_SOURCE) '新たにファイルフルパスが渡されたら、それを新しいファイルフルパスにする' fileFullName = targetFullName MainProcess: On Error GoTo ErrorHandler Erase line_ Call Me.init(fileFullName) Exit Sub ErrorHandler: Debug.Print "Number : " & Err.Number Debug.Print "Description : " & Err.Description Call raiseError(etErrorOccurred, ERR_SOURCE) End Sub 'データ書き換え' '……(*)' Public Sub setData(ByVal targetLine As Long, _ ByVal targetData As String) Const ERR_SOURCE As String = _ "TextFile Class, setData Method" 'initメソッド未実行ならエラー。' If Not isInitialized Then _ Call raiseError(etNotInitialized, ERR_SOURCE) Dim ret As String '存在しない行番号を指定していたらエラー。' If targetLine < 1 Or _ UBound(line_) + 1 < targetLine Then _ Call raiseError(etLineNotExists, ERR_SOURCE) On Error GoTo ErrorHandler 'メインの処理' line_(targetLine - 1) = targetData Dim txtStream As Scripting.TextStream Set txtStream = fsObj.OpenTextFile( _ FileName:=fileFullName, _ IOMode:=ForWriting, _ Create:=False) Dim i As Long For i = 0 To UBound(line_) Call txtStream.WriteLine(line_(i)) Next Call txtStream.Close Set txtStream = Nothing Exit Sub ErrorHandler: Debug.Print "Number : " & Err.Number Debug.Print "Description : " & Err.Description Call raiseError(etErrorOccurred, ERR_SOURCE) End Sub 'エラー発生用' Private Sub raiseError(ByVal typeOfError As ErrorTypes, _ Optional ByVal errorSource As String) Dim msg As String msg = getErrorMessage(typeOfError) Call Err.Raise(Number:=10000 + typeOfError, _ Source:=errorSource, _ Description:=msg) End Sub Private Function getErrorMessage( _ ByVal typeOfError As ErrorTypes) As String '……(***)' Const ERR_SOURCE As String = _ "TextFile class, getErrorMessage Method" On Error GoTo ErrorHandler Dim ret As String Select Case typeOfError Case etFileNotFound ret = "The file you specified isn't found." Case etLineNotExists ret = "This file doesn't have so many lines." Case etNotInitialized ret = "You must run ""init"" method!" Case etErrorOccurred ret = "Some Error has occurred" End Select getErrorMessage = ret Exit Function ErrorHandler: Debug.Print "Number : " & Err.Number Debug.Print "Description : " & Err.Description Call raiseError(etErrorOccurred, ERR_SOURCE) End Function
メソッドを一つ追加した(*)ことに伴い、列挙体の要素を増やし(**)、エラーメッセージのパターンも増やした(***)。
setDataメソッド
Public Sub setData(ByVal targetLine As Long, _ ByVal targetData As String) '……(1)' Const ERR_SOURCE As String = _ "TextFile Class, setData Method" 'initメソッド未実行ならエラー。' '……(2)' If Not isInitialized Then _ Call raiseError(etNotInitialized, ERR_SOURCE) Dim ret As String '存在しない行番号を指定していたらエラー。' '……(3)' If targetLine < 0 Or _ UBound(line_) + 1 < targetLine Then _ Call raiseError(etLineNotExists, ERR_SOURCE) On Error GoTo ErrorHandler 'メインの処理' line_(targetLine - 1) = targetData '……(4)' Dim txtStream As Scripting.TextStream '……(5)' Set txtStream = fsObj.OpenTextFile( _ FileName:=fileFullName, _ IOMode:=ForWriting, _ Create:=False) Dim i As Long '……(6)' For i = 0 To UBound(line_) Call txtStream.WriteLine(line_(i)) Next Call txtStream.Close '……(7)' Set txtStream = Nothing Exit Sub ErrorHandler: Debug.Print "Number : " & Err.Number Debug.Print "Description : " & Err.Description Call raiseError(etErrorOccurred, ERR_SOURCE) End Sub
まず(1)の
Public Sub setData(ByVal targetLine As Long, _ ByVal targetData As String)
で引数の設定。
targetLine
で何行目のデータを書き換えるのかを指定し、targetData
で書き換えるデータを指定する。
(2)の2行(実質1行)
If Not isInitialized Then _ Call raiseError(etNotInitialized, ERR_SOURCE)
はガード節。
擬似コンストラクタinit
メソッドの末尾に
isInitialized = True
があり、Module LevelのisInitialized
をTrue
にするようにしている。こうすることで擬似コンストラクタinit
メソッドの実行を強制している。
(3)の3行(実質1行)もガード節。
If targetLine < 1 Or _ UBound(line_) + 1 < targetLine Then _ Call raiseError(etLineNotExists, ERR_SOURCE)
このメソッドでは、既にある行のデータを書き換えることを想定しているので、元のテキストファイルの行数を超える行番号は指定できないようにしている。もちろん、0行目とか、負の数行目もあり得ないので、ここで弾く。
不正な引数とか、あり得ない操作については、このようにメソッド冒頭で弾いてしまう、という「ガード節」の考え方が気に入っている。good mannerだと思う。
テキストファイルにデータを追記するために、次はappendData
メソッドを作る必要があるかもしれない。
ここまで来たら、ほぼ安全なので、メインの処理に移る。
(4)の
line_(targetLine - 1) = targetData
で、元のテキストファイルの各行のデータを保持したModule Levelの配列line_()
の書き換え対象データを書き換える。
行番号と配列の添字がずれるのはイマイチなので、今にして思えば配列を1
はじまりにした方が良かったかもしれない。このTextFile
クラスのLine
プロパティはItem
みたいなものなのだから……。
まあ、「1ずれる」という部分は、クラスモジュール内に隠蔽してしまっているので、利用する側は何も困らないのだけれど。これもオブジェクト指向の強みだね。
次に、(5)からの5行(実質2行)
Dim txtStream As Scripting.TextStream Set txtStream = fsObj.OpenTextFile( _ FileName:=fileFullName, _ IOMode:=ForWriting, _ Create:=False)
でScripting.TextStream
オブジェクトを準備する。
OpenTextFile
メソッドの引数IOMode
をForWriting
にしているので、書き込みモードだ。
Scripting.TextStream
オブジェクトについては、詳しくはコチラをどうぞ。とにかく、テキストファイルの読み書きを行うときには、TextStream
オブジェクトを通して行う、ぐらいの理解で良いと思う。
これで、TextStream
クラスのインスタンスtxtStream
が用意できたので、コイツを通じてテキストファイルを操作することができる。
あとは、(6)からの4行
Dim i As Long For i = 0 To UBound(line_) Call txtStream.WriteLine(line_(i)) Next
で配列line_()
の各要素を1行目から最終行まで書き込む。
変更していない行まで書き込みし直すのは無駄な気がするけれど、仕方ない。
指定した行だけを書き換える方法ってあるのかな??? あったら誰か教えてくだされ。
最後に(7)の
Call txtStream.Close
でtxtStream
(テキストファイルとの接続?)を閉じておしまい。
実験
このプロジェクトと同じフォルダ内に、test.txt
というテキストファイルを置き、内容を
にしておく。相変わらずアホな内容ですまん。
次のコードを標準モジュールに書く。
リスト2 標準モジュール
Private Sub testTextFileClass() Dim targetPath As String targetPath = ThisDocument.Path & "\" & "test.txt" '" Dim txtFile As TextFile Set txtFile = New TextFile Call txtFile.init(targetPath) Dim ar() As String Dim i As Long '……(1)' For i = 1 To txtFile.LineCount Debug.Print txtFile.Line(i) Call WindowsAPI.waitFor(300) Next Debug.Print "3行目を書き換えるよ。" '……(2)' Call txtFile.setData(3, "ち~んw") Call WindowsAPI.waitFor(500) For i = 1 To txtFile.LineCount '……(3)' Debug.Print txtFile.Line(i) Call WindowsAPI.waitFor(300) Next End Sub
(1)の
Dim i As Long '……(1)' For i = 1 To txtFile.LineCount Debug.Print txtFile.Line(i) Call WindowsAPI.waitFor(300) Next
では、まずは普通にtxtFile
インスタンスに蓄えられた元のテキストファイル(test.txt
)の各行のデータ(笑)を吐き出している。
ループ1回ごとに自作WindowsAPI
クラスのwaitFor
メソッドでポーズを入れている((2)も(3)も同様。)。
WindowsAPI
クラスについては、
コチラをどうぞ。
(2)の
Debug.Print "3行目を書き換えるよ。" Call txtFile.setData(3, "ち~んw")
では、イミディエイトに書き換え宣言を表示させた後、
Call txtFile.setData(3, "ち~んw")
で新作のsetData
メソッドを実行。引数に「3
」と「"ち~んw"
」を渡しているので、test.txt
の「3
」行目を「ち~んw
」というデータ(笑)に書き換えることになる。
あとは、(3)の
For i = 1 To txtFile.LineCount Debug.Print txtFile.Line(i) Call WindowsAPI.waitFor(300) Next
で、新生test.txt
のデータ(笑)を全てゆっくり吐き出す。
リスト2を実行すると……
このとおり。
test.txt
は
こうなっている。
おわりに
異様に長い割に、読んでもあまりためにならない記事になってしまい、深く反省している……。
さらに追記
今はこんなことになっています。