WMIのWin32_Printerクラス
WMIのWin32_Printerクラス
VBAでプリンタまわりを操作したいなあと思って、あれこれ調べていると、「WMI(Windows Management Instrumentation)」というものを使うという道があることがわかった。
そうしょっちゅう使うわけでもないが、覚書として記しておくことにする。
ちなみに、Shell
を使う方法については、
コチラをどうぞ。
目次
お世話になったサイト
いろいろ調べているうちにたどり着いたのは、
こちら。
このページには、次のサンプルコードが掲載されている。
Option Explicit 'WMIにて使用する各種オブジェクトを定義・生成する。' Dim oClassSet Dim oClass Dim oLocator Dim oService Dim sMesStr 'ローカルコンピュータに接続する。' Set oLocator = WScript.CreateObject("WbemScripting.SWbemLocator") Set oService = oLocator.ConnectServer 'クエリー条件を WQL にて指定する。' Set oClassSet = oService.ExecQuery("Select * From Win32_Printer") 'コレクションを解析する。' For Each oClass In oClassSet sMesStr = sMesStr & "プリンタの名前: " & oClass.Caption & vbCrLf & _ "プリンタのドライバー名: " & oClass.DriverName & vbCrLf & _ "プリンタのポート: " & oClass.PortName & vbCrLf & _ "デフォルトプリンタか?: " & CStr(oClass.Default) & vbCrLf & vbCrLf Next MsgBox "プリンタに関する情報です。" & vbCrLf & vbCrLf & sMesStr '使用した各種オブジェクトを後片付けする。' Set oClassSet = Nothing Set oClass = Nothing Set oService = Nothing Set oLocator = NothingWMI Fun!! 様より
詳しいことはよくわからんが、WbemScripting
というライブラリにあるSWbemLocator
クラス(?)から順に下位のクラスを取得していっている模様。
上掲ソースコードはVBSのものだが、VBEで[ツール]→[参照設定]で、Microsoft WMI Scripting V1.2 Library
にチェックを入れれば、オブジェクト ブラウザーである程度まで中身を見ることができる。
上掲コードの
Set oLocator = WScript.CreateObject("WbemScripting.SWbemLocator")
で変数oLocator
にぶち込まれるのは、たぶん、
このWbemScripting.SWbemLocator
クラスのインスタンス。
んで、
Set oService = oLocator.ConnectServer
こいつによって変数oService
にぶち込まれるのは、たぶん、
このSWbemServices
オブジェクト。
さらに。
Set oClassSet = oService.ExecQuery("Select * From Win32_Printer")
で、SWbemServices
オブジェクトのExecQuery
メソッドによってWin32_Printer
というオブジェクトを取得し、変数oClassSet
にぶち込んでいる(のだと思う)。
変数の名前からして、こいつは、
SWbemObjectSet
というオブジェクトだと思う。
あとは、For ~ Each
を使ってSWbemObjectSet
からSWbemObject
(インストールされているプリンタ一つ一つに対応)を取り出して、そのプロパティの値を取得している(のだと思う)。
んで、このとき一つ一つ取り出されるオブジェクトは、
SWbemObject
だろう。
VBAに移植する
先に引用したコードを、VBA向けに移植する。
せっかくMicrosoft WMI Scripting V1.2 Library
を参照設定済みなので、アーリー・バインディング方式でコーディングすることとしよう。
上掲コードではメッセージボックスに表示するようにしているが、うっとうしいので、イミディエイト・ウィンドウに出力するようにしているので、あしからず。
リスト1 標準モジュール
Private Sub test01() Dim currLocator As WbemScripting.SWbemLocator Dim tgtServices As WbemScripting.swbems Dim tgtClassSet As WbemScripting.SWbemObjectSet 'ローカルコンピュータに接続する。' Set currLocator = New WbemScripting.SWbemLocator Set tgtServices = currLocator.ConnectServer 'クエリー条件を WQL にて指定する。' Set tgtClassSet = tgtServices.ExecQuery("SELECT * FROM Win32_Printer") Dim tgtClass As WbemScripting.SWbemObject Dim tmp As String 'コレクションを解析する。' For Each tgtClass In tgtClassSet With tgtClass tmp = "プリンタの名前: " & .Caption & vbCrLf & _ "プリンタのドライバー名: " & .DriverName & vbCrLf & _ "プリンタのポート: " & .PortName & vbCrLf & _ "デフォルトプリンタか?: " & CStr(.Default) & vbCrLf & vbCrLf End With Debug.Print tmp Next '使用した各種オブジェクトを後片付けする。' Set tgtClass = Nothing Set tgtClassSet = Nothing Set tgtServices = Nothing Set currLocator = Nothing End Sub
残された課題
さて。上掲リスト1を実際にVBEで入力してみるとわかることなのだが、For ~ Each
ループの中、すなわち、個別のプリンタを指し示すオブジェクト(変数tgtClass
にぶち込まれている)については、入力補完が効かない。
もし、これが不便だと感じるのならば、たとえばSWbemObject
をラップしたクラスを作って、Win32_Printer
クラスのプロパティ・メソッドを自力で実装する、という手もあるかも知れない。
ここを見たらできるはず。
おわりに
まあ、WMIを以てしても、プリンタ名の「on ~
」の部分は取得できないっぽいので、そこまでムキになることもないかな。
写経の一環としてやってみたら面白いかも知れませんけど。
自作クラスのデフォルトメンバに関する大発見 その2
自作クラスのデフォルトメンバに関する発見 その2
このとき
から10箇月の時を経て、またまた世紀の大発見をしたので、紹介します。
あのときの方法
詳しくはコチラを読んでいただきたいが、早い話が、
インスタンスを突っ込んだ変数をカッコでくくれば、オブジェクト型のデフォルトメンバが発動する
ということ。
たとえば、Worksheet
オブジェクトをラップしたPoweredSheet
クラスのデフォルトメンバSelf
メソッドが、ラップされているWorksheet
オブジェクトを返すとする。
このとき、
Dim Sh As Worksheet Set Sh = ActiveSheet Dim ps As PoweredSheet Set ps = New PoweredSheet Call ps.init(Sh)
のようにしたとしても、変数ps
をWorksheet
型の引数として別メソッドに渡すことはできない。
たとえば、
Private Function getSheetName( _ ByVal TargetSheet As WorkSheet) As String getSheetName = TargetSheet.Name End Function
というメソッドがあったとして、このメソッドに
Debug.Print getSheetName(ps)
と、ps
を引数として渡してもだめである。
しかし、
Debug.Print getSheetName((ps))
このように、変数ps
をカッコでくくってやると、ちゃんと動く。
これが、前回の発見であった。
今回の発見
今回の発見は、次の通り。
すなわち、
変数の後ろにカッコをつけてもデフォルトメンバが呼ばれる
これである。
先の例で言えば、
Debug.Print getSheetName(ps())
こうすれば良いのである。
ほれ、この通り。
空の丸カッコを付けるのは、プログラマにとっては違和感がないと思う。
おわりに
ただ、ちょっと残念なのは、ps().
まで打ち込んだときに出てくる入力候補がWorksheet
クラスのものではなく、あくまでもPoweredSheet
のものになってしまう点。
実に残念。(Name2
はPoweredSheet
クラスに設置したデタラメなプロパティ。)
VBAでプリンタを変更する
プリンタの切り替え
VBAで、使用中のプリンタを切り替える方法。
目次
かつての考え方
Application.ActivePrinterプロパティで切り替える
VBAでプリンタを切り替えるには、Application.ActivePrinter
プロパティの値を変えるしかないと思い込んでいた。
しかし、この方式には難点がある。
Apploication.ActivePrinterプロパティの難点
Application.ActivePrinter
プロパティでプリンタを切り替えるときには、
Application.ActivePrinter = "JUST PDF 3 on Ne02:"
のように、プリンタ名だけでなく、「on Ne02:
」の部分も渡さなければならない。
この「on
」以下の部分がくせ者で、どうやって取得したらよいのかわからなかった。
強引な解決策
だから、以前、
このような、実に乱暴な対応を考えたのだった。
とりあえず、このやり方なら、「on
」以下の部分が、「on NeXX:
」か、「on nul:」になっているなら、(時間は少々かかってしまうが、)取得は可能だ。
問題点
しかし、このやり方には当然問題がある。
上記のように、「on
」以下の部分が、「on NeXX:
」か「on nul:
」でなかったとしたら対応できない、ということだ。
こんな方法があった
ところが、実に簡単な解決策があった。
Worksheet.PrintOutメソッドの引数で切り替える
見出しのように、Worksheet.PrintOut
メソッドの引数でプリンタを指定するという方式である。
実は、Worksheet.PrintOut
メソッドの引数ActivePrinter
に渡すのは、プリンタ名だけで良い。
上記の例でいえば、
Dim Sh As Worksheet Set Sh = ActiveSheet Call Sh.PrintOut(ActivePrinter:="JUST PDF 3")
だけで良いのである。
プリンタ名を取得するだけなら割と簡単にできる。(ShellとかWMIを使う。)
この方法なら「on Ne:XX
」について調べなくてもプリンタを切り替えることが出来て便利だ。
Application.ActivePrinterプロパティの値は切り替わってしまう
ただし、(当然のことながら、)この方法で印刷をすると、Application.ActivePrinter
の値は変わってしまう。
Private Sub test02() Dim Sh As Worksheet Set Sh = ActiveSheet Debug.Print "印刷前 :" & Application.ActivePrinter Call Sh.PrintOut(ActivePrinter:="JUST PDF 3") Debug.Print "印刷後 :" & Application.ActivePrinter End Sub
こんなふうに。
解決策
とはいえ、対応は実に簡単。前もってApplication.ActivePrinter
の値を取得しておき、印刷後、元に戻してやればよろしい。
Private Sub test02() Dim Sh As Worksheet Set Sh = ActiveSheet Dim orgPrinter As String orgPrinter = Application.ActivePrinter Debug.Print "印刷前 :" & Application.ActivePrinter Call Sh.PrintOut(ActivePrinter:="JUST PDF 3") Debug.Print "印刷直後:" & Application.ActivePrinter Application.ActivePrinter = orgPrinter Debug.Print "戻したよ:" & Application.ActivePrinter End Sub
こいつを実行すると、
こんなふうにJUST PDF 3で印刷を実行し、終了後のイミディエイト・ウィンドウには、
このように出力される。
おわりに
あの苦労は一体何だったのか。
範囲内でのセルの相対位置を求める
セルの範囲内での相対位置を求める
特定のセルが、指定範囲内の上から何番目にあるのかを求める必要があったので作った。
「指定範囲」は1列、「特定のセル」は1個限定。
セルの相対位置を返すFunction
とりあえず、指定範囲を上から順に当たっていって、対象のセルの位置を見つけたときの順番を表す数を返したらいいと思った。
リスト1 標準モジュールRangeUtil
Public Function getRelativePosition( _ ByVal TargetRange As Range, _ ByVal Target As Range) As Long '……(1)' Dim ret As Long ret = 0 'Guard clause' If TargetRange.Columns.Count > 1 Then GoTo Finalizer '……(2)' If Target.Count > 1 Then GoTo Finalizer Dim rng As Range Set rng = Application.Intersect(TargetRange, Target) If rng Is Nothing Then GoTo Finalizer 'Main process' Dim i As Long Dim addressStr As String For i = 1 To TargetRange.Rows.Count '……(3)' If TargetRange.Cells(i, 1).Address = Target.Address Then ret = i Exit For End If Next 'Return value' Finalizer: getRelativePosition = ret End Function
まず、(1)の
Public Function getRelativePosition( _ ByVal TargetRange As Range, _ ByVal Target As Range) As Long
で引数と返り値の設定。
引数TargetRange
でセル範囲を受け取り、引数Target
で位置を調べる対象のセルを受け取る。
ちなみに、引数名の記法はパスカル記法にした。組み込みの引数名とかぶらないように考えるのがめんどくさくなったから。
そのうち、メソッド名もパスカル記法に変える日が来ると思う。いまのところキャメル記法だけど。
(2)からの5行、
If TargetRange.Columns.Count > 1 Then GoTo Finalizer If Target.Count > 1 Then GoTo Finalizer Dim rng As Range Set rng = Application.Intersect(TargetRange, Target) If rng Is Nothing Then GoTo Finalizer
はガード節。
セル範囲が2列以上あるとき、位置を調べる対象セルが2個以上あるとき、セル範囲内に位置を調べる対象セルがないとき、にそれぞれ「0
」を返すようにした。
(3)からの6行、
For i = 1 To TargetRange.Rows.Count If TargetRange.Cells(i, 1).Address = Target.Address Then ret = i Exit For End If Next
が位置を調べる処理。
1
からセル範囲の行数分だけループして、セルのアドレスが一致した時点でループを抜ける。
その時点での変数i
の値が、〝上から何番目かを表す値〟になっているはず。
使ってみる
まず、
こんなセル範囲を用意する。
A1セル~A14セルまでの範囲に、「TargetRange
」という名前が付けてある。
こうしておいて、次のコードで実験してみる。
リスト2 標準モジュールModuleMain
Private Sub detectRelativePosition() Dim relPos As Long relPos = RangeUtil.getRelativePosition( _ TargetRange:=Sh01.Range("TargetRange"), _ Target:=Selection) If relPos < 1 Then Call Provoke.makeUserSick( _ Message:="選択箇所がおかしいわボケwww", _ MsgBoxIcon:=mbiCritical, _ Title:="残念www") Exit Sub End If Call Provoke.makeUserSick( _ Message:="お前が選んだセルは、範囲内の上から" & _ CStr(relPos) & "番目やwww。", _ MsgBoxIcon:=mbiInformation, _ Title:="選択セルの範囲内相対位置を調べた結果www") End Sub
選択しているセルが、「TargetRange」と名付けたセルの上から何番目にあるのかを、ちょっと腹の立つメッセージボックスで表示するというだけのプログラム。
ちなみに、コード中のProvoke
というのは標準モジュールの名前で、その中にmakeUserSick
というメソッドを書いている。(標準モジュールProvoke
内のコードは後掲する。)
こいつを、
こんなふうに配置したコマンドボタンに登録して使う。
動作風景
こんな風に動作する。
おわりに
もしかして、セル範囲内の相対位置を返す組み込みの関数とかそういうのがあったりするんでしょうか?
註
標準モジュールProvoke
Option Explicit Public Enum MsgBoxIcon mbiCritical = vbCritical mbiExclamation = vbExclamation mbiInformation = vbInformation mbiQuestion = vbQuestion End Enum '///ち~んw用' Private Const MAKE_USER_SICK_2013 As String = _ " _______" & vbCrLf & _ " / \" & vbCrLf & _ "/ /・\ /・\ \" & vbCrLf & _ "|  ̄ ̄  ̄ ̄ | ち~んw" & vbCrLf & _ "| (_人_) |" & vbCrLf & _ "| \ | |" & vbCrLf & _ "\ \_| /" Private Const MAKE_USER_SICK_2010 As String = _ " _______" & vbCrLf & _ " / \ " & vbCrLf & _ "/ /・\ /・\ \" & vbCrLf & _ "|  ̄ ̄  ̄ | ち~んw" & vbCrLf & _ "| (_人_) |" & vbCrLf & _ "| \ | |" & vbCrLf & _ "\ \_| /" '///ユーザーを煽るAAを表示する' Public Sub makeUserSick( _ Optional ByVal Message As String, _ Optional ByVal MsgBoxIcon As MsgBoxIcon, _ Optional ByVal Title As String) If Message = "" Then Message = "涙拭けよwww" Dim ver As String ver = Application.Version Dim str As String Select Case ver Case "14.0" str = MAKE_USER_SICK_2010 Case "15.0" str = MAKE_USER_SICK_2013 Case "16.0" str = MAKE_USER_SICK_2013 Case Else str = MAKE_USER_SICK_2010 End Select Call MsgBox(Prompt:=Message & vbCrLf & str, _ Buttons:=MsgBoxIcon, _ Title:=Title) End Sub
VBAでWordドキュメントに行番号を振る
VBAでWordドキュメントに行番号を振る
最近、〈脱・パワポ運動〉の一環として、説明用資料の類をWordで作成するようにしています。
パワポで作るいわゆる「ポンチ絵」のわかりにくさ/非効率を解消するのが目的です。
Wordで作成したドキュメントの「参照指示性」を劇的に上げるための方法が、『シラバス論』の著者、芦田宏直氏が発明した、〈Wordで作成したドキュメント全体に通しで行番号を振る〉というものです。
これ、アホみたいに簡単なんですけど、効果は絶大!
お試しあれ。
文書全体に通しで行番号を振る
これは、めっちゃ簡単。
こういう、ごく普通の文書があるとする。
「ページ レイアウト」タブから
「行番号」を選択し、
「連続番号」を選択すると、
ほれ、このとおり、通しの行番号が振られる。
めちゃくちゃ簡単。たったこれだけのことで、資料の「参照指示性」は飛躍的に高まる。「(○ページの)〇〇行目のところを話しますね。」で済む。
パワポのポンチ絵だとこうはいかない。「えっと、○ページの左上のコマの真ん中やや右のあたりに……」みたいになってわけがわからなくなる。
LineNumberingオブジェクト
この行番号機能を司るのは、VBAの場合、「LineNumbering
オブジェクト」というらしい。
「Word2013 developer docs」(Word2013のオフラインヘルプ)によると、
LineNumbering Object (Word)
Represents line numbers in the left margin or to the left of each newspaper-style column.
Remarks
Use the LineNumbering property to return the LineNumbering object. The following example applies line numbering to the text in the first section of the active document.
VBA With ActiveDocument.Sections(1).PageSetup.LineNumbering .Active = True .CountBy = 5 .RestartMode = wdRestartPage End WithThe following example applies line numbering to the pages in the current section.
VBA Selection.PageSetup.LineNumbering.Active = True
ということらしい。
どうやら、[Document].[Section].PageSetup
オブジェクトのLineNumbering
を参照したら得られるLineNumbering
オブジェクトが司っているものらしい。
LineNumbering
オブジェクトには、八つのプロパティがある。
同じく「Word2013 developer docs」によると、
Name Description Active True if line numbering is active for the specified document, section, or sections. Read/write Long. Application Returns an Application object that represents the Microsoft Word application. CountBy Returns or sets the numeric increment for line numbers. Read/write Long. Creator Returns a 32-bit integer that indicates the application in which the specified object was created. Read-only Long. DistanceFromText Returns or sets the distance (in points) between the right edge of line numbers and the left edge of the document text. Read/write Single. Parent Returns an Object that represents the parent object of the specified LineNumbering object. RestartMode Returns or sets the way line numbering runs -- that is, whether it starts over at the beginning of a new page or section or runs continuously. Read/write WdNumberingRule. StartingNumber Returns or sets the starting line number. Read/write Long.
となっている。
総行番号にする場合だと、
StartingNumber
プロパティを1
に、CountBy
プロパティを1
に、RestartMode
プロパティをwdRestartContinuous
に、Active
プロパティをTrue
に
したらよさげ。
ドキュメントに総行番号を振るコード
ThisDocument
に総行番号を振るだけのコードを示す。
リスト1 標準モジュール
Private Sub activateLineNumbering() Dim lnNumbering As LineNumbering '……(*)' Set lnNumbering = ThisDocument.Sections(1).PageSetup.LineNumbering With lnNumbering .StartingNumber = 1 .CountBy = 1 .RestartMode = wdRestartContinuous .Active = True End With End Sub
通常、(*)のところは、オフラインヘルプのサンプルコードのように
With ThisDocument.Sections(1).PageSetup.LineNumbering
とでも書くのだろうけれど、「LineNumbering
オブジェクトを使っているんだ!」という意識を高めるために(笑)、あえて変数に突っ込んで使っている。
ちなみに、Active
プロパティがなんでBoolean
型でなくてLong
型なのかはわからん。
実行
リスト1を実行してみる。
この状態で実行すると、
バッチリ。
おわりに
これで、たくさんのWordドキュメントに一気に行番号表示させることができる。
Rangeオブジェクトの終端があるParagraphオブジェクトのインデックス番号を返すFunction(Word)
Rangeオブジェクトの終端があるParagraphオブジェクトのインデックス番号を返すFunction
更新頻度ガタ落ちですが、またしてもWordVBAネタです。
まずはコードを
お急ぎの方は、コードをコピッペして使ってください。
リスト1
Public Function getParagraphIndex( _ ByVal tgtRange As Range) As Long Dim ret As Long tgtRange.Start = 0 ret = tgtRange.Paragraphs.Count getParagraphIndex = ret End Function
たったこんだけ。正味2行w
まさかこんなに簡単にできるとは思っていませんでした。
使ってみる
たとえば、テキトーなドキュメントを用意して、
こんなふうにテキトーに範囲を選択しておく。
画像では、4~5段落にまたがった範囲を選択している。
で、イミディエイト・ウィンドウに次のコードを書いて[Enter]を押す。
?getParagraphIndex(Selection.Range)
すると、
ちゃんと「5
」が出力されておる。
バッチリ!
解説
こんなしょうもないコードだが、二つも発見があった。
短いコードなので再掲する。
リスト1(再掲)
Public Function getParagraphIndex( _ ByVal tgtRange As Range) As Long Dim ret As Long tgtRange.Start = 0 '……(1)' ret = tgtRange.Paragraphs.Count '……(2)' getParagraphIndex = ret End Function
Start(End)プロパティはRead/Writeだった
これは、完全に思い込み。
勝手にRead onlyだと固く信じて疑っていなかった。
Microsoft Docsの「Range.Start Property
」の項にも、
Range.Start property (Word)
Returns or sets the starting character position of a range. Read/write Long.
と明記してあるし。
(1)の
tgtRange.Start = 0
によって、tgtRange
が指し示すRange
オブジェクトの始端をドキュメントの先頭にしているわけだ。
RangeオブジェクトにもParagraphsコレクションがある
これも全然知らなかった。
「Paragraphs
」というぐらいだからてっきりDocument
オブジェクトの直参だと思っていた。
これまた、Microsoft Docsの「Range.Paragraphs Property
」の項にはっきりと
Range.Paragraphs property (Word)
Returns a Paragraphs collection that represents all the paragraphs in the specified range. Read-only.
と書いてある。
つまり、(1)を実行した段階で、tgtRange
が指し示すRange
オブジェクトの範囲は、〈ドキュメントの始端~選択範囲の終端〉になっているわけなので、(2)の
ret = tgtRange.Paragraphs.Count
によって、〈ドキュメントの始端~選択範囲の終端〉に含まれる段落数、すなわち選択範囲の終端がある段落が先頭から数えて何番目か、を表す数値が変数ret
に返る、というわけだ。
最初にこのアイディアを考えついたやつは天才だと思う。
おわりに
世の中、天才だらけでいやになるぜ。
追記(2022/02/18)
実は、ここで示したコードには重大なバグがあります。
なんと、段落の先頭にカーソルを置いて、引数tgtRange
にSelection.Range
を渡すと、一つ前の段落のインデックスを返してしまうのです。
取り急ぎ、応急処置をしたものを次に挙げておきます。
Public Function GetParagraphIndex( _ ByVal a_Target As Range) As Long Dim ret As Long Dim rng As Range Set rng = a_Target rng.Start = 0 ret = rng.Paragraphs.Count Dim pos As Long pos = rng.End If pos = 0 Then GoTo ReturnValue Dim char As String char = rng.Parent.Range(pos - 1, pos).Text If char = Chr(13) Then ret = ret + 1 End If ReturnValue: GetParagraphIndex = ret End Function
この記事を書いた当時から、コーディングスタイルが変わったので、ちょっと書きぶりは違いますが。
一応、引数a_Target
で渡されたRange
オブジェクトのStart
プロパティを0
にしたときに、Range
オブジェクトの終端が改段落だったら1
を加算して返すようにしました。
これで良いのかどうか、引き続き検証します……。
参照元に参照先の通し行番号を書き込む(2)
参照元に参照先の通し行番号を書き込む(2)
たとえば、
みたいなドキュメントがあるとする。
参照指示性を高めるために、全ての行に通しで行番号を振っている。
これは、芦田宏直氏のアイディアで、詳しいことは氏の著書『シラバス論』(2019 晶文社)をご覧ください。
問題は、文中に「〇〇行目を参照」などと書いた場合。
「相互参照」機能を使えば、段落番号とかページ番号なんかは設定できるんだが、通しの行番号というのがない。
参照先の位置などというものは、編集の都合で揺れ動くものなので、「相互参照」的に設定できないのは非常につらい。
そこで、
このとき、ブックマークを利用して参照先の通しの行番号を割り出し、参照元の行番号の部分を書き換える方法を編み出した。
あとは、前回発覚した
ブックマーク部分の文字を書き換えたらブックマークが消滅する問題
を解消すればよろしい。
消滅するブックマークを復活させる考え方
まず、次のように考えた
- 参照元の
[Range].Text
を書き換える前に、参照元のRange
オブジェクトを取得しておく - 参照元の
[Bookmark].Range.Text
を書き換える Document.Bookmarks.Add
メソッドを、参照元のブックマーク名、1.で取得したRange
オブジェクトを渡して実行する
これでうまくいく、と思った。
しかし、これではうまくいかない。
2.で参照元の[Bookmark].Range.Text
を書き換えた時点で、1.で取得したRange
オブジェクトが潰れてしまっているのだ。
たとえば、「○行目」の「○」の部分(1文字選択の状態)だったはずのRange
オブジェクトが、2.で「48行目」というふうに書き換えたとすると、「48」の手前のカンチャン(0文字選択の状態)になってしまうのだ。
これではまずい。
そこで、次のような手順を踏むことにした。
- 参照元の
[Range].Text
を書き換える前に、参照元のRange
オブジェクトを取得しておく - 参照元の
[Bookmark].Range.Text
を書き換える - 1.で取得した
Range
オブジェクトについてSelect
メソッドを実行する。(書き換えた行番号の手前にカーソルが移動する。) Selection.MoveRight
メソッドを用いて、行番号の文字数分だけカーソルを右に動かす。(右にドラッグする。)Document.Bookmarks.Add
メソッドを、参照元のブックマーク名、4.で取得したSelection.Range
オブジェクトを渡して実行する
こんなふうにした。
参照先の行番号に応じて参照元を書き換えるメソッド
上記の考えに基づいて作成したメソッドがコチラ。
リスト1
Public Sub refreshLineNumberReference( _ ByVal TargetDocument As Document, _ ByVal ReferrerName As String, _ ByVal ReferenceName As String) '### 参照先ブックマークのある行番号を取得して、参照元 ###' '### ブックマークの箇所の行番号を書き換える ###' '///ReferrerName :参照元ブックマーク名' '///ReferenceName :参照先ブックマーク名' Dim Doc As Document Set Doc = TargetDocument '参照先、参照元ブックマークが存在しなかったらExit' If Not bookmarkExists(Doc, ReferenceName) Then Exit Sub If Not bookmarkExists(Doc, ReferrerName) Then Exit Sub 'メインの処理' '参照元ブックマークを取得' Dim bmFrom As Bookmark Set bmFrom = Doc.Bookmarks(ReferrerName) '参照先の行番号を取得' Dim lineNum As Long lineNum = getLineNumber(Doc.Bookmarks(ReferenceName).Range) '参照元のRangeオブジェクトを取得' Dim tgtRange As Range Set tgtRange = bmFrom.Range '参照元の行番号を書き換える' Dim tmp As String tmp = CStr(lineNum) bmFrom.Range.Text = tmp '参照元ブックマークが消滅しているので、復元する' 'Rangeオブジェクトが潰されてしまっているので、行番号を表す' '文字数分右に広げてRagneオブジェクトを取得し直す' Call tgtRange.Select Call Selection.MoveRight(wdCharacter, Len(tmp), wdExtend) Set tgtRange = Selection.Range '再度ブックマークを設定する' Call Doc.Bookmarks.Add(ReferrerName, tgtRange) End Sub Private Function bookmarkExists( _ ByVal tgtDoc As Document, _ ByVal tgtName As String) As Boolean '///ブックマーク名の存否を確認' bookmarkExists = True Dim i As Long For i = 1 To tgtDoc.Bookmarks.Count If tgtDoc.Bookmarks(i).Name = tgtName Then Exit Function End If Next bookmarkExists = False End Function Public Function getLineNumber( _ ByVal tgtRange As Range) As Long Dim ret As Long '///tgtRangeのあるページ番号を取得' Dim currPage As Long currPage = tgtRange.Information(wdActiveEndPageNumber) 'tgtRangeのあるページ内での行番号を取得' Dim currLine As Long currLine = tgtRange.Information(wdFirstCharacterLineNumber) 'tgtRangeが1ページ目にあるときは、その行番号を返す' If currPage = 1 Then ret = currLine GoTo Finalizer: End If '2ページ以上ある時は、手前のページまでの累計を足さなければいけない' Dim Doc As Document Set Doc = tgtRange.Parent 'カーソル位置を記録' Dim orgRange As Range Set orgRange = Selection.Range '文書の先頭にカーソルを置く' Call Doc.Range(0, 0).Select '1ページ目の最終位置を取得' Dim pageEnd As Long '1ページ目の最終位置を選択' Dim i As Long For i = 1 To currPage - 1 pageEnd = Doc.Bookmarks("\Page").End Call Doc.Range(pageEnd - 1, pageEnd - 1).Select ret = ret + Selection.Range.Information(wdFirstCharacterLineNumber) '次のページの先頭へ' Call Selection.MoveRight(wdCharacter, 1, wdMove) Next ret = ret + currLine 'カーソル位置を戻す' Call orgRange.Select Finalizer: getLineNumber = ret End Function
Range
オブジェクトのある通し行番号を返すメソッドのコードを再掲したので、異様にタテ長になってしまったが、気にしないでくだされ。
今回もかなり細かくコメントを入れたので、説明は省略。
使ってみる
このように、参照先と参照元にそれぞれ「参照先01」、「参照元01」という名前のブックマークを設定し、次のコードで使ってみる。
リスト2
Private Sub test00() Dim Doc As Document Set Doc = Application.ActiveDocument Call LineNumUtil.refreshLineNumberReference( _ Doc, _ "参照元01", _ "参照先01") End Sub
ほれ。このように、ちゃんと参照先の行番号に置き換わっておる。
さらに、参照先をテキトーに動かしてから実行しても、ちゃんと参照先の行番号に置き換わっておる。
おわりに
やはりWordのRange
オブジェクトは癖が強い。まだまだ理解が足りないな……。
もっとわかってきたら、洗練されてくると思う。