C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

Re[5]: 画像のサムネイルを縦1列に表示したい


(過去ログ 123 を表示中)

[トピック内 6 記事 (1 - 6 表示)]  << 0 >>

■73637 / inTopicNo.1)  画像のサムネイルを縦1列に表示したい
  
□投稿者/ キム (19回)-(2014/10/21(Tue) 14:49:49)

分類:[C#] 

開発環境: Visual C# 2010 Windows フォーム アプリケーション

目的
特定のフォルダー内にある画像ファイルのサムネイルを縦1列に綺麗に表示したいです。
サムネイルは160×120Pixel程度の固定サイズでその下にファイル名(ベース名のみ)を表示するアイテムが縦1列に並ぶ感じです。
『綺麗に表示』とは、以下の条件を満たすことです。
・左右及びアイテム間が適切な余白間隔(数Pixel程度)で表示される。
・スクロールがスムース(Pixel単位かそれに近い状態)に行われる。
・スクロールバーは垂直スクロールバーのみで、可能であれば常に表示したい。

出来ていること
・特定のフォルダー内にある画像ファイル名一覧の取得
・サムネイル画像の生成
・サムネイルの縦1列表示(不完全)

行き詰っていること
サムネイルの縦1列表示が不完全で綺麗な表示が出来ない。

試したこと
まず、ListViewコントロールを試しました。
LargeIcon表示形式のListViewコントロールを縦長に配置して、LargeImageListプロパティに生成したサムネイルのImageListを設定しました。
これで縦1列に表示出来るのですが、余白がかなり大きくなってしまいます。
特に左右の余白が大きくなってしまうのが問題で、ListViewの幅をかなり大きめ(+70Pixel程度)にしないと水平スクロールバーが表示されてしまいます。
これはWindowsのアイコンの間隔(横)設定で指定された間隔の分までアイテムの幅に加えられてしまってるためと思います。
試しにウィンドウの色とデザイン設定でアイコンの間隔(横)を0にしたところ、水平スクロールバーが表示されることなく適切な余白で綺麗に表示されました。
とは言え、アプリが勝手にこの設定をいじるわけにもいかないので、このままでは使えません。
また、スクロールバー制御もこのままでは出来ません。
(Protectedメンバーでスクロールバーにアクセス出来そうなので派生クラスを作れば何とかなりそうですが、余白で躓いてしまったので試していません)

次に、LixtBoxコントロールを試しました。
DrawModeをOwnerDrawFixedにしてサムネイルとファイル名をオーナードローしました。
(アイテムにはサムネイル画像と文字列をもつ独自クラスのインスタンスを登録しています)
自分で描画しているので、表示は問題なく適切な余白を持って出来ています。
スクロールバーの制御も標準の状態で問題なく出来ています。
が、スクロールが目的の動作と異なります。
ListViewコントロールのスクロールはスクロールバーに連動してPixel単位にスムースに行われますが、LixtBoxコントロールのスクロールはアイテムの高さ単位に行われるようです。
具体的には、スクロールバーをある程度動かすまでまったく変化無く、移動幅があるサイズを越えた瞬間に1アイテムの高さ分だけ一瞬で表示位置が切り替わります。
なので、スクロール操作が非常にわかりにくく、ちらついた感じに見えてしまいます。
OwnerDrawVariableモードも試しましたが同じ結果でした。

ご教示いただきたいこと
縦1列のサムネイルを『綺麗に表示』するにはどうすればよいでしょうか。
使用すべきコントロール、手法などをご教示いただけるとうれしいです。
よろしくお願いします。
引用返信 編集キー/
■73646 / inTopicNo.2)  Re[1]: 画像のサムネイルを縦1列に表示したい
□投稿者/ 魔界の仮面弁士 (151回)-(2014/10/21(Tue) 19:07:56)
No73637 (キム さん) に返信
> 開発環境: Visual C# 2010 Windows フォーム アプリケーション
WPF ならどうだろう、と一瞬思いましたが、それはさておき:

> 使用すべきコントロール、手法などをご教示いただけるとうれしいです。
未検証ですが、System.Windows.Forms.VScrollBar を横に配した
PictureBox に描画してみては如何でしょうか。


PictureBox にスクロールバーを付けて画像を表示するサンプル
http://dobon.net/vb/dotnet/graphics/scrollimage.html

PictureBox + VScrollBar + HScrollBar なアプリのスクロール高速化
http://dobon.net/vb/bbs/log3-39/23844.html
引用返信 編集キー/
■73650 / inTopicNo.3)  Re[2]: 画像のサムネイルを縦1列に表示したい
□投稿者/ キム (20回)-(2014/10/22(Wed) 14:03:16)
No73646 (魔界の仮面弁士 さん) に返信

