VB.NET ListViewのアイコンをホバーでPictureBoxに拡大プレビュー表示する方法【WinForms】

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典型的な用途
LargeIconLargeImageList大きめのサムネイル一覧(アイコンビュー)
SmallIconSmallImageList小さめアイコンの一覧
DetailsSmallImageListエクスプローラー風の「詳細」表示
ListSmallImageList縦方向に並べるリスト表示
TileLargeImageList(カスタム次第)タイル表示(テキスト情報も多め)

この記事のプレビューコードでは、この表にしたがって「どの ImageList から画像を取るか」を View に応じて切り替えています。

画質をきれいに保つための工夫

ImageList に設定されている画像は、通常 16×16 や 32×32 などの小さいサイズです。そのため、PictureBox にそのまま拡大表示すると、ジャギーが目立ったり、ぼやけた印象になってしまいます。

きれいに拡大したい場合の現実的な選択肢は次の 2 つです。

  • 元の PNG(高解像度)をどこかにキャッシュしておく
  • プレビュー専用の大きな ImageList を用意しておく

ここでは扱いやすい「元 PNG を ListViewItem.Tag にキャッシュする」方法を例として紹介します。

読み込み時に元 PNG を Tag に保持する

フォルダーから PNG を読み込む処理で、次のようにします。

  1. ファイルから Image を読み込む(元画像)
  2. ListView 用に縮小したサムネイルを別途作成し、ImageList に追加
  3. 元画像を 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 = Details or LargeIcon、チェックボックス付きなど好きな設定
  • picPreview … プレビュー用 PictureBox
  • btnBrowse … フォルダー選択ボタン
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 だけで十分実用的な画像ビューアを構築できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次