VB.NET/WinForms で「フォルダー内の PNG を ListView に一覧表示し、マウスを乗せたアイコンだけ PictureBox に大きくプレビューしたい」という要件は、ツール系アプリでとてもよく登場します。本記事では、並べ替え(Sort)後もズレず、コード量も少ない実装方法をサンプル付きで丁寧に解説します。
ListView のアイコンをホバーで PictureBox に拡大表示したい
まずは実現したい挙動を整理します。ここでは、ユーザーが選んだフォルダー内の PNG ファイルを ListView に「チェックボックス+アイコン+ファイル名」で並べて表示し、マウスカーソルがアイコン上に乗ったときに、フォーム上の PictureBox(例:picPreview)にその画像を大きく表示する、という想定です。
| タイミング | ListView の状態 | PictureBox の動作 |
|---|---|---|
| 通常時 | PNG のサムネイルをアイコン表示 | 何も表示しない or 直前の画像を保持 |
| アイコン上にホバー | マウス位置のアイテムが Hot 状態 | そのアイコンを拡大してプレビュー表示 |
| マウスが ListView から外れた | どのアイテムにも乗っていない | プレビューをクリアするか、そのまま残す |
いちばん大事なのは、並べ替えをしても画像とアイテムの対応がズレないことと、マウス操作に対して違和感なくプレビューが切り替わることです。
ありがちな実装と問題点
この手の機能を実装するとき、ありがちなアプローチを整理しておきます。
| 実装パターン | 概要 | 主な問題点 |
|---|---|---|
MouseMove + GetItemAt | マウス移動ごとに ListView.GetItemAt(e.X, e.Y) でアイテムを特定し、その ImageIndex を使って ImageList から画像を取得する | ・マウスが少し動いただけでもイベントが大量に発火しやすく、ちらつきやすい ・ Item.Index や独自のインデックスを使うと、並べ替え後にズレやすい・コード量が多くなりがち |
| 選択変更時のみ更新 | SelectedIndexChanged など、選択状態が変わったときに PictureBox を更新する | ・「ホバーした瞬間」のプレビューにならない ・キーボード操作中心なら有効だが、「マウスを乗せるだけ」の UX には不向き |
ItemMouseHover(推奨) | ListView が用意している「アイテムにマウスが乗った」イベントを利用 | ・特になし。最短・安全に実装できる |
本記事で紹介するのは、最後の ListView.ItemMouseHover イベントを使う方法です。これを使うことで、並べ替えに強く、コードもシンプルな実装が可能になります。
最短の解決策:ItemMouseHover で PictureBox にプレビュー
ItemMouseHover イベントは、「マウスが乗っている ListViewItem」そのものを引数で渡してくれるのが最大のメリットです。つまり、マウス座標から無理にアイテムを逆算する必要がありません。
e.Itemが「今マウスが乗っているアイテム」e.Item.ImageIndexまたはe.Item.ImageKeyで、紐づいている画像を特定- その画像を ListView に設定している
ImageListから取り出して PictureBox に表示
基本的なコードは以下の通りです。
基本コード(そのままコピペ可)
' ListView1 … 画像付きで表示している ListView
' picPreview … プレビュー表示用の PictureBox
Private Sub ListView1_ItemMouseHover(sender As Object, e As ListViewItemMouseHoverEventArgs) _
Handles ListView1.ItemMouseHover
' ListView の表示モードに応じて、参照すべき ImageList を決定
Dim srcList As ImageList =
If(ListView1.View = View.Details OrElse ListView1.View = View.SmallIcon,
ListView1.SmallImageList,
ListView1.LargeImageList)
If srcList Is Nothing Then
picPreview.Image = Nothing
Exit Sub
End If
Dim img As Image = Nothing
' ImageIndex 優先で取得(設定がなければ ImageKey で取得)
If e.Item.ImageIndex >= 0 AndAlso e.Item.ImageIndex < srcList.Images.Count Then
img = srcList.Images(e.Item.ImageIndex)
ElseIf Not String.IsNullOrEmpty(e.Item.ImageKey) AndAlso srcList.Images.ContainsKey(e.Item.ImageKey) Then
img = srcList.Images(e.Item.ImageKey)
End If
' PictureBox 側の表示モードを調整(拡大表示したい場合は Zoom 推奨)
picPreview.SizeMode = PictureBoxSizeMode.Zoom
picPreview.Image = img
End Sub
コードのポイント
- どの ImageList を参照するかを View で切り替え
ListView.ViewがDetailsやSmallIconのときはSmallImageList、それ以外(LargeIconやTile)のときはLargeImageListを参照しています。 - ImageIndex / ImageKey の両対応
ImageIndexが設定されている場合はそれを優先し、設定されていない場合はImageKeyの有無を確認してから取得するようにしています。 - PictureBox の表示モードは Zoom
拡大・縮小してプレビューする用途ではPictureBoxSizeMode.Zoomが扱いやすく、縦横比も保ってくれます。 - 並べ替えしてもズレない
ImageIndexやImageKeyは アイテム自身が保持している情報 のため、ListView のソートによる表示順変更とは無関係です。そのため、表示順を変えてもプレビュー表示がズレません。
ImageList と View の関係を整理する
ListView でアイコン付き表示を行う場合、ImageList をどこに設定しているかを整理しておくと、プレビュー実装が理解しやすくなります。
| View | 通常利用する ImageList | 典型的な用途 |
|---|---|---|
LargeIcon | LargeImageList | 大きめのサムネイル一覧(アイコンビュー) |
SmallIcon | SmallImageList | 小さめアイコンの一覧 |
Details | SmallImageList | エクスプローラー風の「詳細」表示 |
List | SmallImageList | 縦方向に並べるリスト表示 |
Tile | LargeImageList(カスタム次第) | タイル表示(テキスト情報も多め) |
この記事のプレビューコードでは、この表にしたがって「どの ImageList から画像を取るか」を View に応じて切り替えています。
画質をきれいに保つための工夫
ImageList に設定されている画像は、通常 16×16 や 32×32 などの小さいサイズです。そのため、PictureBox にそのまま拡大表示すると、ジャギーが目立ったり、ぼやけた印象になってしまいます。
きれいに拡大したい場合の現実的な選択肢は次の 2 つです。
- 元の PNG(高解像度)をどこかにキャッシュしておく
- プレビュー専用の大きな ImageList を用意しておく
ここでは扱いやすい「元 PNG を ListViewItem.Tag にキャッシュする」方法を例として紹介します。
読み込み時に元 PNG を Tag に保持する
フォルダーから PNG を読み込む処理で、次のようにします。
- ファイルから
Imageを読み込む(元画像) - ListView 用に縮小したサムネイルを別途作成し、ImageList に追加
- 元画像を
ListViewItem.Tagに詰めておく
Imports System.IO
' 例: PNG 読み込み処理の一部
Private Sub LoadPngFiles(folderPath As String)
ListView1.BeginUpdate()
Try
ListView1.Items.Clear()
If ListView1.SmallImageList Is Nothing Then
Dim il As New ImageList()
il.ImageSize = New Size(32, 32)
il.ColorDepth = ColorDepth.Depth32Bit
ListView1.SmallImageList = il
ListView1.LargeImageList = il
End If
Dim imgList = ListView1.SmallImageList
imgList.Images.Clear()
For Each filePath In Directory.EnumerateFiles(folderPath, "*.png")
Dim original As Image = Image.FromFile(filePath) ' 元の PNG
Dim thumb As New Bitmap(original, imgList.ImageSize) ' サムネイルを作成
Dim key As String = Path.GetFileName(filePath)
imgList.Images.Add(key, thumb)
Dim item As New ListViewItem(key)
item.ImageKey = key
item.Tag = original ' プレビュー用に元画像を保持
ListView1.Items.Add(item)
Next
Finally
ListView1.EndUpdate()
End Try
End Sub
このようにしておくと、ItemMouseHover 側で e.Item.Tag から元 PNG を取り出せるので、PictureBox に高解像度のまま表示できます。
ItemMouseHover 側で Tag を優先して使う
先ほどの基本コードを少し拡張し、元画像が Tag に入っている場合はそちらを優先的に表示します。
Private Sub ListView1_ItemMouseHover(sender As Object, e As ListViewItemMouseHoverEventArgs) _
Handles ListView1.ItemMouseHover
Dim srcList As ImageList =
If(ListView1.View = View.Details OrElse ListView1.View = View.SmallIcon,
ListView1.SmallImageList,
ListView1.LargeImageList)
If srcList Is Nothing Then
picPreview.Image = Nothing
Exit Sub
End If
Dim img As Image = Nothing
' Tag に元画像が入っていればそれを優先
img = TryCast(e.Item.Tag, Image)
' Tag が空の場合は ImageList から取得
If img Is Nothing Then
If e.Item.ImageIndex >= 0 AndAlso e.Item.ImageIndex < srcList.Images.Count Then
img = srcList.Images(e.Item.ImageIndex)
ElseIf Not String.IsNullOrEmpty(e.Item.ImageKey) AndAlso srcList.Images.ContainsKey(e.Item.ImageKey) Then
img = srcList.Images(e.Item.ImageKey)
End If
End If
picPreview.SizeMode = PictureBoxSizeMode.Zoom
picPreview.Image = img
End Sub
ListView 上のアイコンは小さくても、プレビューは元の PNG を使うので、クオリティ高く拡大表示できます。
ちらつき防止と無駄な再描画を減らすテクニック
ItemMouseHover は、アイテム上に一定時間マウスが止まったときに発火します。通常はそれほど多くは発火しませんが、実装によっては同じアイテムに何度もマウスが乗る場面もあります。
「すでに表示中のアイテムと同じなら何もしない」というガードを入れておくと、無駄な再描画を抑えられます。
最後にプレビューしたアイテムを覚えておく
' フォームのメンバー変数として保持
Private _lastPreviewItem As ListViewItem
Private Sub ListView1_ItemMouseHover(sender As Object, e As ListViewItemMouseHoverEventArgs) _
Handles ListView1.ItemMouseHover
' 同じアイテムなら何もしない
If _lastPreviewItem Is e.Item Then
Return
End If
_lastPreviewItem = e.Item
Dim srcList As ImageList =
If(ListView1.View = View.Details OrElse ListView1.View = View.SmallIcon,
ListView1.SmallImageList,
ListView1.LargeImageList)
If srcList Is Nothing Then
picPreview.Image = Nothing
Exit Sub
End If
Dim img As Image = TryCast(e.Item.Tag, Image)
If img Is Nothing Then
If e.Item.ImageIndex >= 0 AndAlso e.Item.ImageIndex < srcList.Images.Count Then
img = srcList.Images(e.Item.ImageIndex)
ElseIf Not String.IsNullOrEmpty(e.Item.ImageKey) AndAlso srcList.Images.ContainsKey(e.Item.ImageKey) Then
img = srcList.Images(e.Item.ImageKey)
End If
End If
picPreview.SizeMode = PictureBoxSizeMode.Zoom
picPreview.Image = img
End Sub
Private Sub ListView1_MouseLeave(sender As Object, e As EventArgs) _
Handles ListView1.MouseLeave
' マウスが ListView から出たらプレビューをクリア
picPreview.Image = Nothing
_lastPreviewItem = Nothing
End Sub
このようにしておくと、ユーザーのカーソルが ListView 内で多少揺れても、同じアイテムであれば無駄に PictureBox を描き直すことがなくなります。
全体サンプル:フォルダー内の PNG を一覧+ホバープレビュー
ここまでの内容を踏まえて、フォルダーから PNG を読み込んで ListView に表示し、ホバーで PictureBox に拡大プレビューするまでを一つのサンプルとしてまとめます。
フォームには次のコントロールが配置されている想定です。
ListView1… View =DetailsorLargeIcon、チェックボックス付きなど好きな設定picPreview… プレビュー用 PictureBoxbtnBrowse… フォルダー選択ボタン
Imports System.IO
Public Class Form1
' 最後にプレビューしたアイテム
Private _lastPreviewItem As ListViewItem
Public Sub New()
InitializeComponent()
' ListView の初期設定例
ListView1.FullRowSelect = True
ListView1.HideSelection = False
ListView1.CheckBoxes = True
ListView1.View = View.LargeIcon ' LargeIcon / Details など好みで
' ImageList の準備(ここでは 32x32 のサムネイル)
Dim il As New ImageList()
il.ImageSize = New Size(32, 32)
il.ColorDepth = ColorDepth.Depth32Bit
ListView1.SmallImageList = il
ListView1.LargeImageList = il
End Sub
' フォルダー選択ボタン
Private Sub btnBrowse_Click(sender As Object, e As EventArgs) Handles btnBrowse.Click
Using fbd As New FolderBrowserDialog()
fbd.Description = "PNG ファイルが入っているフォルダーを選択してください。"
If fbd.ShowDialog(Me) = DialogResult.OK Then
LoadPngFiles(fbd.SelectedPath)
End If
End Using
End Sub
' PNG の読み込みと ListView への反映
Private Sub LoadPngFiles(folderPath As String)
' 以前の元画像を解放(Tag に Image を入れている場合)
For Each item As ListViewItem In ListView1.Items
Dim img As Image = TryCast(item.Tag, Image)
If img IsNot Nothing Then
img.Dispose()
End If
Next
ListView1.BeginUpdate()
Try
ListView1.Items.Clear()
Dim imgList = ListView1.SmallImageList
imgList.Images.Clear()
For Each filePath In Directory.EnumerateFiles(folderPath, "*.png")
Dim original As Image = Image.FromFile(filePath) ' 元画像
Dim thumb As New Bitmap(original, imgList.ImageSize) ' サムネイル
Dim key As String = Path.GetFileName(filePath)
imgList.Images.Add(key, thumb)
Dim item As New ListViewItem(key)
item.ImageKey = key
item.Tag = original ' プレビュー用に元画像を保持
ListView1.Items.Add(item)
Next
Finally
ListView1.EndUpdate()
End Try
_lastPreviewItem = Nothing
picPreview.Image = Nothing
End Sub
' アイテム上にマウスが乗ったときにプレビュー更新
Private Sub ListView1_ItemMouseHover(sender As Object, e As ListViewItemMouseHoverEventArgs) _
Handles ListView1.ItemMouseHover
' 同じアイテムなら何もしない
If _lastPreviewItem Is e.Item Then
Return
End If
_lastPreviewItem = e.Item
' View に応じて ImageList を選択
Dim srcList As ImageList =
If(ListView1.View = View.Details OrElse ListView1.View = View.SmallIcon,
ListView1.SmallImageList,
ListView1.LargeImageList)
If srcList Is Nothing Then
picPreview.Image = Nothing
Exit Sub
End If
Dim img As Image = TryCast(e.Item.Tag, Image)
' Tag になければ ImageList から取得
If img Is Nothing Then
If e.Item.ImageIndex >= 0 AndAlso e.Item.ImageIndex < srcList.Images.Count Then
img = srcList.Images(e.Item.ImageIndex)
ElseIf Not String.IsNullOrEmpty(e.Item.ImageKey) AndAlso srcList.Images.ContainsKey(e.Item.ImageKey) Then
img = srcList.Images(e.Item.ImageKey)
End If
End If
picPreview.SizeMode = PictureBoxSizeMode.Zoom
picPreview.Image = img
End Sub
' マウスが ListView から離れたらプレビューをクリア
Private Sub ListView1_MouseLeave(sender As Object, e As EventArgs) Handles ListView1.MouseLeave
picPreview.Image = Nothing
_lastPreviewItem = Nothing
End Sub
End Class
このサンプルをベースに、チェック状態での処理やコンテキストメニューなどを追加していけば、画像ビューアや簡易アセット管理ツールとしても十分使える UI になります。
よくあるハマりポイントと対策
ItemMouseHover が発火しない/反応が遅い
ItemMouseHover は、「一定時間アイテムの上でマウスが止まった」ときに発火します。マウスを素早く通過させるだけではイベントが発生しない場合があります。ユーザビリティ的に「もっと即座に反応させたい」場合は、以下のような工夫が考えられます。
MouseMoveでGetItemAtを使い、一定距離以上マウスが動いたときだけ処理する(自前で簡易スロットリング)Timerを併用し、最後にアイテム上にマウスが来た時刻から一定時間経ったらプレビューする
ただし、シンプルさを優先するなら、まずは ItemMouseHover だけで実装してみるのがおすすめです。
PictureBox の画像がファイルとロックされてしまう
Image.FromFile で読み込んだ画像は、その Image オブジェクトを Dispose() するまで OS 上でファイルがロックされた状態になります。そのため、「PNG を読み込んで ListView に表示 → 別処理で同じファイルを上書き保存したい」といったケースでは注意が必要です。
ファイルロックを避ける代表的な方法は次の通りです。
FromFileで読み込んだら、すぐに別のBitmapにコピーして、元の Image を破棄するFileStreamをUsingで開き、Image.FromStreamで読み込んだあとストリームを閉じる
簡単なツールであればロックを気にしない場合もありますが、運用上問題になるようであれば上記のような対策も検討してください。
メモリ使用量が気になる場合
高解像度の PNG をすべてメモリに保持すると、ファイル数や解像度によってはメモリ使用量が増えてしまいます。
- プレビュー時にのみ元 PNG を読み込み、閉じるときに
Dispose()する - 最近プレビューした数件だけ LRU 的にキャッシュし、しばらく使っていない画像は破棄する
- ImageList 用サムネイルだけを保持し、元 PNG のキャッシュはしない(画質よりメモリ優先)
用途に応じて「画質」「メモリ」「実装コスト」のバランスを取るとよいでしょう。
応用アイデア:プレビュー UI をもう一歩リッチにする
この記事の仕組みをベースに、少し手を加えるだけで使い勝手の良いビューアに発展させることができます。
- プレビューにファイル情報を表示
PictureBox の隣にラベルを配置し、ListViewItem.Textやファイルサイズ、更新日時などを表示することで、簡易アセットブラウザーのような UI になります。 - ダブルクリックでフルスクリーンプレビュー
ListView.DoubleClickイベントで、別フォームをフルスクリーン表示し、大きな PictureBox で画像を表示する。 - キーボードでもプレビュー更新
マウスを使わず、矢印キーで選択を移動したときにもプレビューを切り替えたい場合は、ItemSelectionChangedやSelectedIndexChangedで同様のロジックを呼ぶと統一感が出ます。 - ホバー中のアイテムを強調表示
OwnerDraw やHotTrackingを併用し、ホバー中のアイテム背景色を変えると、どのアイコンが対象なのか直感的に分かりやすくなります。
まとめ
- 「ListView のアイコンにマウスを乗せた瞬間に PictureBox に拡大表示したい」ときは、
ItemMouseHoverイベントを使うのが最短で安全な方法です。 e.Item.ImageIndexやImageKeyはアイテム自身が保持しているため、並べ替え(Sort)をしてもプレビュー対象がズレません。- 画質を重視するなら、PNG の元画像を
ListViewItem.Tagや別のコレクションにキャッシュし、プレビュー時にはそちらを使う構成が実務的に有効です。 - 最後にプレビューしたアイテムを記録しておくことで、無駄な再描画やちらつきを抑えられます。
- 本記事のサンプルをベースに、ファイル情報表示やフルスクリーン表示などを追加すれば、VB.NET/WinForms だけで十分実用的な画像ビューアを構築できます。

コメント