お返事ありがとうございます。

> 未検証ですが、System.Windows.Forms.VScrollBar を横に配した
> PictureBox に描画してみては如何でしょうか。

なるほど、コンテナ(Panel)にスクロールバーを処理させるのですね。
おもしろいです。
今回の場合はアイテム数に比例してPictureBoxのHeightを大きくしていけばいいですね。
高速化の手法までご紹介いただいてありがとうございます。

そこで、実装に向けて調べたところ、PictureBoxのサイズ限界が気になりました。
仕様として 32767 を超えられないという情報と、メモリ不足で それよりさらに小さい値で
エラーになる(環境依存)という情報がありました。(自分では未確認です)
1フォルダー内のファイル数を制限したとしても限界を超えてしまいそうです。

でも、Panelにスクロールバーを処理させるのはとても有効なアイデアだと思うので、それを応用して自分なりに考えてみました。

1.PanelにVScrollBarコントロールを貼り付ける。
2.画像数に応じてスクロールバーの上限値を設定する。
3.PanelのPaint処理でPanel自体にスクロール位置に応じたアイテム(サムネイル+ファイル名文字列)を描画する。

この方法で実験コードを書いてみようと思います。

そして、もうひとつの方法は、やはりListViewを使うというものです。
・ListViewは仮想化して、アイテムは自分で管理する。
・スクロールバーの問題は、上記のスクロールバーはPanel+VScrollBarで制御
(またはListViewから派生したクラスでスクロールバーの表示有無だけを制御)
・描画の問題はオーナードローすることで何とか解決する(詳細は未調査)。

こちらの方向も検討しようと思います。

引用返信 編集キー/
■73661 / inTopicNo.4)  Re[3]: 画像のサムネイルを縦1列に表示したい
□投稿者/ 魔界の仮面弁士 (155回)-(2014/10/22(Wed) 18:50:22)
No73650 (キム さん) に返信
>> 未検証ですが、System.Windows.Forms.VScrollBar を横に配した
>> PictureBox に描画してみては如何でしょうか。
> なるほど、コンテナ(Panel)にスクロールバーを処理させるのですね。

それは、VScrollBar を使わないパターンですね。紹介した URL の一つ目の方。

VScrollBar.Value の位置に応じて、該当する内容のみを PictureBox 直接描画すれば、
Panel は不要ですしサイズ制限もありません。
二つ目の URL で使われているのはこちらですね。
(紹介した事例では巨大な単一画像の話だったので、コンテンツの管理方法は異なりますが)
引用返信 編集キー/
■73689 / inTopicNo.5)  Re[4]: 画像のサムネイルを縦1列に表示したい
□投稿者/ キム (21回)-(2014/10/23(Thu) 12:31:34)
No73661 (魔界の仮面弁士 さん) に返信

お返事ありがとうございます。

はい、2番目のURLの魔界の仮面弁士さんご自身の書き込みも大変に勉強になりました。
こちらの主題は GDI+ の巨大画面での遅さを API(GDI) を利用することで改善することだと理解していますが、
直接書き込むパターンがとても参考になると思ってその部分を興味深く拝見しました。
が、1番目のURLの記事と混同してしまい、PictureBoxも大きくなると思い込んでしまっていました。
2番目のURLでも途中でそのような流れがあったので勘違いに拍車がかかってしまいました。
ごめんなさい。

自分で直接書き込む場合はPictureBoxでもPanelでも同じことだと思うので、2番目のURLの魔界の仮面弁士さん
の書き込みがとても参考になると思います。
ありがとうございます。

あとは、サムネイルを選択したときの処理(ListBoxnoのように背景や文字列をハイライト表示したり、選択を検出
したりする処理)ですが、アイテム高さが固定なのでスクロールバー位置とマウスの位置で簡単に処理できると思い
ます。

引用返信 編集キー/
■73720 / inTopicNo.6)  Re[5]: 画像のサムネイルを縦1列に表示したい
□投稿者/ キム (22回)-(2014/10/24(Fri) 19:40:19)
おかげさまでサムネイル表示が完成しました。
希望通りの動作で、ちらつきも無く速度も速いです。
ありがとうございます。

アドバイスを参考にPictureBox+VScrollBarで実装しました。
マウスでの選択も実装できました。
キー操作には対応していませんが、不要と言われているので、どこかで時間をとってコツコツ実装しようと思います。
本件はこれにて解決とします。
ありがとうございます。

