C#のListViewを使ってみた

C#のWPFアプリでListViewを使ってみたメモです。追加、削除、ソート、アイテムの選択をやってみました。

目次

  1. こんなアプリを作ってみた
  2. ListViewにバインディングするコレクションを作る
  3. ListViewとコレクションのバインディング
  4. アイテムを追加する
  5. 選択したアイテムのフィールドを表示する
  6. アイテムを削除する
  7. ListViewのヘッダをクリックしたときにソートさせる
    1. XAMLへの仕込み
    2. コード部分
  8. コード全文
  9. 動かしてみた
  10. まとめ

こんなアプリを作ってみた

ListViewを1つ、Buttonが2つ、TextBoxが1つ、TextBlockが1つからなるアプリです。下記の機能を持ちます。

  • 「Add」ボタンを押すと、Field1にTextBoxの内容が入ったアイテムが追加される。

  • アイテムを選択すると、選択したアイテムのフィールドがTextBlockに表示される。

  • 「Del」ボタンを押すと、選択したアイテムが削除される。

  • ListViewのヘッダをクリックすると、アイテムがソートされる。 160205-1-01

ListViewを扱うアプリって、だいたいこういう機能を持ってますよね。

ListViewにバインディングするコレクションを作る

まず、アイテムの入れ物となるコレクションを作らないと話になりません。

アイテムの型を下記のようなクラスにします。

public class HogeHoge
{
    public Int64 Id { get; set; }
    public string Field1 { get; set; }
    public string Field2 { get; set; }
}

ネーミングセンスが枯渇してます。IdがInt64なのは、System.Data.SQLiteといろいろしたいという思惑からです。

このHogeHoge型のコレクションのインスタンスを作ります。

ObservableCollection<HogeHoge> list = new ObservableCollection<HogeHoge>();

ObservableCollectionにするのはListViewにバインディングする際のお約束のようなもののようなのですが、MSDNによれば、ListBoxやListViewなどに動的バインディングをしてコレクションの変更を自動的にUIに反映させたい場合はINotifyCollectionChangedインターフェースを持つコレクションをバインドする必要があって、ObservableCollectionにはINotifyCollectionChangedインターフェースが実装されているのだそうです。確かにMSDNによると、ObservableCollectionクラスにはCollectionChangedとPropertyChangedというイベントが記載されてますが、Collectionクラスにはイベントの記載がありません。

というわけで、ListViewにバインディングするコレクションにはObservableCollectionクラスを使います。

ListViewとコレクションのバインディング

MainWindow.xamlのListView要素にバインディングの設定をします。ListView要素のItemsSource="{Binding}"と各GridViewColumn要素のDisplayMemberBinding="{Binding Path=xx}"部分がバインディング用の記述です。

<ListView x:Name="listView" ItemsSource="{Binding}" HorizontalAlignment="Left" Height="109" Margin="10,10,0,0" VerticalAlignment="Top" Width="175" SelectionChanged="listView_SelectionChanged">
    <ListView.View>
        <GridView>
            <GridViewColumn DisplayMemberBinding="{Binding Path=Id}">
                <GridViewColumnHeader Content="Id" Tag="Id" Click="GridViewColumnHeader_Click" />
            </GridViewColumn>
            <GridViewColumn DisplayMemberBinding="{Binding Path=Field1}">
                <GridViewColumnHeader Content="Field1" Tag="Field1" Click="GridViewColumnHeader_Click" />
            </GridViewColumn>
            <GridViewColumn DisplayMemberBinding="{Binding Path=Field2}">
                <GridViewColumnHeader Content="Field2" Tag="Field2" Click="GridViewColumnHeader_Click" />
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

MainWindow.xaml.csの方では、WindowのLoadedイベントのところで、ListViewのDataContextプロパティにバインディングするコレクションを指定します。

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    listView.DataContext = list;
}

アイテムを追加する

アイテムを追加します。Idが重複しないように、既存のアイテムのIdの最大値に1を足したものを新しいアイテムのIdとします。既存のコレクションの中から指定したプロパティの最大値を求める計算には、Linqを使ってみました。

