指定フォルダ内の画像をサムネイル表示

解決


おも  2007-10-26 01:34:26  No: 137940

はじめまして、VB経験3週間のおもと申します。

VB2005でフォルダ内のJPG画像をサムネイル表示(120x120pixel程度を約100枚ぐらい)できるプログラムを作りたいのですが、最初から躓いておりまして、アドバイスいただきたく思います。

まず、縮小画像生成はこちらのページのFromStreamとGetThumbnailImageを利用したサンプルを参考にしたいと思いました。

http://www.atmarkit.co.jp/fdotnet/dotnettips/606fastthumbnail/fastthumbnail.html

サンプルを動かすことは出来たのですが、取得した縮小画像をPictureBoxに渡す方法がわかりませんでした。そこでGetThumnailImageで検索してたらこちらを見つけました。

http://dobon.net/vb/dotnet/graphics/thumbnail.html

このページの2番目「Image.GetThumbnailImageメソッドを使用する」がヒントになりそうだったのですが、最初のサンプルと組み合わせるどころか、これだけを試してみてもフォーム上のPictureBoxにはなにも表示されません。とりあえず1枚でも画像が表示できれば進める気がするので、なにが問題なのかアドバイス頂きたく思います。

それともうひとつ、縮小画像が取得したらPictureBoxをフォーム上に画像の数だけ配置してそれぞれに画像を表示して行きたいと思います。GetThamnailImageメソッドが使えないので、今は一時的にLoadメソッドで進めようと思っていましたが、ここでまた躓いてしまいました。VBでコントロールのインスタンスをループ処理で複数作っていく方法がわかりません。コレクションを使うと聞いたのですが、どのように記述していけばよいでしょうか。具体的にはPictureBoxとLabelをセットにして、1つのコントロールのようにして、各画像の下にタイトルなどを表示させるようにしたいです。

VB経験は参考書のサンプルプログラムを作った程度ですが、FlashのActionscriptでは似たようなプログラムを作ったことはあります。質問内容が回りくどくてすみませんが、何卒アドバイスよろしくお願いいたします。


魔界の仮面弁士  2007-10-26 02:01:37  No: 137941

> フォーム上に画像の数だけ配置してそれぞれに画像を表示して行きたいと思います。
コントロールを増やすのではなく、直接『描画』することをお奨めします。


おも  2007-10-26 06:05:53  No: 137942

>魔界の仮面弁士さん

