「ネオ写経」のすすめ
「ネオ写経」のすすめ
新型コロナウイルス対応で外出の自粛が求められる中、みなさまいかがお過ごしでしょうか。
ろくにテレワーク環境も整っていないのに、「とにかくテレワークだ!」的に導入されてしまった事業所も、それなりにあると思います。
個人的には、この期間は〈終息後のダメージ回復〉を効果的に行うために個人が力量を高める機会ととらえるのが良いと思います。それができる余裕のある業界に限られますが……。
「ネオ写経」とは
私が勝手に考えました。
もともと、プログラミング界隈で「写経」といえば、サンプル・コードの類を写すこと。(ですよね?)
ただ、私は「写経」というものをしたことがほぼない。写しているうちに、「あら? じゃあ、ここはこうした方がおもろいやんけ。」とか、「ついでにこうしといたれ。」みたいなのが出てくるから、「般若心経」を写していたはずなのに、なぜか「あほだら経」が出来上がっていた、ということが起こる。
そんな私が考えた「ネオ写経」とは!?
既存クラスをラップしたクラスを作る。
これ。
ニセFileSystemObjectを作る
FileSystemObject
は便利だ。
しかも、プロパティ名とかメソッド名は、さすがプロが作っただけあって、実に良くできている。コードが実に読みやすくなる。
ところが、デフォルトでは使えない。
いちいち参照設定をせねばならん。
もちろんCreateObject
を使えば参照設定せずにすむ。
しかし、これだとObject
型変数に突っ込んで使用することになり、コーディング時に入力補完の恩恵が得られずイマイチ。
もちろん、コーディング時に参照設定をしておき、完成したら参照設定を切る、という方法もあるにはあるが、それはそれでメンドクサイ。
だったら、FileSystemObject
クラスをラップしたFileSystemObject
クラスを自作しちまえばいいんでねえの!
コンストラクタ
これは簡単。Class_Initialize
でScripting.FileSystemObject
クラスのインスタンスを得ればよい。
まず、プロジェクトにクラスモジュールを挿入し、オブジェクト名をFileSystemObject
にする。そして、クラスモジュールに次のコードを書く。
クラスモジュール FileSystemObject
'Declarations Section' 'Module Level Variables' Private fsObj As Object 'Constructor' Private Sub Class_Initialize() Set fsObj = CreateObject("Scripting.FileSystemObject") End Sub
これだけ。
これで、プロパティとかメソッドをこの変数fsObj
を経由して呼び出すようにすればいい。
プロパティとメソッドの実装(笑)
大袈裟な見出しだが、プロパティをメソッドを実装(笑)するときの教科書が、おれたちの「オブジェクト ブラウザー」様だ!
「ブラウザー」と延ばしているところがボスキャラ感があっていいよね。
[F2]キーを押すか何かして、「オブジェクト ブラウザー」を開き、FileSystemObject
を指定すると、
こんなふうにメンバを確認することができるし、
こんなふうに各メンバの実装方法を確認することもできる。
あとはコーディングあるのみ!
……である!
プロパティの実装(笑)
……といっても、FileSystemObject
にはプロパティは一つしかない。これは意外だった。なんと、Drives
コレクションを返すDrives
プロパティしかないのだ。
実装方法は、オブブラ(略すなw)によると、
Property Drives As Drives
とのこと。
もちろん、このまま打ち込んでもコンパイルエラーになるので、脳内でコードを補完して
Public Prooerty Get Drives() As Drives
とする。
もちろん、Microsoft Scripting Runtime
を参照設定しないのだから、As Drives
ではまずい。で、
Public Prooerty Get Drives() As Object
と、返り値をObject
型に改めておく。
あとは中身。
Dim ret As Object Set ret = fsObj.Drives '……(1)' Set Drives = ret '……(2)'
これでいい。変数fsObj
はモノホンのFileSystemObject
クラスのインスタンスを指しているから、そのDrives
プロパティの返り値はもちろんモノホンのDrives
コレクション。
だから、まずは(1)の
Set ret = fsObj.Drives
で返り値用の変数ret
にモノホンのDrives
コレクションを突っ込んでおき、(3)の
Set Drives = ret
でニセのDrives
プロパティの返り値としてやる。
プロシージャ全体は
Public Prooerty Get Drives() As Object Dim ret As Object Set ret = fsObj.Drives Set Drives = ret End Property
こう。
メソッドの実装(笑)
メソッドも基本的にはこの方法。
このように、オブブラ(笑)で、
Sub
/Function
の別- 引数リスト
- 引数が
Optional
かどうか - 返り値の型
を確認し、それに応じてコーディングすればいい。
たとえば先の画像のCreateTextFile
メソッドならば、
- 種別は
Function
- 引数は
FileName
、OverWrite
、Unicode
の三つ。 - 引数
OverWrite
・Unicode
はOptional
- 返り値は
TextStream
型
なので、それに応じてコーディングする。
Public Function CreateTextFile( _ ByVal FileName As String, _ Optional ByVal OverWrite As Boolean = True, _ Optional ByVal Unicode As Boolean = False) As TextStream
となる。
しかし、TextStream
クラスもScripting.FileSystemObject
クラスのメンバなので、Object
にする。
Public Function CreateTextFile( _ ByVal FileName As String, _ Optional ByVal OverWrite As Boolean = True, _ Optional ByVal Unicode As Boolean = False) As Object
こうなる。
プロシージャ全体は
Public Function CreateTextFile( _ ByVal FileName As String, _ Optional ByVal OverWrite As Boolean = True, _ Optional ByVal Unicode As Boolean = False) As Object Dim ret As Object Set ret = fsObj.CreateTextFile(FileName, OverWrite, Unicode) Set CreateTextFile = ret End Function
こう。
引数が列挙体型のやつがある
あと、メソッドの中には引数がScripting.FileSystemObject
の中で定義された列挙体であるものがある。
たとえば、
このGetStandardStream
メソッドの場合、
Function GetStandardStream(StandardStreamType As StandardStreamTypes, [Unicode As Boolean = False]) As TextStream
とあるように、第1引数StandardStreamType
がStandardStreamTypes
という見慣れない型である。
こいつは、オブブラ(笑)で見ると
となっているように、Scripting
クラスで定義された列挙体なんである。
当然、Microsoft Scripting Runtime
を参照設定していないと使えない。
そういうときはどうするか。
このニセFileSystemObject
クラスモジュール内でPublic Enum
にしてしまえばいいのである!
'Declarations Section' 'Constants' Public Enum StandardStreamTypes StdIn = 0 StdOut = 1 StdErr = 2 End Enum
列挙体のメンバがそれぞれどの数値を表しているのか、というのもオブブラ(笑)先輩を見ればわかる。
ほれ。こんなふうに。
Scripting.__MIDL___MIDL_itf_scrrun_0001_0001_0003
ちゅうのは何のことやらわからんがw
おわりに
上記のようにして、ひたすらプロパティ・メソッドを実装(笑)し続けることを、「ネオ写経」と呼んでおります。
オブジェクトの仕組みがよくわかって実に勉強になります。
「新型コロナ自粛で勉強ぐらいしかすることがない」という人は、一度やってみてはいかがでしょうか?
ちなみに、FileSystemObject
は、配下にDrives
、Drive
、Folders
、Folder
、Files
、File
、TextStream
というScripting
内のオブジェクトを抱えているので、本気でFileSystemObject
クラスを丸ごとラップしようと思ったら、全部で八つもクラスモジュールを作ることになりますw
こんなふうに。
特に、Folders
-Folder
のような階層構造を持つクラスを表現するのにめちゃくちゃ頭を使いましたw
FormattedTextプロパティの怪
FormattedTextプロパティの怪
『Writing Word Macros』という本を買った。
FormattedTextプロパティ
Range
オブジェクトのところを読んでいたら、FormattedText
というプロパティについて書いてあった。
へえ。そんなものがあったのか。
で、『Word 2013 developer docs』(オフラインヘルプ)で調べてみた。
すると、
Returns or sets a Range object that includes the formatted text in the specified range or selection. Read/write.
こんなふうに書いてある。
「FormattedText
」という名前だが、Range
型らしい。
まあ、書式情報を含んでいないといけないわけだから、String
型のわけがないのだが。
実験
んで、ちょいと実験。
おなじみ、
このようなDocument(笑)を準備。
この中で、ゴシック体になっている「月面宙返り」の部分を、このDocument(笑)のケツに挿入するコードを書いてみる。
リスト1
Private Sub testFormattedTextProperty() Dim rng As Range Set rng = Selection.FormattedText '……(1)' With ActiveDocument Call .Range(.Range.End - 1, .Range.End - 1).Select '……(2)' Set Selection.FormattedText = rng '……(3)' End With End Sub
(1)の
Set rng = Selection.FormattedText
で変数rng
にSelection
オブジェクトのFormattedText
プロパティの返り値(Range
型)を突っ込む。
あとは
With ActiveDocument Call .Range(.Range.End - 1, .Range.End - 1).Select '……(2)' Set Selection.FormattedText = rng '……(3)' End With
の(2)でDocument(笑)の末尾位置を選択し([Range].End
プロパティは、末尾の改段落記号まで含めた数値を返すので、1
を引いておかないとカーソル移動ができず、エラーになるので注意。)、(3)でそのFormattedText
プロパティに(1)で取得したRange
オブジェクト(rng
)をセットする。
これで盤石である。時は来た。それだけだ!
先のDocument(笑)の「月面宙返り」の部分を
このように選択して、リスト1を実行。
なんと、コンパイルエラー……。
「不正」呼ばわりである。
冗談半分で、リスト1の(3)の部分を
Selection.FormattedText = rng
にして実行してみる。
すると、
えっ?! なんで???
やりたかったことが実現できたとはいえ、わけがわからん。
よくわからないこと
オブジェクト ブラウザーによると、
Word.Range
オブジェクトの既定メンバはText
プロパティである。
ということは、
Selection.FormattedText = rng
というのは
Selection.FormattedText.Text = rng.Text
ということにならないのだろうか。
FormattedText
プロパティがRange
型であり、「Read/write
」である以上、
Set Selection.FormattedText = rng
でないとおかしいように思うのだが……。
おわりに
ちなみに、たとえばリスト1の(3)のところを
Selection.FormattedText.Text = rng
にして実行すると、
このようにわけのわからない結果になる。
また、そもそもはコーディング・ミスだったのだが、リスト1の(3)を
Selection.FormattedText = rng.FormattedText
としても、
このように正しい(?)結果になる。
また、リスト1の(3)を
Selection.FormattedText = rng.Text
にすると、
「型が一致しません」というコンパイルエラーになる。
左辺と右辺をあれこれ変えてみた結果をまとめたのが
これ。
わからぬ……。
Tableオブジェクトの怪(Word)
Tableオブジェクトの怪(Word)
実に気色悪い現象に出くわしたので報告。
表の余分な行を削除する
たとえば、Wordでドキュメント内の表にデータを差し込むようなとき、
このように、使用しない行が生ずることがある。
宛先によってデータの数が異なるとき、テキトーな上司なら「ま、別にええんちゃう?」で済むのだが、神経質な上司だったりすると、「空白行は消さんかい!」などということに。
そこで、マクロで空白行を削除することを企てるのである。
マクロで空白行を削除する
次のようなコードで、空白行の削除を試みる。
リスト1
Private Sub test03() Dim tbl As Table Set tbl = ActiveDocument.Tables(1) '……(1)' Dim i As Long With tbl '……(2)' For i = .Rows.Count To 2 Step -1 '……(3)' If .Cell(i, 1).Range.Text = Chr(13) & Chr(7) Then '……(4)' Call .Rows(i).Delete Else Exit For End If Next End With End Sub
まず、(1)の
Set tbl = ActiveDocument.Tables(1)
で対象の表(Table
オブジェクト)を変数tbl
にぶち込む。
(2)の
With tbl
で記述をまとめておいて、(3)の
For i = .Rows.Count To 2 Step -1 If .Cell(i, 1).Range.Text = Chr(13) & Chr(7) Then '……(4)' Call .Rows(i).Delete Else Exit For End If Next
のFor
ループ。
「削除するときはケツから!」の原則に基づいて、Table
オブジェクトのRows
コレクションのCount
プロパティの値、すなわち表の行数からスタートして、2
行目まで繰り返すことにする。
ループ内では、(4)の
If .Cell(i, 1).Range.Text = Chr(13) & Chr(7) Then
で、1列目に文字が入っているかどうかを判定。
Wordの表では、文字の入っていないセルにはChr(13)
とChr(13)
が入っている。
セル内に文字が入っていなければ
Call .Rows(i).Delete
で行ごと削除。
セル内に文字が入っていれば、(上の行から順にデータを入れている以上)これ以上削除する行はないと言うことだからElse
ブロックに進んで
Exit For
でループを抜ける。
実行
これで基本的にはうまいこと行くはずである。
しかし、ループに突入し、一つ目(つまり、5行目)を削除した途端、
工工工エエエエエエェェェェェェ(゚Д゚)ェェェェェェエエエエエエ工工工
突然表の横幅がビニョーーーーンと伸びてしもたやないか……。
[Table].Columns
コレクションからColumn
オブジェクトを取得してWidth
プロパティを調べてみる。
上が無残にも横に引き延ばされてしまったTables(1)
、下があらかじめ同じものをコピッペしておいたTables(2)
である。
このように、全然違うサイズに変わり果ててしまっていることがわかる。
おわりに
さっぱりわけがわからん。
何故、何故なんだ~?!(2回目。)
改行マークの怪(Word)
改行マークの怪
前回
の続き。
改行マークの正体とは?
まず、
このようなドキュメント(笑)を用意し、画像のように改行マークを選択状態にしておく。
そして、イミディエイトに
?Asc(Selection.Range.Text)
と入力して[Enter]を押す。
このように、「11
」を得た。
コチラの文字コード表によると、「11
」は、
なんと、「VT
」というよくわからないものだった。
てっきり「10
」の「LF
」だと思っていたのだが。
検証
では、選択部分を文字コード「10
(LF
)」にするとどうなるのか。
このように改行マークを選択した状態でイミディエイトに
Selection.Range.Text = Chr(10)
と入力して[Enter]
を押す。すると、
工工工エエエエエエェェェェェェ(゚Д゚)ェェェェェェエエエエエエ工工工
これ、改段落マークとちゃいますのん???
さらに検証
では、この「改段落マーク」は何ものなのだろうか。
先ほど出現した「改段落マーク」(「Chr(10)
」のはず。)を選択状態にして、イミディエイトに
?(Selection.Range.Text = vbCr)
と入力して[Enter]
を押す。すると、
(゚Д゚)ハァ?(゚Д゚)ハァ?(゚Д゚)ハァ?
えっ……なんで……??? そうなの???
さらに、イミディエイトに
?(Selection.Range.Text = Chr(13))
と入力して[Enter]
を押す。すると、
(゚Д゚)ハァ?(゚Д゚)ハァ?(゚Д゚)ハァ?
えっ……なんで……??? Selection.Range.Text
はChr(10)
にしたやんか……。
さらに、イミディエイトに
?(Selection.Range.Text = Chr(10))
と入力して[Enter]
を押す。すると、
(゚Д゚)ハァ?(゚Д゚)ハァ?(゚Д゚)ハァ?
そもそもSelection.Range.Text
をChr(10)
にしたはずなのに、一周回ってChr(10)
じゃなくなっとる……。
おわりに
さっぱりわけがわかりまへん。
改行・改段落の怪(Word)
改行・改段落の怪
前回
の続き。
前回のリスト1を再掲する。
前回のリスト1
'テキストの置換' Private Sub replaceText(ByVal str1 As String, _ With Selection.Find Call .ClearFormatting Call .Replacement.ClearFormatting End With With Selection.Find Call .Execute(FindText:=str1, _ replacewith:=str2, _ Replace:=wdReplaceAll) End With With Selection.Find Call .ClearFormatting Call .Replacement.ClearFormatting End With End Sub Private Sub removeUnsightlyCR() '連続するCarriageReturnを一つにする' Call replaceText(vbCr & vbCr, vbCr) '……(*)' End Sub
これの(*)の部分、replaceText
メソッドの第2引数をvbLf
に変えたらどうなるのだろうか。やってみた。
vbCrをvbLfに置き換える
前掲リストの(*)部分を次のように書き換える。
Call replaceText(vbCr & vbCr, vbLf)
そして、
おなじみ、このドキュメント(笑)を用意して、上掲コードを実行する。
なかなか衝撃的な結果ではあるまいか。
てっきり
こうなるものと思っていたのだが。
おわりに
ますますわけがわからなくなってきたぞ。
VBAによる置換の怪(Word)
VBAによる置換の怪
ちょっと変な現象に出くわしたので報告。
無駄な改段落マークを削除する
最近、Webページ上で公開されている議事録の類をWordドキュメント化する作業にハマっている。今すぐ役に立つわけではないけれど、後で利用するときに楽かな、と思って。
Webページ上からWordドキュメントにテキスト部分をコピッペして、後は主にマクロを使って整形する。
そのときにやたら遭遇するのが
のようなパターン。
行と行の間に無駄な改段落マークがあるやつ。
まずはこいつを一掃したかった。
要は、二つ連なっている改段落マークを一つにすればよいのだから、次のようなコードでやった。
リスト1
'テキストの置換' Private Sub replaceText(ByVal str1 As String, _ ByVal str2 As String) With Selection.Find '……(1)' Call .ClearFormatting Call .Replacement.ClearFormatting End With With Selection.Find '……(2)' Call .Execute(FindText:=str1, _ replacewith:=str2, _ Replace:=wdReplaceAll) End With With Selection.Find '……(3)' Call .ClearFormatting Call .Replacement.ClearFormatting End With End Sub Private Sub removeUnsightlyCR() '……(4)' '連続するCarriageReturnを一つにする' Call replaceText(vbCr & vbCr, vbCr) End Sub
まずは、replaceText
メソッド。
str1
とstr2
の二つの引数を受け取って、ドキュメント中のstr1
をstr2
に置換する。それだけ。
(1)の
With Selection.Find Call .ClearFormatting Call .Replacement.ClearFormatting End With
でFind
オブジェクトの設定をリセットする。
次に(2)の
With Selection.Find '……(2)' Call .Execute(FindText:=str1, _ ReplaceWith:=str2, _ Replace:=wdReplaceAll) End With
でFind.Execute
メソッドを実行する。
str1
をstr2
に置換したいので、引数FindText
にstr1
を、引数ReplaceWith
にstr2
を渡す。
また、全て置換するために引数Replace
にはwdReplaceAll
を渡す。
後は、(3)の
With Selection.Find Call .ClearFormatting Call .Replacement.ClearFormatting End With
で再度Find
オブジェクトをリセットしておしまい。
このreplaceText
メソッドを、(4)の
Private Sub removeUnsightlyCR() '連続するCarriageReturnを一つにする' Call replaceText(vbCr & vbCr, vbCr) End Sub
のように、str1
にvbCr & vbCr
(二つ連なった改段落マーク)、str2
にvbCr
を指定して実行することによって、行のカンチャンの目障りな改行マークを一掃するのである!
実行
さて、
この状態で、上記リスト1のremoveUnsightlyCR
を実行すると、当然
こうなる。
しかし!
この状態で、イミディエイトに
?ActiveDocument.Paragraphs.Count
と打ち込んで[Enter]を押すと、
なんと、段落数は1
なのである。
どう見ても5
なのに。
標準機能で置換する
ちなみに、
このように、標準機能を用いて置換した場合、
段落数は5
になる。VBAでやった場合と標準機能を用いた場合とで結果が異なるのである。まさにち~んw珍現象!
おわりに
テキストドキュメントを整形する機会のある人は、注意しましょう。
段落冒頭の半角スペースを除去する(Word)
各段落冒頭の半角スペースを取り除く
Webで公開されている議事録の類をWordドキュメント化することが割と増えた。
しばらく待っているとPDFで正式な議事録が出される場合もあるが、割と時間がかかる上、PDFだと記載内容をコピッペする際に割とめんどくさい。
そこで、Webページに掲載されている議事録のテキストをWordドキュメントにコピッペして整形する、という方法をとった。
段落冒頭にことごとく半角スペースがある問題
Webページから直接コピッペすると、改行位置もちゃんと反映されるので、整形するにあたっては実に楽。
ただし、今回私が取り扱った物件は、
行頭にことごとく半角スペースが入っている
という実にうっとうしいものであった。
何せ、Wordドキュメントで約30ページ、4万字超の議事録が五つも六つもあるのである。手作業で取り除くのはナンセンス。
ただ半角スペースの全てを取り除けば良いわけではないから、置換も使えない。
そこで、マクロでやることにした。
考え方
次のように考えた。
- 取り除きたいのは段落冒頭の半角スペースに限る。
- したがって、まずは改段落マークの場所(
Range
オブジェクト)を取得する。 - 改段落マークの場所を取得したら、その次の文字の場所(
Range
オブジェクト)を取得する。 - 次の文字の場所を表す
Range
オブジェクトのText
プロパティの値を調べ、そいつが半角スペースだったら""
で置きかえる。 - 検索で改段落マークがヒットしなくなるまでループ
うむ、万全である!
指定した文字列の場所(Range)を取得するメソッド
getNextTextRangeメソッド
Private Function getNextTextRange( _ ByVal tgtText As String) As Range Dim ret As Range With Selection.Find Call .ClearFormatting Call .Replacement.ClearFormatting End With With Selection.Find .Text = tgtText .Replacement.Text = "" .Wrap = wdFindStop .Format = False .Highlight = False .MatchCase = False .MatchWholeWord = False .MatchByte = False .MatchAllWordForms = False .MatchSoundsLike = False .MatchWildcards = False .MatchFuzzy = False End With Call Selection.Find.Execute If Not Selection.Find.Found Then GoTo Finalizer Set ret = Selection.Range Call Selection.Collapse(wdCollapseEnd) Finalizer: With Selection.Find Call .ClearFormatting Call .Replacement.ClearFormatting End With Set getNextTextRange = ret End Function
Find
オブジェクトを使う際の宿命、どうしてもタテ長になってしまう。しかし、やっていることは非常に簡単。引数tgtText
で受け取った文字列が見つかったら、その場所を取得してRange
オブジェクトを返すだけ。
段落冒頭の半角スペースを除去する
上記getNextTextRange
を用いて、同じく上記「考え方」を元に次のリスト1を作成。
リスト1
Private Sub removeSBSpaceAtTheTopOfParagraph() Dim tmpRange As Range Do Set tmpRange = getNextTextRange(vbCr) '……(1)' If tmpRange Is Nothing Then Exit Do Call tmpRange.Select '……(2)' Call Selection.Collapse(wdCollapseEnd) Call Selection.MoveRight(wdCharacter, 1, wdExtend) '……(3)' If Selection.Range.Text = " " Then '……(4)' Selection.Range.Text = "" End If DoEvents Loop Call ActiveDocument.Range(0, 0).Select End Sub
いきなりDo
ループに突入!
(1)からの2行、
Set tmpRange = getNextTextRange(vbCr) If tmpRange Is Nothing Then Exit Do
で先のgetNextTextRange
メソッドを用いて直近の改行改行マークの場所(Range
オブジェクト)を取得。
tmpRange
がNothing
だったらループを抜ける。
次に、(2)からの2行、
Call tmpRange.Select Call Selection.Collapse(wdCollapseEnd)
で、先ほど取得したRange
オブジェクトを選択状態にし、
さらに選択範囲を後方に向かって潰しておく。
そして、(3)の
Call Selection.MoveRight(wdCharacter, 1, wdExtend)
で、右に向かって1文字分だけ選択範囲を広げる。
これで、改行マークの次の1文字を選択した状態になる。
あとは、(4)からの3行、
If Selection.Range.Text = " " Then Selection.Range.Text = "" End If
で、選択されている箇所(次の段落の冒頭)が半角スペースだったらそいつを""
に置きかえる。
この繰り返し。
ちなみに、getNextTextRange
がドキュメント(笑)最後の改行マークの場所を取得したときは、
Call tmpRange.Select
を実行すると、
こうなって、
Call Selection.Collapse(wdCollapseEnd)
を実行して、
こうなって、
Call Selection.MoveRight(wdCharacter, 1, wdExtend)
を実行して、
こうなる。んで、この状態で次のループに突入してgetNextTextRange
メソッドを実行すると、
このように、なぜか改行マークが検索でヒットせず(Find.Found
プロパティがFalse
を返す)、getNextTextRange
メソッドがNothing
を返すので、無事にDo
ループから抜け出すことができる。
最後に動作の様子をお目にかけよう。
うむ、バッチリである!!!!!!!!!!!!!!!!
おわりに
んで、ここまで書いておいてアレなんですが……。
これ、
[Ctrl]+[ H ]で置換ダイアログ呼んで、「検索する文字列」に「^p
」(「 ^p
」と半角スペース)、「置換後の文字列」に「^p
」と入力して置換したら一発
ということに気づきましたよ。とほほ……。