private void button_Click(object sender, RoutedEventArgs e)
{
    // 追加する項目のIdの値を計算する
    Int64 newId;
    if (list.Count > 0)
    {
        // 既存の項目のIdの最大値をを求めて、それに+1する
        var query = from p in list select p.Id;
        newId = query.Max() + 1;
    }
    else
    {
        newId = 1;
    }
    HogeHoge item = new HogeHoge { Id = newId, Field1 = textBox.Text, Field2 =null }; // 追加する項目の内容を設定する
    list.Add(item); // listに項目を追加する
}

listコレクションにアイテムを追加すると、ListViewにもアイテムの追加が自動的に反映されます。

選択したアイテムのフィールドを表示する

ListViewのSelectionChangedイベントにコードを書いてみました。どうやってアイテムを取り出すかちょっと悩みました。ListView.SelectedItemプロパティで選択されたアイテムを取り出しできます。取り出したアイテムを、アイテムの型(今回はHogeHoge型)に変換して、そのインスタンスのプロパティにアクセスするとアイテムの中身を取り出せます。

private void listView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (listView.SelectedItem == null) return; // ListViewで何も選択されていない場合は何もしない

    HogeHoge item = (HogeHoge)listView.SelectedItem; // ListViewで選択されている項目を取り出す
    textBlock.Text = item.Field1; // 取り出された項目のプロパティをTextBlockに表示する
}

ListViewのSelectionChangedイベントは、ユーザーがListViewをクリックする以外の操作でも発生する(例えば、アイテムを削除したとき)ようで、SelectionChangedイベントですがSelectedItemがnullの場合は何もしないようにしておかないと、エラーになります。

アイテムを削除する

選択したアイテムを削除します。LitView.SelectedItemプロパティで選択されたアイテムを取り出して、Remove()メソッドでそのアイテムに一致するものをコレクションから削除します。コレクションからアイテムが削除されると、ListViewにも自動的に反映されます。

private void buttonRemove_Click(object sender, RoutedEventArgs e)
{
    if (list.Count < 1) return; // listに項目が無い場合は何もしない

    HogeHoge item = (HogeHoge)listView.SelectedItem; // ListViewで選択されている項目を取り出す
    list.Remove(item); // listから選択された項目と一致するものを削除する
}

Remove()メソッドは引数に一致する最初のアイテムをコレクションから削除するということなので、操作するコレクションとListViewの項目が一致しないような作りになっている場合は要注意です。

ListViewのヘッダをクリックしたときにソートさせる

DetaGridにはヘッダをクリックするとそのフィールドを基準にしてアイテムをソートする機能があります。ですが、ListViewのグリッド表示モードにはその機能がありません。でも、ListViewのようなリスト表示をされたら、ヘッダをクリックしてソートしようとしますよね。

「だったらDataGridを使えば良いじゃん」と思うのですが、確かWPFのDataGridって.Net 3.5ではサポートされていなかったような。そして、世のPCの大半(特に仕事用)はWindows7だったりします。OfficeとかAcrobat Readerが.Net 4.5あたりを自動インストールしてくれれば良いのに。

というわけで、ヘッダをクリックしたらそのフィールドを基準にしてソートするようにしてみます。ただし、好みのソート順になるまで、2回くらいクリックするかもしれないというなんちゃって仕様です。

XAMLへの仕込み

XAMLにクリックした時のイベントを設定します。各GridViewColumnHeader要素にTag=xxx Click="GridViewColumnHeader_Click"というTagとClick属性を追加します。Click属性はコードへのコマンドバインディングのようなもので、Tag属性はどのカラムのヘッダがクリックされたのかを判断するために使います。

<ListView x:Name="listView" ItemsSource="{Binding}" HorizontalAlignment="Left" Height="109" Margin="10,10,0,0" VerticalAlignment="Top" Width="175" SelectionChanged="listView_SelectionChanged">
    <ListView.View>
        <GridView>
            <GridViewColumn DisplayMemberBinding="{Binding Path=Id}">
                <GridViewColumnHeader Content="Id" Tag="Id" Click="GridViewColumnHeader_Click" />
            </GridViewColumn>
            <GridViewColumn DisplayMemberBinding="{Binding Path=Field1}">
                <GridViewColumnHeader Content="Field1" Tag="Field1" Click="GridViewColumnHeader_Click" />
            </GridViewColumn>
            <GridViewColumn DisplayMemberBinding="{Binding Path=Field2}">
                <GridViewColumnHeader Content="Field2" Tag="Field2" Click="GridViewColumnHeader_Click" />
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