早速のアドバイスありがとうございます。CPUパワーもメモリもたくさん使いそ  うだと思っていたので、PictureBoxを増やしていくのは不安に思っていたところでした。さっそく描画について調べて、見た目は希望通りに表示することが出来ました。ループで画像を描画していき、右端に行ったら一段下がってまた繰り返すという感じにしました。
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
Private Sub PictureBox1_Paint(ByVal sender As Object, ByVal e .......

  Dim field_w As Integer = PictureBox1.Width
  Dim x As Integer = 20 最初の画像のleft位置
  Dim y As Integer = 20 最初の画像のtop位置
  Dim w As Integer = 120 画像の幅
  Dim h As Integer = 170 画像の高さ

  For i As Integer = 0 To imgCount
     imgFile = imgArray(i)
     Dim _bmp As New Bitmap(imgFile)
     e.Graphics.DrawImage(_bmp, x, y, w, h)
     x += w.width + space_x
     If x + w >= field_w AndAlso i < imgCount Then
          x = 20
          y += h + space_y
     End If
  Next i

End Sub
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
ここまではできましたが、さらに疑問がでてきました。

1.画像一枚一枚にクリックイベントをつけたいのですが、どのような方法があるでしょうか。

2.それと、やはりサンプルの方法を使ってGetThumbnailImage()で縮小画像を取得したいのですが、こうやって得た画像はどうすればBMPとして描画することができるでしょうか。

‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
Imports System
Imports System.IO
Imports System.Drawing
Imports System.Diagnostics

Class CreateThumbnailTest

  Shared Function dummy() As Boolean
    Return False
  End Function

  Shared Sub Main()

    Dim dir As String = "C:\jpgs" ' 画像のあるディレクトリ
    Dim jpgFiles As String() = Directory.GetFiles(dir, "*.jpg")

    Dim sw As Stopwatch = Stopwatch.StartNew()

    For Each jpg As String In jpgFiles
      Console.WriteLine(jpg)

      ' 画像オブジェクトの作成
      Dim orig As Image = Image.FromFile(jpg)

      ' サムネイルの作成
      Dim thumbnail As Image = orig.GetThumbnailImage(160, 120, _
        New Image.GetThumbnailImageAbort(AddressOf dummy), _
        IntPtr.Zero)

      ' サムネイルの保存
      thumbnail.Save("tn_" + Path.GetFileName(jpg), _
        System.Drawing.Imaging.ImageFormat.Jpeg)

      orig.Dispose()
      thumbnail.Dispose()
    Next
    sw.Stop()
    Console.WriteLine(sw.ElapsedMilliseconds & "ミリ秒")

  End Sub
End Class
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
重ね重ねすみませんが、魔界の仮面弁士さん、
みなさん、アドバイスください<(_ _)>


魔界の仮面弁士  2007-10-26 06:21:58  No: 137943

> 画像一枚一枚にクリックイベントをつけたいのですが、どのような方法があるでしょうか。
MouseDown/MouseUp の際に、座標を調べましょう。

> こうやって得た画像はどうすればBMPとして描画することができるでしょうか。
描画先となる Graphics クラスの「DrawImage メソッド」を使いましょう。

> ' サムネイルの保存
「表示」だけが目的であれば、ファイルとして保存する必要はありません。
作成された Image を、そのまま描画すれば OK です。


おも  2007-10-27 05:10:36  No: 137944

>魔界の仮面弁士さま
どうもありがとうございます。
おっしゃるとおりの方法で一旦は描画できました。ただここでもう1つトラブルがありまして、恐れ入りますが最後にまたアドバイス頂きたく思います。

PictureBoxのPaintイベントハンドラに描画処理を記述しているんですが、Windowリサイズ時など、あまりに最描写されすぎて重くなっている気がします。そこでまずリサイズイベントから呼び出してみたところうまく行きませんでした。本来は100枚程度ループしたり位置指定したりしているので複雑ですが、簡略化して記述すると下記のような感じです(これでも結果は同じでした)。

‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
’PictureBoxのPaintイベントに記述
’これだと一応は表示されます。しかしリサイズ時のイベント発生が多すぎて負荷が大きいです。
Private Sub PictureBox_Paint(ByVal sender As Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) Handles picTN.Paint

     e.Graphics.DrawImage(cbmp(1), 0, 0, 120, 170)

End Sub

‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
’今度はResizeイベントハンドラに記述
’なぜか、プログラムをロードしてすぐに例外が発生してしまいます。
Private Sub PictureBox_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles picTN.Resize
    e.Graphics.DrawImage(cbmp(1), 0, 0, 120, 170)
End Sub

’このように別にメソッドを作ってもやはりリサイズイベントに記述すると例外が発生します。
Private Sub drowThumbs()
     Dim g As Graphics = picTN.CreateGraphics
     g.DrawImage(cbmp(1), 0, 0, 120, 170)
End Sub

Private Sub PictureBox_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles picTN.Resize
     drowThumbs()
End Sub

‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾

’今度は試しにクリックイベントに記述してみましたが、なぜかこれだとうまく表示されました。
’もちろんクリックイベントでの再描画は実用的じゃないですが・・・。

Private Sub drowThumbs()
     Dim g As Graphics = picTN.CreateGraphics
     g.DrawImage(cbmp(1), 0, 0, 120, 170)
End Sub

Private Sub PictureBox_MouseClick(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) Handles picTN.MouseClick
      drowThumbs()
End Sub

‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾

長々と書いてしまいましたが、
1.リサイズイベントでエラーになるのはどうしてでしょうか。
2.Paintイベントで再描画する場合、もう少し無駄なく描画する方法はあるでしょうか。
この2点、ご意見頂きたく思います。
スクロールバーも使っているのでPaintイベントが必要な気もしますが、素人考えではResizeイベントと他のイベントを組み合わせたほうがスマートかなと考えています。恐れ入りますが、何卒よろしくお願いいたします<(_ _)>


おも  2007-10-27 06:45:08  No: 137945

追記です。
リサイズイベントでエラーになる原因がわかりました。リサイズイベントはプログラム起動後すぐに実行されるので、表示する画像の準備ができていない為でした。フラグを立てて画像が準備されるまで待たせることでエラーは回避できました。

ただ、エラーは出なくても、ウィンドウサイズをマウスでリサイズしている時のみチラチラと描画されるのみで、マウスを離すと全部消えてしまいます。クリックイベントで再描画してもそのようなことはありません。

やはりPaintインベントで再描画したほうがいいんでしょうか・・・。


魔界の仮面弁士  2007-10-27 07:38:26  No: 137946

> あまりに最描写されすぎて重くなっている気がします。
そのあたりは、描画内容や描画手順にもよりそうなので、何とも。

ただ、120x120 を 100 枚という事なので、例えば縦横 10 枚として、
せいぜい 1200x1200 ドットで表現できるので……、それならば
大き目の「1600x1200のデスクトップ」の『壁紙表示』程度の負荷率かも。

> 本来は100枚程度ループしたり位置指定したりしているので
それらの処理は、Paint イベントのたびに行う必要は無いですよね。
「位置指定を行うタイミング」と「再描画を行うタイミング」は別物ですし。

> Private Sub drowThumbs()
>    Dim g As Graphics = picTN.CreateGraphics
>    g.DrawImage(cbmp(1), 0, 0, 120, 170)
> End Sub
これはマズイです。
「自分で Create した Graphics」は、使用後に Dispose する必要があります。


おも  2007-10-27 08:45:30  No: 137947

早速のご返信ありがとうございます<(_ _)>
やはりもともとの画像が多いですね。ほかにも未熟なコーディングで重くなっているところが多そうですが、スムーズに描写するソフトもよく見かけるので、なんとかがんばりたいです。

さて、Graphics.Disposeすると描画の命令がエラーになってしまいます。スクロールやリサイズなどで再描画するからだと思うのですが、Disposeは、サムネイル画面を閉じたりする節目に行うのではいけないでしょうか?

依然、Resizeイベントに描画を記述するとResize中だけ画像が点滅し、Resizeし終えると全て消えてしまいます。Resizeイベントというのはコツがいるのでしょうか。スクロールイベントに至っては、Debug.writeline("スクロール中")と記述してテストしてみたのですが反映されず、イベント自体無視されているような気もして途方に暮れております<(_ _)>


魔界の仮面弁士  2007-10-27 18:50:15  No: 137948

全体のコードが見えないので、具体的な修正案を出しにくいです。

画像の変化が少ない場合には、Paint イベントで毎回描画するのではなく、
BackgroundImage プロパティに、Bitmap を固定的に割り当てるという手も
あるのですが、今回のアプリでは使えないでしょうか?

> さて、Graphics.Disposeすると描画の命令がエラーになってしまいます。
第三者にも状況が伝わるよう、せめてエラーの内容ぐらいは併記しましょう。(^^;

で。どの Graphics を、いつ Dispose していますか?
当然ながら、描画メソッドを利用する前に Dispose してはいけません。

また、「自分で生成(CreateGraphics 等)したもの」以外の Graphics オブジェクト
——たとえば、Paint イベント引数で得られた Graphics など——も Dispose しません。

> Resizeイベントに描画を記述すると
そこは、「画面」に描画すべきタイミングとしては、あまり相応しくないかと。

たとえば高速にサイズ変更が行われた場合、フォームサイズが 1 変化するたびに
再描画していたのでは処理が追いつきません。Resize では描画位置の再計算等を行う
(必要であれば Invalidate メソッドを呼ぶ)程度にとどめ、実際の描画処理は
Paint イベントに任せるようにした方が良いかと思います。

それでも処理速度が不足するようであれば、

  SetStyle メソッドでダブルバッファリングを有効にして、描画のちらつきを抑える。

  リサイズ中やスクロール中に、画像品質を一時的に落とす。たとえば、
  SmoothingMode プロパティ(HightSpeed / AntiAlias) や、
  TextRenderingHint プロパティ(SinglebitPerPixel / AntiAliasGridFit)を使う。

  スクロール中はサムネイル画像の名前のみを描画し、画像描画は後回しにする。

  フォームのリサイズに伴って、コントロールの位置/サイズ変更が伴う場合は、
  再レイアウトのタイミングを遅延させるために、下記のような方法を使う。
  http://www.microsoft.com/japan/msdn/thisweek/step7/Improve_performance_Winform/SuspendLayout.aspx

などの手法もあります。

> スクロールイベントに至っては、
スクロール処理は、どのような方法で実装させていますか?
HScrollBar / VScrollBar コントロールの Scroll イベントでしょうか。
それとも、Form の Scroll イベントでしょうか?


魔界の仮面弁士  2007-10-27 21:51:17  No: 137949

> Private Sub PictureBox_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles picTN.Resize
>     e.Graphics.DrawImage(cbmp(1), 0, 0, 120, 170)
> End Sub

e.Graphics を使えるのは、描画系イベントだけです。

Paint イベントの引数は PaintEventArgs 型ですが、
Resize イベントの引数は、EventArgs 型であることに注意してください。


おも  2007-10-28 02:01:23  No: 137950

長々とお付き合いいただきまして誠に恐れ入ります<(_ _)>

>どの Graphics を、いつ Dispose していますか?
再描画のことを考えずにサンプルなどからほぼコピペ同然でDpisposeしてしまっていました。サムネイルを閲覧しながら各操作が行える画像管理ソフトが出来上がる予定ですので、プログラム終了までDesposeはするべきではなかったですね。

>スクロール処理は、どのような方法で実装させていますか?
>HScrollBar / VScrollBar コントロールの Scroll イベントでしょうか。
すみません、こちらもケアレスミスでした^^;
コンテナのScrollイベントを使っていたのですが、マウスホイイールイベントも別に用意されているとは気づきませんでした。

いろいろと知識不足で躓いているところが多いみたいです<(_ _)>
とりあえずご紹介していただいた再レイアウトのタイミングを遅延させる方法実装してみたいと思います。
ダブルバッファリングも後々余裕が出てきたら試してみたいです。

‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
Private Sub picBox_Paint(ByVal sender As Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) Handles picBox.Paint

    Dim x As Integer = 20  '最初のX位置
    Dim y As Integer = 20  '最初のY位置
    Dim w As Integer = 113  '画像の幅
    Dim h As Integer = 170  '画像の高さ
    Dim x_space As Integer = 20 '画像の横間隔
    Dim y_space As Integer = 50 '画像の縦間隔

    Dim key As String
    'あるデータテーブルから、画像の枚数を取得(cYYYYMMはキーです)
    Dim imgCount As Integer = csvDT(cYYYYMM).rows.count() - 1
    For i As Integer = 0 To imgCount

         '事前にコレクションcbmpに入れたBMPを位置指定して描画する
         e.Graphics.DrawImage(cbmp(i), x, y, w, h)
         'ラベルの位置を再指定
         clabel(key).Left = x
         clabel(key).Top = y
         '1枚ごとに次の画像のX位置を右にずらす
         x += w + x_space
         '次の座標がPicBoxの右端を超えていたら、
         'Y位置を一段下げ、X位置は初期化する
         If x + w >= PicBox.width AndAlso i < imgCount Then
              x = 20
              y += h + y_space
         End If

    Next i
    '最後にPicBoxの高さを画像の縦羅列分の高さにあわせる
    PicBox.Height = y + h + y_space

End Sub
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
負荷は別として、再描画はほぼこのような感じでは落ち着いております。
しかし、

Dim g As Graphics = picBox.CreateGraphics

と宣言しておき、

e.Graphics.DrawImage(cbmp(i), x, y, w, h)
    ↓
g.DrawImage(cbmp(i), x, y, w, h)

に書き換えると、一瞬再描写されてすぐに消えてしまいます。
他のメソッドに書いて呼び出すようにしたいのですが、このような書き換え方しか知らないため、Paintイベント内に直接記述しております。他のメソッドに書いてうまく呼び出すことは出来ないでしょうか。


魔界の仮面弁士  2007-10-28 04:32:05  No: 137951

> Dim g As Graphics = picBox.CreateGraphics
今回、このメソッドの出番は無い気がします。

> For i As Integer = 0 To imgCount
imgCount って、「イメージの個数」ですよね。であれば、
  For i As Integer = 0 To imgCount - 1
となるのでは。

> 負荷は別として、再描画はほぼこのような感じでは落ち着いております。

Paint のたびに座標計算する必要はない気がするので、たとえば

  Private Sub picBox_Resize(…
    Dim newImage As New Bitmap(picBox.Width, picBox.Height)
    DrawThumbnail(newImage)
    Dim oldImage As Image = picBox.BackgroundImage
    picBox.BackgroundImage = newImage
    If oldImage IsNot Nothing Then
      oldImage.Dispose()
    End If
  End Sub

  Private Sub DrawThumbnail(ByVal img As Image)
    Using g As Graphics = Graphics.FromImage(img)
      '描画処理
    End Using
  End Sub

といった方法は使えませんか? (試していませんけど)


おも  2007-10-28 07:48:35  No: 137952

一枚のBMPとしてPictureBoxのBackgroundImageにしてしまう方法ですね。便利そうで興味があったのですが、教えていただいた記述ではうまくいかず、VBが固まってしまいました^^;
少し思い当たるところをいじってみたのですが、今のスキルではうまく修正できず、今回は諦めたいと思います。おかげさまで、今までに頂いたアドバイスだけでも十分実用的なサムネイル表示が出来るようになりました。

>imgCount って、「イメージの個数」ですよね。であれば、
>  For i As Integer = 0 To imgCount - 1
>となるのでは。

imgCountに数字を代入するときに事前に-1にしました。

はじめてプログラミングの質問しましたので支離滅裂になってしまいましたが、どうもありがとうございました<(_ _)>


魔界の仮面弁士  2007-10-28 08:13:17  No: 137953

> imgCountに数字を代入するときに事前に-1にしました。

それだと、「Count = 個数」という変数名と矛盾していますので、
「上限値」を意味する変数名に変更した方が良いと思いますよ。
たとえば、upperBound とか maxIndex とか。

配列の処理では、+1 や -1 した値が必要になる事も多いので、
後々の混乱を避けるためにも、できるかぎり一貫性のある変数名を
付けておくことをおすすめします。


おも  2007-10-28 10:14:42  No: 137954

アドバイスありがとうございます。
実はいつも複数の配列を使ったり、いくつかループ処理している間に、
混乱して時間をとられてしまうことがあります^^;
参考にさせていただきます。
maxIndexというのもわかりやすいですね。


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加