はじめに
私はListViewコントロールをよく使います。といっても、大小のアイコンビューを表示するためではなく、もっぱらWindows Explorer画面の右側に表示されるような詳細ビューを表示するために使っています。詳細ビューとは、Windows Explorerの[表示]メニューで[詳細]を選択するか、ツールバーのドロップダウンボタンで[詳細]を選択したときに表示されるビューです。
詳細ビューとして表示した場合、ListViewは簡単に使える読み取り専用グリッドのような動作をします。ListViewコントロールを使用すると、開発者とユーザーの双方にメリットがあります。開発者にとってのメリットは、アイテムおよびサブアイテムの追加、削除、再配置が簡単に行えることです。ユーザーにとってのメリットは、行の選択、列の再配置、さらに行のソートまで行えることです。
ListViewにはこうした操作のための基本的な手段が用意されていますが、うまく動かすためにはある程度のコードを追加する必要があります。本稿で説明するSortableListViewコントロールでも、便利な機能を実現するためにコードを書き加えています。SortableListViewはListView
クラスを継承しており、ListViewコントロールのすべての機能に加え、全列によるソートや選択した列によるソートの機能にも対応しています。
選択した列によるソート処理
ListViewコントロールには、表示するアイテムを保持するItemsコレクションが含まれています。このコレクション内の各アイテムは、ListViewItem
クラスのインスタンスであり、アイテムのもっと詳細な情報(サブアイテム)を取り扱うSubItemsコレクションを公開しています。詳細ビューでは、一番左の列にアイテムを表すテキストが、その他の列にそのサブアイテムがそれぞれListViewによって一覧表示されます。
ListViewコントロールには、自らのデータをソートするSort
メソッドが最初から用意されています。しかし、デフォルトでは、このコントロールによるソートの対象はアイテムだけであり、サブアイテムはソートできません。幸い、ListViewコントロールのListViewItemSorter
プロパティを使えば、このコントロールによるアイテムのソート方法を変更できます。
ソート処理をカスタマイズするには、ListViewItemSorter
プロパティに対して、IComparerインターフェイスを実装したオブジェクトを指定しなければなりません。このインターフェイスで定義されているのは、Compare
メソッドだけです。このメソッドは、2つのアイテムをパラメータとして受け取り、両アイテムのソート順の比較結果によって、-1(最初のアイテムが2番目のアイテムより小さい)、0(両アイテムのソート順が同じ)、1(最初のアイテムが2番目より大きい)のいずれかを返します。
今回作成するSortableListViewコントロールでは、IComparer
クラスを使って2つの新しいソート機能を実現します。1つ目の機能では、最初の列に限らず、任意の列を選択してソートできるようにします。図1に、Pages列で昇順(値が最小の要素が一番上になる)にソートしたListViewコントロールの様子を示します。Pages列にデータを持たない行が一番上にソートされることに注意してください。ListViewコントロールによって、現在のソートの基準となっている列(Pages列)に上向きの矢印が表示されています。これは、その列が昇順にソートされていることを示しています(矢印は最小のアイテムの方向を指します)。列ヘッダをクリックすると、降順のソート処理に切り換わります。
別の列ヘッダをクリックすると、Windows Explorerと同じように、その列を基準としてソートが行われます。図2に、Title列を2回クリックした後の同じフォームの状態を示します。1回目のクリックでこの列によるソートが実行され、2回目のクリックで降順のソートに変わっています。
以下に、SortableListViewコントロールの中でこの機能を実現するために使っているSelectedColumnSorter
クラスのコードを示します。なお、本稿のダウンロードファイルにはVisual Basic版とC#版の両方が収録されています。
' Sort the ListView items by the selected column. Private Class SelectedColumnSorter Implements IComparer ' Compare two ListViewItems. Public Function Compare(ByVal x As Object, _ ByVal y As Object) As Integer _ Implements System.Collections.IComparer.Compare ' Get the items. Dim itemx As ListViewItem = DirectCast(x, ListViewItem) Dim itemy As ListViewItem = DirectCast(y, ListViewItem) ' Get the selected column index. Dim slvw As SortableListView = itemx.ListView Dim idx As Integer = slvw.m_SelectedColumn If idx < 0 Then Return 0 ' Compare the sub-items. If itemx.ListView.Sorting = SortOrder.Ascending Then Return String.Compare( _ ItemString(itemx, idx), ItemString(itemy, idx)) Else Return -String.Compare( _ ItemString(itemx, idx), ItemString(itemy, idx)) End If End Function ' Return a string representing this item's sub-item. Private Function ItemString( _ ByVal listview_item As ListViewItem, ByVal idx As Integer) _ As String Dim slvw As SortableListView = listview_item.ListView ' Make sure the item has the needed sub-item. Dim value As String = "" If idx <= listview_item.SubItems.Count - 1 Then value = listview_item.SubItems(idx).Text End If ' Return the sub-item's value. If slvw.Columns(idx).TextAlign = _ HorizontalAlignment.Right _ Then ' Pad so numeric values sort properly. Return value.PadLeft(20) Else Return value End If End Function End Class
Compare
関数は、2つのパラメータをジェネリックなObjectからListViewItem
オブジェクトに変換します。また、最初のアイテムのListView
プロパティを使って、そのアイテムを含むSortableListViewコントロールを取得しています。
続いて、SortableListViewコントロールのm_SelectedColumn
変数を参照して、ソートに使用すべき列を決めます(この変数については後で説明します)。どの列も選択されていない場合、この関数はそれ以上何も行わずに処理を終了します。
次に、SortableListViewコントロールのSorting
プロパティを参照して、オブジェクトのソートを昇順で行うか降順で行うかを決めます。SelectedColumnSorter
オブジェクトは自らのItemString
関数を呼び出し、ListViewItem
オブジェクトのそれぞれについて、選択した列のアイテムを表す文字列を生成します。さらに、これらの各文字列をString.Compare
を使って比較し、その結果を返します。アイテムのソートが降順で行われる場合は、適切な結果が得られるようにString.Compare
の結果の符号を反転させます。
ItemString
関数は、あるListViewItemで選択されている列を表す文字列を返します。まず、そのListViewItemに目的の列があるかどうかを確認します。例えば、ListViewItemに1つしかサブアイテムがないのに5番目の列でソートしようとした場合、その列については空の文字列が使われます。
対象列の適切な値(サブアイテムのテキスト文字列または空文字列)が得られると、対応する列のTextAlign
プロパティをチェックします。その列のテキストが右詰めであれば、数値データである可能性が高いため、単純にアルファベット順でソートを行ってはなりません。例えば、アルファベット順では文字列"100"が"11"よりも前になってしまいますが、ソート後のリストではおそらく"11"のほうを先に表示したいはずです。
右詰めの数字を適切にソートするために、ItemString
関数はそうした値の左側に空白を追加します。空白は、アルファベット順で数字よりも前になるので、文字列"11"はねらい通りに"100"よりも前に来ることになります。