コード部分

XAMLにClick属性を追加すると、コードの方にもClickされたときのイベントが自動的に追加されます。GridViewColumnHeaderクラスのインスタンスを作って、そのTagプロパティを参照すると、XAMLで指定したTag属性の値が得られます。今回は、実際にソートをする部分は別のメソッドにしましたので、ソートのメソッドを呼び出します。

private void GridViewColumnHeader_Click(object sender, RoutedEventArgs e)
{
    GridViewColumnHeader columnHeader = sender as GridViewColumnHeader;
    string tag = columnHeader.Tag as string; // ヘッダーに設定してあるtagを取り出す
    SortListView(listView, tag, false); // 並び替えのメソッドを呼び出す
}

ListView.Items.SortDescriptionsプロパティにソートするカラムとソート順の情報を設定すると、それに従ってソートされます。本当は複数のカラムを組み合わせてソートできるはずなのですが、今回は1カラムだけでソートするようにします。いろいろしてる感じのコードですが、基本的には既存の登録がAscendingかDecendingか調べて逆のものをセットしているだけです。

public void SortListView(ListView listView, string tag, bool IsMultiSort)
{
    if (listView.Items.Count < 2) return; // ListViewの項目が0個または1個の場合は何もしない

    // なんちゃってソート
    ListSortDirection direction;
    // SortDescriptionsに何も登録されていない場合はSortDescriptionsにDescendingを登録して(Descendingで並び替えて)処理を終わる
    if (listView.Items.SortDescriptions.Count==0)
    {
        direction = ListSortDirection.Descending;
        listView.Items.SortDescriptions.Add(new SortDescription(tag, direction));
        return;
    }
    // 最後に登録されているSortDescriptionに合わせて、AscendingかDescendingか選択する
    if (listView.Items.SortDescriptions.Last().Direction == ListSortDirection.Ascending)
    {
        direction = ListSortDirection.Descending;
    }
    else
    {
        direction = ListSortDirection.Ascending;
    }
    // SortDescriptionをクリアして、選択したdirectionを登録する(選択したdirectionで並び替える)
    listView.Items.SortDescriptions.Clear();
    listView.Items.SortDescriptions.Add(new SortDescription(tag, direction));
}

コード全文

コードをまとめておきます。

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="200" Loaded="Window_Loaded">
    <Grid>
        <ListView x:Name="listView" ItemsSource="{Binding}" HorizontalAlignment="Left" Height="109" Margin="10,10,0,0" VerticalAlignment="Top" Width="175" SelectionChanged="listView_SelectionChanged">
            <ListView.View>
                <GridView>
                    <GridViewColumn DisplayMemberBinding="{Binding Path=Id}">
                        <GridViewColumnHeader Content="Id" Tag="Id" Click="GridViewColumnHeader_Click" />
                    </GridViewColumn>
                    <GridViewColumn DisplayMemberBinding="{Binding Path=Field1}">
                        <GridViewColumnHeader Content="Field1" Tag="Field1" Click="GridViewColumnHeader_Click" />
                    </GridViewColumn>
                    <GridViewColumn DisplayMemberBinding="{Binding Path=Field2}">
                        <GridViewColumnHeader Content="Field2" Tag="Field2" Click="GridViewColumnHeader_Click" />
                    </GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>
        <Button x:Name="button" Content="Add" HorizontalAlignment="Left" Margin="110,144,0,0" VerticalAlignment="Top" Width="37" Click="button_Click"/>
        <TextBox x:Name="textBox" HorizontalAlignment="Left" Height="18" Margin="10,144,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="95"/>
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="10,124,0,0" TextWrapping="Wrap" Text="{Binding}" VerticalAlignment="Top" Width="175" Height="15"/>
        <Button x:Name="buttonRemove" Content="Del" HorizontalAlignment="Left" Margin="152,144,0,0" VerticalAlignment="Top" Width="33" Click="buttonRemove_Click"/>
    </Grid>
