none
Listviewへのコントロール追加 RRS feed

  • 質問

  • Visual Studio 2010を使用して簡単なWindowsフォームアプリを開発しています。

    初学者ゆえ試行錯誤中ですが、皆様のお知恵を拝借したく、ひとつ質問させていただきます。

    ListViewコントロールをフォーム内に配置し、ViewプロパティをLargeIconに設定することで

    簡易サムネイルリストを表示できますが、各サムネイルエリア内にテキストボックスやボタンを配置した状態で

    描画することは可能でしょうか。

    イメージ的には、市販のデジカメソフトで印刷枚数を設定する際、各写真サムネイルの下にある

    「+」「-」ボタンを押下して調整するような機能です。

    ちなみに現状は、「ユーザーコントロール」「カスタムコントロール」という単語にたどり着き、

    既存のコントロールを改造する形で実装する…のかな?と思っている状態です。

    よろしくお願いいたします。

    2014年8月14日 10:20

回答

  • Windows Formsでもできないことはないと思いますが、かなり力技が要ります。また、仮想化せずに大量のコントロールを生成すると、GDIオブジェクト数の上限にも引っかかりやすくなります。なのでAzuleanさんの意見同様、WPFを使ったほうが良いと思います。Windowsストアアプリへの応用も効きますし。

    Visual C#のWPFアプリケーションで、「WpfThumbnailViewerTest」というプロジェクトを作成し、下記2つのソースを書き換えます。フォルダー参照ダイアログを補助的に使っているので、参照設定にSystem.Windows.Formsを追加します。

    MainWindow.xaml

    <Window
        x:Class="WpfThumbnailViewerTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        mc:Ignorable="d"
        Title="WPF Thumbnail Viewer Test"
        Width="525"
        Height="350"
        WindowStartupLocation="CenterScreen"
        >
        <Grid Margin="10">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="10"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <DockPanel Grid.Row="0">
                <Button Grid.Row="0" Name="buttonSelectFolder" Content="フォルダー選択" Padding="5"/>
                <TextBox Name="textboxFolderPath" Margin="10,0,0,0" IsReadOnly="True" VerticalContentAlignment="Center"/>
            </DockPanel>

            <ListBox Grid.Row="2" Name="listBox1" AlternationCount="2" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
                <ListBox.ItemContainerStyle>
                    <Style TargetType="ListBoxItem">
                        <!-- アイテムの選択状態を ViewModel 側にも持たせる。 -->
                        <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
                    </Style>
                </ListBox.ItemContainerStyle>
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel/>
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Vertical">
                            <Image Width="100" Height="100" Margin="2" Source="{Binding Path=ThumbnailImage}" ToolTip="{Binding Path=ImageFilePath}"/>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="Auto"/>
                                </Grid.ColumnDefinitions>
                                <Label Grid.Column="0" Content="印刷枚数:" VerticalAlignment="Center"/>
                                <TextBox Grid.Column="1" Width="60" Padding="5,0" Text="{Binding Path=PrintingCount, Mode=TwoWay}" HorizontalContentAlignment="Right" VerticalContentAlignment="Center"/>
                                <StackPanel Grid.Column="2" Orientation="Vertical">
                                    <Button Content="+" Name="buttonPlusPrintingCount" Click="buttonPlusPrintingCount_Click"/>
                                    <Button Content="-" Name="buttonMinusPrintingCount" Click="buttonMinusPrintingCount_Click"/>
                                </StackPanel>
                            </Grid>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Grid>
    </Window>

    MainWindow.xaml.cs

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;

    namespace WpfThumbnailViewerTest
    {
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
            ObservableCollection<DataModels.MyItemInfo> targetItems = new ObservableCollection<DataModels.MyItemInfo>();

            public MainWindow()
            {
                InitializeComponent();

                this.buttonSelectFolder.Click += this.buttonSelectFolder_Click;
                this.listBox1.ItemsSource = this.targetItems;

                // テストコード。
    #if false
                for (int i = 0; i < 10; ++i)
                {
                    this.targetItems.Add(new DataModels.MyItemInfo() { PrintingCount = i });
                }
    #endif
            }

            void buttonSelectFolder_Click(object sender, RoutedEventArgs e)
            {
                // TODO: 参照設定に System.Windows.Forms を追加しておくこと。
                using (var dialog = new System.Windows.Forms.FolderBrowserDialog())
                {
    #if false
                    if (String.IsNullOrEmpty(this.textboxFolderPath.Text))
                    {
                        dialog.RootFolder = Environment.SpecialFolder.MyPictures;
                    }
    #endif
                    dialog.SelectedPath = this.textboxFolderPath.Text;
                    var result = dialog.ShowDialog();
                    if (result == System.Windows.Forms.DialogResult.OK)
                    {
                        this.textboxFolderPath.Text = dialog.SelectedPath;
                        var filePaths = System.IO.Directory.GetFiles(
                            this.textboxFolderPath.Text, "*.jpg", System.IO.SearchOption.TopDirectoryOnly);
                        this.targetItems.Clear();
                        foreach (var filePath in filePaths)
                        {
                            // HACK: ロードできた画像から順次表示していく、非同期読み込みができるとよい。
                            this.targetItems.Add(new DataModels.MyItemInfo() { ImageFilePath = filePath });
                        }
                    }
                }
            }

            // HACK: 説明および簡単のため Button を2つ埋め込み、それぞれイベント ハンドラーを明示的に定義したが、
            // Extended WPF Toolkit のようなキーリピートの効くスピン エディット コントロールを作り、
            // さらにデータ バインディングしたほうがよい。

            private void buttonPlusPrintingCount_Click(object sender, RoutedEventArgs e)
            {
                var button = sender as Button;
                if (button != null)
                {
                    var item = button.DataContext as DataModels.MyItemInfo;
                    if (item != null)
                    {
                        ++item.PrintingCount;
                    }
                }
            }

            private void buttonMinusPrintingCount_Click(object sender, RoutedEventArgs e)
            {
                var button = sender as Button;
                if (button != null)
                {
                    var item = button.DataContext as DataModels.MyItemInfo;
                    if (item != null)
                    {
                        --item.PrintingCount;
                    }
                }
            }
        }

        namespace DataModels
        {
            class MyItemInfo : INotifyPropertyChanged
            {
                public event PropertyChangedEventHandler PropertyChanged;

                protected void NotifyPropertyChanged(string propertyName)
                {
                    if (this.PropertyChanged != null)
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                    }
                }

                public MyItemInfo()
                {
                }

                public const int MinPrintingCount = 0;
                public const int MaxPrintingCount = 10;

                bool isSelected = false;
                int printingCount = 0;
                string imageFilePath = null;
                BitmapSource thumbnailImage = null;

                public bool IsSelected
                {
                    get { return this.isSelected; }
                    set
                    {
                        if (!Equals(this.isSelected, value))
                        {
                            this.isSelected = value;
                            this.NotifyPropertyChanged("IsSelected");
                        }
                    }
                }

                public int PrintingCount
                {
                    get { return this.printingCount; }
                    set
                    {
                        int newValue = Math.Min(Math.Max(value, MinPrintingCount), MaxPrintingCount);
                        if (!Equals(this.printingCount, newValue))
                        {
                            this.printingCount = newValue;
                            this.NotifyPropertyChanged("PrintingCount");
                        }
                    }
                }

                public string ImageFilePath
                {
                    get { return this.imageFilePath; }
                    set
                    {
                        if (!Equals(this.imageFilePath, value))
                        {
                            this.imageFilePath = value;
                            this.thumbnailImage = CreateBitmapImageFromFile(this.imageFilePath, 100);
                            this.NotifyPropertyChanged("ImageFilePath");
                            this.NotifyPropertyChanged("ThumbnailImage");
                        }
                    }
                }

                public BitmapSource ThumbnailImage
                {
                    get { return this.thumbnailImage; }
                }

                private static BitmapSource CreateBitmapImageFromFile(string imageFilePath, int thumbnailWidth)
                {
                    try
                    {
                        // HACK: URI を使って構築する方法だと対象ファイルがロックされてしまうはず。
                        // しかし FileStream と WriteableBitmap を使う方法だとサムネイルの自動作成ができない。
                        var bitmapImage = new BitmapImage();
                        bitmapImage.BeginInit();
                        bitmapImage.UriSource = new Uri(imageFilePath);
                        bitmapImage.DecodePixelWidth = thumbnailWidth;
                        bitmapImage.EndInit();
                        return bitmapImage;
                    }
                    catch (Exception err)
                    {
                        Debug.WriteLine(err.Message);
                        return null;
                    }
                }
            }
        }
    }

    Visual Studio 2013(.NET 4.5)とWindows 8.1環境でしかテストしてませんが、たぶんVS 2010でもいけるんじゃないでしょうか。

    ちなみにMyItemInfo.NotifyPropertyChanged()メソッドの実装は簡単のためプロパティ名の文字列リテラルを直接指定する方式にしましたが、System.Linq.Expressionsによるラムダ式木からのプロパティ名取得やC# 5.0のCaller Infoが使えれば、もっとメンテナンス性を上げることはできます。

    • 編集済み sygh 2014年8月15日 3:13
    • 回答としてマーク FG10 2014年8月20日 23:21
    2014年8月14日 12:50

