ExcelのVBAでListView(リストビュー)のコントロールを使っている。
「Listitem(1).Selected = True」みたいなコードでアイテムを選択状態にすることはできるけれど、そのアイテムの位置までスクロールはしてくれない。
どうにかならないのかと思ってネットで探してみるのだけれど、VBAではなかなかその手法が見つからなかった。
というわけで、力技で強引に解決した。
同じようにネットを彷徨っているVBAユーザーがいるかもしれないので共有しておく。
ただし断っておくが、本当に強引な方法である。
「他にもっと良い方法があるのでは?」と思わなくもないけれど、今の私の実力だとこれが限界だ。
Function ScrollListView(iIndex As Integer, Lsv As MSComctlLib.ListView) As Boolean
On Error GoTo lblErrTrap
Dim i As Long, j As Long
With Lsv
If iIndex > .ListItems.Count Or iIndex < 1 Then Exit Function
Call SendMessage(.hWnd, WM_SETREDRAW, False, 0)
Call SendMessage(.hWnd, WM_VSCROLL, SB_TOP, 0)
.ListItems(iIndex).Selected = True
For i = 1 To iIndex - 1
Call SendMessage(.hWnd, WM_VSCROLL, SB_LINEDOWN, 0)
Next
Call SendMessage(.hWnd, WM_SETREDRAW, True, 0)
.Refresh
End With
ScrollListView = True
lblErrTrap:
End Function
という感じだ。
見ての通りWindows APIを使っている。
宣言やウインドウメッセージの定数は省略しているので注意。(宣言は32bitか64bitかで違うし)
使用してるAPIは「SendMessage」だけだ。
ウインドウメッセージの定数についてはこの記事の最後にまとめておく。
方法としてはシンプルだ。
- 最上部へスクロール
- 目的のアイテムの行数分だけ下にスクロール
欠点は行数が増えるほどスクロールの回数が増えるので時間がかかること。
それでも目で見ながらマウスホイールを転がすよりは速いと思う。
それと「WM_SETREDRAW」でリストビューの描写を止めている。
これは行数が多くなると何度も送られてくるSendMessageに耐えられなくなるのか、ユーザーフォームが途中で画面から消えてしまう現象が発生したからだ。
最後に描写を再開して「Refresh」をかけている。
もう少し手を加えればListitems.Countから取得した全体のアイテム数に対して、目的行が下からと上からどちらから近いかを確かめて「SB_TOP」+「SB_LINEDOWN」ではなく「SB_BOTTOM」+「SB_LINEUP」を使うように分岐する、というような手段を使う手もあるだろう。
「SB_PAGEDOWN」などである程度はまとめてスクロールするとか。
本当は「SB_THUMBPOSITION」でピンポイントに一気にスクロールしたかったのだけれど、どうにもうまくできなかった。
VBAでSendMessageを使う際のHIWORDとLOWORDの指定方法がわからなかったのだ。
ビット結合?
うーん……、この辺りがまだまだ未熟。
一応ListViewとして書いているけれど、TreeView(ツリービュー)なんかでも同じ手法が使えるはずだ。
TreeViewとなるとノードの展開状態などによってスクロール量が変わるから、もう少し考えないといけないけれど。
しかし「WM_SETREDRAW」を使った描写停止による高速化は共通して使えそうだから、それだけでも上々だろうか?
使用あるいは関連したウインドウメッセージの定数
Const WM_SETREDRAW = &HB
Const WM_VSCROLL = &H115
Const SB_LINEUP = 0
Const SB_LINEDOWN = 1
Const SB_PAGEUP = 2
Const SB_PAGEDOWN = 3
Const SB_TOP = 6
Const SB_BOTTOM = 7