</Window>
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        ObservableCollection<HogeHoge> list = new ObservableCollection<HogeHoge>();

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            listView.DataContext = list;
        }

        // Addボタンがクリックされたときの処理
        private void button_Click(object sender, RoutedEventArgs e)
        {
            // 追加する項目のIdの値を計算する
            Int64 newId;
            if (list.Count > 0)
            {
                // 既存の項目のIdの最大値をを求めて、それに+1する
                var query = from p in list select p.Id;
                newId = query.Max() + 1;
            }
            else
            {
                newId = 1;
            }
            HogeHoge item = new HogeHoge { Id = newId, Field1 = textBox.Text, Field2 = null }; // 追加する項目の内容を設定する
            list.Add(item); // listに項目を追加する
        }

        // Delボタンをクリックしたときの処理
        private void buttonRemove_Click(object sender, RoutedEventArgs e)
        {
            if (list.Count < 1) return; // listに項目が無い場合は何もしない

            HogeHoge item = (HogeHoge)listView.SelectedItem; // ListViewで選択されている項目を取り出す
            list.Remove(item); // listから選択された項目と一致するものを削除する
        }

        // ListViewのヘッダーがクリックされたときの処理
        private void GridViewColumnHeader_Click(object sender, RoutedEventArgs e)
        {
            GridViewColumnHeader columnHeader = sender as GridViewColumnHeader;
            string tag = columnHeader.Tag as string; // ヘッダーに設定してあるtagを取り出す
            SortListView(listView, tag, false); // 並び替えのメソッドを呼び出す
        }

        public void SortListView(ListView listView, string tag, bool IsMultiSort)
        {
            if (listView.Items.Count < 2) return; // ListViewの項目が0個または1個の場合は何もしない

            // なんちゃってソート
            ListSortDirection direction;
            // SortDescriptionsに何も登録されていない場合はSortDescriptionsにDescendingを登録して(Descendingで並び替えて)処理を終わる
            if (listView.Items.SortDescriptions.Count == 0)
            {
                direction = ListSortDirection.Descending;
                listView.Items.SortDescriptions.Add(new SortDescription(tag, direction));
                return;
            }
            // 最後に登録されているSortDescriptionに合わせて、AscendingかDescendingか選択する
            if (listView.Items.SortDescriptions.Last().Direction == ListSortDirection.Ascending)
            {
                direction = ListSortDirection.Descending;
            }
            else
            {
                direction = ListSortDirection.Ascending;
            }
            // SortDescriptionをクリアして、選択したdirectionを登録する(選択したdirectionで並び替える)
            listView.Items.SortDescriptions.Clear();
            listView.Items.SortDescriptions.Add(new SortDescription(tag, direction));
        }

        // リストビューの項目が選択されたときの処理
        private void listView_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (listView.SelectedItem == null) return; // ListViewで何も選択されていない場合は何もしない

            HogeHoge item = (HogeHoge)listView.SelectedItem; // ListViewで選択されている項目を取り出す
            textBlock.Text = item.Field1; // 取り出された項目のプロパティをTextBlockに表示する
        }
    }

    public class HogeHoge
    {
        public Int64 Id { get; set; }
        public string Field1 { get; set; }
        public string Field2 { get; set; }
    }
}

動かしてみた

起動直後の状態です。XAMLの方にListViewのヘッダのタイトルを記述したので、ヘッダの文字列が表示されています。

160205-1-02

TextBoxに「いろはに」と入力して、「Add」ボタンを押した状態です。ListViewに項目が追加されました。

160205-1-03

続けて「ほへと」と「ちりぬるを」を追加して、「ほへと」を選択した状態です。TextBlockに「ほへと」が表示されます。

160205-1-04

ヘッダ「Field1」をクリックすると、下図の様にアイテムが並び替わります。

160205-1-05

「ちりぬるを」を選択して「Del」をクリックすると、下図の様に「ちりぬるを」のアイテムが削除されます。

160205-1-06

まとめ

ListViewについてググってみて感じたのですが、WindowFormのListViewとWPFのListViewで結構違います。WPFの方はMVVMも入ってきたりして、いろいろな書き方があるようです。プロの人には便利でしょうが、初心者には取っ付きにくいですね。

更新日
公開日

広告