まとめとして作成した実験コードを乗せておきます。
※例外処理は省いてあります。

実験用フォームの構成は下記の通りです。
1.フォームにPanelを配置して、BorderStyleをFixedSingleにする。
2.Panel上にVScrollBarを配置して、DockをRightにする。
3.Panel上にPictureBoxを配置して、DockをFill、BackColorをWindowにする。


まず、下記のような画像情報を管理するクラスを用意して、List<T>にサムネイルを収集します。

public class ImageInfo
{
    public string FilePath  { get; private set; }
    public Image Thumbnail  { get; private set; }
    public string Text      { get; private set; }

    public ImageInfo(string path, int thumbWidth, int thumbHeight)
    {
        FilePath = Path.GetFullPath(path);  // 画像ファイルのパスを保持

        using (FileStream fs = File.OpenRead(path))
        using (Image image = Image.FromStream(fs, false, false))
        {
            // 指定されたサイズのサムネイルと説明(ファイルベース名)を保持
            Thumbnail = image.GetThumbnailImage(thumbWidth, thumbHeight, null, IntPtr.Zero);
            Text = Path.GetFileName(path);
        }
    }
}

実験用フォームクラス内のサムネイル読み込み部分です。
今回は実験なので非同期処理はせずに起動時に呼び出して一気に読み込ませました。

const string TargetFolder = @"D:\Test\Jpegs";
const int ThumbWidth = 160;
const int ThumbHeight = 120;
const int ItemHeight = ThumbHeight + 24;
const int TopMargin = 4;

List<ImageInfo> thumbList_ = new List<ImageInfo>();
int selectedIndex_ = -1;

private void LoadThumbnail()
{
    string[] jpgFiles = Directory.GetFiles(TargetFolder, "*.jpg");

    foreach (string path in jpgFiles)
    {
        thumbList_.Add(new ImageInfo(path, ThumbWidth, ThumbHeight));
        if (!vScrollBar1.Enabled)
        {
            pictureBox1.Invalidate();
        }

        if (ItemHeight * thumbList_.Count <= pictureBox1.Height)
        {
            vScrollBar1.Value = 0;
            vScrollBar1.Enabled = false;
        }
        else
        {
            int largeChange = pictureBox1.Height;
            vScrollBar1.Maximum = ItemHeight * thumbList_.Count - pictureBox1.Height + largeChange;
            vScrollBar1.LargeChange = largeChange;
            vScrollBar1.SmallChange = largeChange / 4;
            vScrollBar1.Enabled = true;
        }
    }
}


実験用フォームクラス内のスクロールバースクロールイベントです。
Paintイベントが呼ばれるようにpictureBoxの表示全体を無効化するだけです。

private void vScrollBar1_Scroll(object sender, ScrollEventArgs e)
{
    pictureBox1.Invalidate();
}


実験用フォームクラス内のPictureBox描画イベントです。

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
    if (thumbList_.Count > 0)
    {
        int itemIndex = vScrollBar1.Value / ItemHeight;

        int x = (pictureBox1.ClientSize.Width - ThumbWidth) / 2;
        for (int y = -(vScrollBar1.Value % ItemHeight); y < pictureBox1.Height && itemIndex < thumbList_.Count; y += ItemHeight)
        {
            Brush br = (itemIndex == selectedIndex_) ? SystemBrushes.Highlight : SystemBrushes.Window;
            e.Graphics.FillRectangle(br, 0, y, pictureBox1.ClientSize.Width, ItemHeight);

            e.Graphics.DrawImage(thumbList_[itemIndex].Thumbnail, x, y + TopMargin);

            int textTop = y + TopMargin + ThumbHeight;
            Rectangle rect = new Rectangle(0, textTop, pictureBox1.ClientSize.Width, y + ItemHeight - textTop);
            Color color = (itemIndex == selectedIndex_) ? SystemColors.HighlightText : SystemColors.WindowText;
            TextRenderer.DrawText(e.Graphics, thumbList_[itemIndex].Text, this.Font, rect, color, TextFormatFlags.HorizontalCenter);

            ++itemIndex;
        }
    }
}


実験用フォームクラス内のPictureBoxマウスダウンイベントです。
アイテムを選択します。

private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
    int itemIndex = (e.Location.Y + vScrollBar1.Value) / ItemHeight;
    if (itemIndex < thumbList_.Count)
    {
        selectedIndex_ = itemIndex;
        pictureBox1.Invalidate();
    }
}

解決済み
引用返信 編集キー/


トピック内ページ移動 / << 0 >>

このトピックに書きこむ

過去ログには書き込み不可

管理者用

- Child Tree -