すべての返信

  • ListView コントロールにそういった機能はありませんので「(普通には)できません」。

    近いことをやろうとすると、テキストボックスやボタンを絵として描くことになるんじゃないでしょうか。
    ただ、細かい挙動にこだわることが難しくなってくるので、ListView を使うこと自体をあきらめて、ListView っぽいコントロールを1から自作することになるんじゃないでしょうか。
    (WPF 方面も検討候補に入れた方がよいかもしれませんね)

    // 「できません」の部分に(普通には)という但し書きを追記。
    2014年8月14日 11:18
    モデレータ
  • ListViewにTextBoxやButtonを重ねることで実現は可能です。
    描画する代わりに編集可能なコントロールを表示するといろいろできます。
    サンプルではアイコンを描画してTextBoxとButtonを重ねていますが、UserControlを重ねることもできます。

    C#

    using System;
    using System.Drawing;
    using System.Windows.Forms;
    
    namespace WindowsFormsApplication1
    {
        public partial class Form1 : Form
        {
            private TextBox inputTextBox;//ListViewに重ねるTextBox
            private Button inputButton; //ListViewに重ねるButton
            private ListView listView1;
    
            public Form1()
            {
                InitializeComponent();
    
                this.listView1 = new ListView();
                this.listView1.Dock = DockStyle.Fill;
                this.listView1.MultiSelect = false;//複数選択の時はとりあえず考慮しない
                this.listView1.OwnerDraw = true;//自分でListViewのアイテムを描画する
                this.listView1.DrawItem += this.listView1_DrawItem;//自分でListViewのアイテムを描画する必要があるときに呼ばれるイベントに登録
                
                this.listView1.Items.Clear();
                this.listView1.SelectedIndexChanged += new EventHandler(listView1_SelectedIndexChanged);
                this.listView1.LargeImageList = this.imageList1;//イメージが3個以上登録されているImageListを用意してください。
                //this.listView1.LargeImageList.ImageSize = new Size(64, 64);
                this.Controls.Add(this.listView1);
    
                this.inputTextBox = new TextBox();
                this.inputTextBox.Visible = false;
                this.inputTextBox.TextChanged += new EventHandler(textbox_TextChanged);
                this.listView1.Controls.Add(inputTextBox);
    
                this.inputButton = new Button();
                this.inputButton.Visible = false;
                this.inputButton.Click += new EventHandler(button_Click);
                this.listView1.Controls.Add(inputButton);
    
                this.listView1.Items.Add(new ListViewItem("TEST1", 0));
                this.listView1.Items.Add(new ListViewItem("TEST2", 1));
                this.listView1.Items.Add(new ListViewItem("TEST3", 2));
            }
    
            void textbox_TextChanged(object sender, EventArgs e)
            {
                TextBox txb = (TextBox)sender;
                var item = txb.Tag as ListViewItem;
                if (item != null && item.Text != txb.Text)
                {
                    item.Text = txb.Text;
                }
            }
    
            void button_Click(object sender, EventArgs e)
            {
                Button btn = (Button)sender;
                var item = btn.Tag as ListViewItem;
                if (item != null)
                {
                    MessageBox.Show(item.Text);
                }
            }
            
            void listView1_SelectedIndexChanged(object sender, EventArgs e)
            {
                //ListViewの選択項目がなくなったらTextBoxとButtoを非表示にする
                ListView lv = (ListView)sender;
                if (lv.SelectedIndices.Count == 0)
                {
                    this.inputTextBox.Tag = null;
                    this.inputTextBox.Visible = false;
    
                    this.inputButton.Tag = null;
                    this.inputButton.Visible = false;
                }
            }
    
            //ListViewの各アイテムが描画されるときに呼ばれる
            private void listView1_DrawItem(object sender, DrawListViewItemEventArgs e)
            {
                ListView lv = (ListView)sender;
                var imglist = e.Item.ImageList;
                var image = imglist.Images[e.Item.ImageIndex];
    
                //まずアイコンを描画する
                e.Graphics.DrawImage(image, e.Bounds.Location );
    
                //文字列の書式
                StringFormat sf=new StringFormat();
                sf.Alignment = StringAlignment.Near;
                sf.LineAlignment = StringAlignment.Center ;
                sf.Trimming = StringTrimming.Character;
    
                //アイコンを除いた領域
                Rectangle rect=new Rectangle(e.Bounds.X,e.Bounds.Top+imglist.ImageSize.Height, e.Bounds.Width,e.Bounds.Height-imglist.ImageSize.Height);
                if (e.Item.Selected)
                {   //選択している項目の上にTextBoxとButtonを表示させる
                    MoveEditControls(lv, e.Item, rect);
    
                    //e.Graphics.FillRectangle(SystemBrushes.Highlight, rect);
                    //e.Graphics.DrawString(e.Item.Text, lv.Font,SystemBrushes.HighlightText ,rect, sf );
                }
                else
                {
                    //選択されていないときは文字列を描画する
                    e.Graphics.DrawString(e.Item.Text, lv.Font, new SolidBrush(lv.ForeColor),rect, sf );
                }
            }
    
            private void MoveEditControls(ListView lv,ListViewItem item , Rectangle rect)
            {
                //選択されている項目用のTextBoxとButtonを表示する
                inputButton.Visible = true;
                inputTextBox.Visible = true;
    
                //TextBoxとButtonを描画可能な領域に配置する
                inputTextBox.Height = rect.Height;
                inputTextBox.Width =Math.Max( rect.Width - rect.Height,0);
    
                inputButton.Height = (int)rect.Height;
                inputButton.Width = rect.Height;
    
                inputTextBox.Location = new Point(rect.X, rect.Y);
                inputButton.Location = new Point(rect.X + inputTextBox.Width, rect.Y);
    
                //TextBoxに表示する文字を設定する
                inputTextBox.Text = item.Text;
    
                inputTextBox.Tag = item;
                inputButton.Tag = item;
            }
        }
    }

    VB.NET

    Imports System
    Imports System.Drawing
    Imports System.Windows.Forms
    
    Public Class Form1
    
        Private WithEvents inputTextBox As TextBox
        Private WithEvents inputButton As Button
        Private WithEvents listView1 As ListView
    
        Sub New()
    
            ' この呼び出しはデザイナーで必要です。
            InitializeComponent()
    
            ' InitializeComponent() 呼び出しの後で初期化を追加します。
    
            Me.listView1 = New ListView()
            Me.listView1.Dock = DockStyle.Fill
            Me.listView1.MultiSelect = False '複数選択の時はとりあえず考慮しない
            Me.listView1.OwnerDraw = True '自分でListViewのアイテムを描画する
    
            Me.listView1.Items.Clear()
            Me.listView1.LargeImageList = Me.ImageList1 'イメージが3個以上登録されているImageListを用意してください。
            'me.listView1.LargeImageList.ImageSize = new Size(64, 64)
            Me.Controls.Add(Me.listView1)
    
            Me.inputTextBox = New TextBox()
            Me.inputTextBox.Visible = False
            Me.listView1.Controls.Add(inputTextBox)
    
            Me.inputButton = New Button()
            Me.inputButton.Visible = False
            Me.listView1.Controls.Add(inputButton)
    
            Me.listView1.Items.Add(New ListViewItem("TEST1", 0))
            Me.listView1.Items.Add(New ListViewItem("TEST2", 1))
            Me.listView1.Items.Add(New ListViewItem("TEST3", 2))
    
        End Sub
    
    
        Private Sub inputTextBox_TextChanged(sender As Object, e As System.EventArgs) Handles inputTextBox.TextChanged
            Dim txb As TextBox = DirectCast(sender, TextBox)
            Dim item As ListViewItem = TryCast(txb.Tag, ListViewItem)
            If (item IsNot Nothing AndAlso item.Text <> txb.Text) Then
                item.Text = txb.Text
            End If
        End Sub
    
        Private Sub inputButton_Click(sender As Object, e As System.EventArgs) Handles inputButton.Click
            Dim btn As Button = DirectCast(sender, Button)
            Dim item As ListViewItem = TryCast(btn.Tag, ListViewItem)
            If (item IsNot Nothing) Then
                MessageBox.Show(item.Text)
            End If
        End Sub
    
        Private Sub listView1_SelectedIndexChanged(sender As Object, e As System.EventArgs) Handles listView1.SelectedIndexChanged
            'ListViewの選択項目がなくなったらTextBoxとButtoを非表示にする
            Dim lv As ListView = DirectCast(sender, ListView)
            If (lv.SelectedIndices.Count = 0) Then
                Me.inputTextBox.Tag = Nothing
                Me.inputTextBox.Visible = False
    
                Me.inputButton.Tag = Nothing
                Me.inputButton.Visible = False
            End If
        End Sub
    
        'ListViewの各アイテムが描画されるときに呼ばれる
        Private Sub listView1_DrawItem(sender As Object, e As System.Windows.Forms.DrawListViewItemEventArgs) Handles listView1.DrawItem
            Dim lv As ListView = DirectCast(sender, ListView)
            Dim imglist As ImageList = e.Item.ImageList
            Dim image As Image = imglist.Images(e.Item.ImageIndex)
    
            'まずアイコンを描画する
            e.Graphics.DrawImage(image, e.Bounds.Location)
    
            '文字列の書式
            Dim sf As StringFormat = New StringFormat()
            sf.Alignment = StringAlignment.Near
            sf.LineAlignment = StringAlignment.Center
            sf.Trimming = StringTrimming.Character
    
            'アイコンを除いた領域
            Dim rect As New Rectangle(e.Bounds.X, e.Bounds.Top + imglist.ImageSize.Height, e.Bounds.Width, e.Bounds.Height - imglist.ImageSize.Height)
            If (e.Item.Selected) Then
                '選択している項目の上にTextBoxとButtonを表示させる
                MoveEditControls(lv, e.Item, rect)
    
                '/e.Graphics.FillRectangle(SystemBrushes.Highlight, rect);
                'e.Graphics.DrawString(e.Item.Text, lv.Font,SystemBrushes.HighlightText ,rect, sf );
    
            Else
    
                '選択されていないときは文字列を描画する
                e.Graphics.DrawString(e.Item.Text, lv.Font, New SolidBrush(lv.ForeColor), rect, sf)
            End If
        End Sub
        Private Sub MoveEditControls(ByVal lv As ListView, ByVal item As ListViewItem, ByVal rect As Rectangle)
    
            '選択されている項目用のTextBoxとButtonを表示する
            inputButton.Visible = True
            inputTextBox.Visible = True
    
            'TextBoxとButtonを描画可能な領域に配置する
            inputTextBox.Height = rect.Height
            inputTextBox.Width = Math.Max(rect.Width - rect.Height, 0)
    
            inputButton.Height = rect.Height
            inputButton.Width = rect.Height
    
            inputTextBox.Location = New Point(rect.X, rect.Y)
            inputButton.Location = New Point(rect.X + inputTextBox.Width, rect.Y)
    
            'TextBoxに表示する文字を設定する
            inputTextBox.Text = item.Text
    
            inputTextBox.Tag = item
            inputButton.Tag = item
        End Sub
    
    End Class

    C#とVB.Netのどちらを使っているのか不明なので両方書いてみた

    #複雑になるとWPFを使った方が楽になりますけどね。


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)


    • 編集済み gekkaMVP 2014年8月14日 12:41 VB.NETのコードを追記
    2014年8月14日 12:19
  • Windows Formsでもできないことはないと思いますが、かなり力技が要ります。また、仮想化せずに大量のコントロールを生成すると、GDIオブジェクト数の上限にも引っかかりやすくなります。なのでAzuleanさんの意見同様、WPFを使ったほうが良いと思います。Windowsストアアプリへの応用も効きますし。

    Visual C#のWPFアプリケーションで、「WpfThumbnailViewerTest」というプロジェクトを作成し、下記2つのソースを書き換えます。フォルダー参照ダイアログを補助的に使っているので、参照設定にSystem.Windows.Formsを追加します。

    MainWindow.xaml

    <Window
        x:Class="WpfThumbnailViewerTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        mc:Ignorable="d"
        Title="WPF Thumbnail Viewer Test"
        Width="525"
        Height="350"
        WindowStartupLocation="CenterScreen"
        >
        <Grid Margin="10">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="10"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <DockPanel Grid.Row="0">
                <Button Grid.Row="0" Name="buttonSelectFolder" Content="フォルダー選択" Padding="5"/>
                <TextBox Name="textboxFolderPath" Margin="10,0,0,0" IsReadOnly="True" VerticalContentAlignment="Center"/>
            </DockPanel>

            <ListBox Grid.Row="2" Name="listBox1" AlternationCount="2" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
                <ListBox.ItemContainerStyle>
                    <Style TargetType="ListBoxItem">
                        <!-- アイテムの選択状態を ViewModel 側にも持たせる。 -->
                        <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
                    </Style>
                </ListBox.ItemContainerStyle>
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel/>
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Vertical">
                            <Image Width="100" Height="100" Margin="2" Source="{Binding Path=ThumbnailImage}" ToolTip="{Binding Path=ImageFilePath}"/>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="Auto"/>
                                </Grid.ColumnDefinitions>
                                <Label Grid.Column="0" Content="印刷枚数:" VerticalAlignment="Center"/>
                                <TextBox Grid.Column="1" Width="60" Padding="5,0" Text="{Binding Path=PrintingCount, Mode=TwoWay}" HorizontalContentAlignment="Right" VerticalContentAlignment="Center"/>
                                <StackPanel Grid.Column="2" Orientation="Vertical">
                                    <Button Content="+" Name="buttonPlusPrintingCount" Click="buttonPlusPrintingCount_Click"/>
                                    <Button Content="-" Name="buttonMinusPrintingCount" Click="buttonMinusPrintingCount_Click"/>
                                </StackPanel>
                            </Grid>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Grid>
    </Window>

    MainWindow.xaml.cs

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;

    namespace WpfThumbnailViewerTest
    {
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
            ObservableCollection<DataModels.MyItemInfo> targetItems = new ObservableCollection<DataModels.MyItemInfo>();

            public MainWindow()
            {
                InitializeComponent();

                this.buttonSelectFolder.Click += this.buttonSelectFolder_Click;
                this.listBox1.ItemsSource = this.targetItems;

                // テストコード。
    #if false
                for (int i = 0; i < 10; ++i)
                {
                    this.targetItems.Add(new DataModels.MyItemInfo() { PrintingCount = i });
                }
    #endif
            }

            void buttonSelectFolder_Click(object sender, RoutedEventArgs e)
            {
                // TODO: 参照設定に System.Windows.Forms を追加しておくこと。
                using (var dialog = new System.Windows.Forms.FolderBrowserDialog())
                {
    #if false
                    if (String.IsNullOrEmpty(this.textboxFolderPath.Text))
                    {
                        dialog.RootFolder = Environment.SpecialFolder.MyPictures;
                    }
    #endif
                    dialog.SelectedPath = this.textboxFolderPath.Text;
                    var result = dialog.ShowDialog();
                    if (result == System.Windows.Forms.DialogResult.OK)
                    {
                        this.textboxFolderPath.Text = dialog.SelectedPath;
                        var filePaths = System.IO.Directory.GetFiles(
                            this.textboxFolderPath.Text, "*.jpg", System.IO.SearchOption.TopDirectoryOnly);
                        this.targetItems.Clear();
                        foreach (var filePath in filePaths)
                        {
                            // HACK: ロードできた画像から順次表示していく、非同期読み込みができるとよい。
                            this.targetItems.Add(new DataModels.MyItemInfo() { ImageFilePath = filePath });
                        }
                    }
                }
            }

            // HACK: 説明および簡単のため Button を2つ埋め込み、それぞれイベント ハンドラーを明示的に定義したが、
            // Extended WPF Toolkit のようなキーリピートの効くスピン エディット コントロールを作り、
            // さらにデータ バインディングしたほうがよい。

            private void buttonPlusPrintingCount_Click(object sender, RoutedEventArgs e)
            {
                var button = sender as Button;
                if (button != null)
                {
                    var item = button.DataContext as DataModels.MyItemInfo;
                    if (item != null)
                    {
                        ++item.PrintingCount;
                    }
                }
            }

            private void buttonMinusPrintingCount_Click(object sender, RoutedEventArgs e)
            {
                var button = sender as Button;
                if (button != null)
                {
                    var item = button.DataContext as DataModels.MyItemInfo;
                    if (item != null)
                    {
                        --item.PrintingCount;
                    }
                }
            }
        }

        namespace DataModels
        {
            class MyItemInfo : INotifyPropertyChanged
            {
                public event PropertyChangedEventHandler PropertyChanged;

                protected void NotifyPropertyChanged(string propertyName)
                {
                    if (this.PropertyChanged != null)
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                    }
                }

                public MyItemInfo()
                {
                }

                public const int MinPrintingCount = 0;
                public const int MaxPrintingCount = 10;

                bool isSelected = false;
                int printingCount = 0;
                string imageFilePath = null;
                BitmapSource thumbnailImage = null;

                public bool IsSelected
                {
                    get { return this.isSelected; }
                    set
                    {
                        if (!Equals(this.isSelected, value))
                        {
                            this.isSelected = value;
                            this.NotifyPropertyChanged("IsSelected");
                        }
                    }
                }

                public int PrintingCount
                {
                    get { return this.printingCount; }
                    set
                    {
                        int newValue = Math.Min(Math.Max(value, MinPrintingCount), MaxPrintingCount);
                        if (!Equals(this.printingCount, newValue))
                        {
                            this.printingCount = newValue;
                            this.NotifyPropertyChanged("PrintingCount");
                        }
                    }
                }

                public string ImageFilePath
                {
                    get { return this.imageFilePath; }
                    set
                    {
                        if (!Equals(this.imageFilePath, value))
                        {
                            this.imageFilePath = value;
                            this.thumbnailImage = CreateBitmapImageFromFile(this.imageFilePath, 100);
                            this.NotifyPropertyChanged("ImageFilePath");
                            this.NotifyPropertyChanged("ThumbnailImage");
                        }
                    }
                }

                public BitmapSource ThumbnailImage
                {
                    get { return this.thumbnailImage; }
                }

                private static BitmapSource CreateBitmapImageFromFile(string imageFilePath, int thumbnailWidth)
                {
                    try
                    {
                        // HACK: URI を使って構築する方法だと対象ファイルがロックされてしまうはず。
                        // しかし FileStream と WriteableBitmap を使う方法だとサムネイルの自動作成ができない。
                        var bitmapImage = new BitmapImage();
                        bitmapImage.BeginInit();
                        bitmapImage.UriSource = new Uri(imageFilePath);
                        bitmapImage.DecodePixelWidth = thumbnailWidth;
                        bitmapImage.EndInit();
                        return bitmapImage;
                    }
                    catch (Exception err)
                    {
                        Debug.WriteLine(err.Message);
                        return null;
                    }
                }
            }
        }
    }

    Visual Studio 2013(.NET 4.5)とWindows 8.1環境でしかテストしてませんが、たぶんVS 2010でもいけるんじゃないでしょうか。

    ちなみにMyItemInfo.NotifyPropertyChanged()メソッドの実装は簡単のためプロパティ名の文字列リテラルを直接指定する方式にしましたが、System.Linq.Expressionsによるラムダ式木からのプロパティ名取得やC# 5.0のCaller Infoが使えれば、もっとメンテナンス性を上げることはできます。

    • 編集済み sygh 2014年8月15日 3:13
    • 回答としてマーク FG10 2014年8月20日 23:21
    2014年8月14日 12:50
  • ListViewコントロールをフォーム内に配置し、ViewプロパティをLargeIconに設定することで

    簡易サムネイルリストを表示できますが、各サムネイルエリア内にテキストボックスやボタンを配置した状態で

    描画することは可能でしょうか。

    みなさんも書かれていますが、これを実現するのは決して簡単なことではありません。歴史的なことを言えば、このようなことはAccessのサブフォームを使えば簡単に実現できることなのに、Windowsフォームでは難しいという、もどかしい時代がありました。やがて、WPFが発表され、Accessのサブフォームのように表現ができるということを知り、心が躍ったことを覚えています。今ではWPFで、そのようなことは簡単に出来てしまいます。
    初学者ということですが、Windowsフォームで開発しなければならない理由がないのであれば、WPFで開発されることをお勧めします。ちなみに私の場合ですが、新規に開発する物件は、特に理由がなければWPFで開発を行います。MVVMなどというプログラムの組み方にこだわらず、普通にコードビハインドで開発すれば、Windowsフォームとほぼ同じ感覚で開発ができますので、まずはそれから慣れられるのも敷居が下がって良いのではないかと思います。

    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    2014年8月14日 13:05
    モデレータ
  • 返信が大変遅くなり申し訳ありません。

    丁寧な解説をありがとうございます。

    2014年8月20日 23:21
  • 返信が大変遅くなり申し訳ありません。

    WPFの使用は予想外でした。

    記述いただいたコードで即動作しましたので、いったん回答として

    マークさせていただきます。コード内容の勉強をさせていただきます。

    ありがとうございました。

    2014年8月20日 23:23
  • 返信が大変遅くなり申し訳ありません。

    記述いただいたコード内容の勉強をさせていただきます。

    ありがとうございました。

    2014年8月20日 23:24
  • MSDNフォーラムでは「返信」を選択しても、ツリーの表示を「フラット リスト表示」にしていると、誰に対する返信なのか分かりませんので、できるだけメールのように返信相手の名前を先頭に書くようにされると良いと思います。

    trapemiyaさんもおっしゃっていますが、最初はMFCやWindows Formsと同じ方法論でベタに作成していき、慣れたらWPF/WinRT特有のバインディングやMVVMといった概念を少しずつ勉強されると良いと思います。

    また、UIをデザインするときも、上級者はWindows Formsとは違いXAMLデザイナーを使ってドラッグ&ドロップで部品を貼り付けていくということはほとんどなく、使い込んでいくうちに実はXAMLエディターを使ってコードを直接書いたほうが効率的できれいな相対配置ができるということに気付くと思います。

    なお、提示したサンプルでも一部Windows Formsを補助的に使っているように、メインはWPFだけど手間を減らすためWindows Formsを部分的に使うということはよくあるので、Windows Formsの経験や知識自体は決して無駄になりません。 

     

    ちなみに今回は説明を簡単にするためSystem.Windows.Media.Imaging.BitmapImageを使いましたが、ファイルロック問題のほかにも注意して使わないとリソースリークする可能性があるそうです。こちらは自分で情報を集めて模索・解決してみてください。

    俺が遭遇したWPFイメージコントロールのメモリーリークと回避法(?)の1つ - C#でプログラミングあれこれ

    • 編集済み sygh 2014年8月23日 13:14
    2014年8月21日 11:46
  • syghさん、返信についてのご説明ありがとうございます。

    syghさんにご提示いただいたサンプルコードを参考に実装を進めているのですが、

    テキストボックス内に入力された枚数を集計(どの写真が何枚指定されたか)する取得がうまくいかず苦戦しています。

    私が試したlistBox.SelectedItemsやSelectedItemでは取得できませんでした。

    listBoxで選択している画像(単数or複数)のパスだけでも取得できたらと思うのですが…

    回答済みのサンプルコードへの質問となり大変恐縮ですが、どうかご教示お願いいたします。

    2014年8月28日 13:29
  • MainWindow.targetItemsフィールドがGUIの状態と連動したデータモデルそのものです。双方向データバインディングにより、GUIのListBoxのアイテムとtargetItemsコレクションデータの状態は同期されます。つまりtargetItemsを調べれば、GUIの状態がそのまま分かります。

    もしListBoxで選択されたアイテムのみに関する状態が欲しいのであれば、SelectedItemもしくはSelectedItemsの各要素をMyItemInfoとしてas演算子で変換することで、GUIにバインディングされている対応データのインスタンスが得られます。デバッガでそれぞれのプロパティの中身に何が入っているのか、ちゃんとのぞいてみたりしましたか? 他に、MyItemInfo.IsSelectedを見ながらtargetItemsを先頭から末尾まで線形探索する方法もあります。for/foreachをベタに書くほか、LINQを使ってもよいでしょう。

    ちなみにC#コードビハインドではコントロールを明示的に作成したり、コントロールから入力データを明示的に取得したり、逆にコントロールに対してデータを明示的に設定したりするようなコードを一切書いていないのに、ListBox.ItemsSourceにtargetItemsというコレクションデータを設定するだけでリストボックス内に対応するアイテムが生成されたり、TextBox.Textを変更しているわけでもないのにMyItemInfo.PrintingCountを操作することでGUIにデータ変更が反映されていたりすることに気づきませんか? これがWPFのデータテンプレート、そしてINotifyPropertyChangedによる双方向データバインディングの威力です。

    私の提供するヒントはここまでです。後はMSDNのマニュアルやサンプル、入門サイトを熟読しながら試行錯誤を繰り返し、自分で苦労しながら解析・模倣したほうがいいと思います。特にデバッガの使い方を覚えましょう。開発環境の進化のおかげで、昔に比べたら遥かに開発しやすくなっています。お説教をしているつもりではありませんが、自分で最大限の努力をせずに、他人に1から10まで教えられて得たものは結局身に付きません。

    • 編集済み sygh 2014年8月29日 19:04
    2014年8月29日 17:00