ListViewコントロールに柔軟なソート機能を追加するはじめに私はListViewコントロールをよく使います。といっても、大小のアイコンビューを表示するためではなく、もっぱらWindows Explorer画面の右側に表示されるような詳細ビューを表示するために使っています。詳細ビューとは、Windows Explorerの[表示]メニューで[詳細]を選択するか、ツールバーのドロップダウンボタンで[詳細]を選択したときに表示されるビューです。 詳細ビューとして表示した場合、ListViewは簡単に使える読み取り専用グリッドのような動作をします。ListViewコントロールを使用すると、開発者とユーザーの双方にメリットがあります。開発者にとってのメリットは、アイテムおよびサブアイテムの追加、削除、再配置が簡単に行えることです。ユーザーにとってのメリットは、行の選択、列の再配置、さらに行のソートまで行えることです。 ListViewにはこうした操作のための基本的な手段が用意されていますが、うまく動かすためにはある程度のコードを追加する必要があります。本稿で説明するSortableListViewコントロールでも、便利な機能を実現するためにコードを書き加えています。SortableListViewは 選択した列によるソート処理 ListViewコントロールには、表示するアイテムを保持するItemsコレクションが含まれています。このコレクション内の各アイテムは、 ListViewコントロールには、自らのデータをソートする ソート処理をカスタマイズするには、 今回作成するSortableListViewコントロールでは、 別の列ヘッダをクリックすると、Windows Explorerと同じように、その列を基準としてソートが行われます。図2に、Title列を2回クリックした後の同じフォームの状態を示します。1回目のクリックでこの列によるソートが実行され、2回目のクリックで降順のソートに変わっています。 以下に、SortableListViewコントロールの中でこの機能を実現するために使っている ’ 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 続いて、SortableListViewコントロールの 次に、SortableListViewコントロールの 対象列の適切な値(サブアイテムのテキスト文字列または空文字列)が得られると、対応する列の 右詰めの数字を適切にソートするために、 著者による注釈
データによっては、こうしたチェックをもっと念入りに行う必要があるかもしれません。例えば、このコードでは、右詰めの列にはせいぜい20桁の単純な数値しか含まれていないと仮定しています。つまり、このコードでは、"1e10"が"1E+10"と同じであることや、"April 1"が"January 1"よりも後になることは判断できないのです。ただし、一般的な考え方を示すサンプルコードにはなっています。
SelectedColumnSorterクラスを使う これで、SortableListViewコントロールは リスト1
Imports System.ComponentModel <ToolboxBitmap(GetType(SortableListView), _ "tbxSortableListView")> _ Public Class SortableListView Inherits ListView Public Enum SortStyles SortDefault SortAllColumns SortSelectedColumn End Enum ’ The current sort column for selected column sorting. Private m_SelectedColumn As Integer = -1 ’ Whether we sort by all columns, one column, or not at all. Private m_SortStyle As SortStyles = SortStyles.SortDefault Public Property SortStyle() As SortStyles Get Return m_SortStyle End Get Set(ByVal value As SortStyles) ’ If the current style is SortSelectedColumn, ’ remove the column sort indicator. If m_SortStyle = SortStyles.SortSelectedColumn Then If m_SelectedColumn >= 0 Then Me.Columns(m_SelectedColumn).ImageKey = _ Nothing m_SelectedColumn = -1 End If End If ’ Save the new value. m_SortStyle = value Select Case m_SortStyle Case SortStyles.SortDefault Me.ListViewItemSorter = Nothing Case SortStyles.SortAllColumns Me.ListViewItemSorter = New AllColumnSorter() Case SortStyles.SortSelectedColumn Me.ListViewItemSorter = _ New SelectedColumnSorter() End Select ’ Resort. ’ Me.Sort() End Set End Property ’ Change the selected sort column. Protected Overrides Sub OnColumnClick( _ ByVal e As System.Windows.Forms.ColumnClickEventArgs) MyBase.OnColumnClick(e) If Me.SortStyle = SortStyles.SortSelectedColumn Then ’ If this is the same sort column, switch the ’ sort order. If e.Column = m_SelectedColumn Then If Me.Sorting = SortOrder.Ascending Then Me.Sorting = SortOrder.Descending Else Me.Sorting = SortOrder.Ascending End If End If ’ Remove the image from the previous sort column. If m_SelectedColumn >= 0 Then Me.Columns(m_SelectedColumn).ImageKey = Nothing End If ’ If we’re not currently sorting, sort ascending. If Me.Sorting = SortOrder.None Then Me.Sorting = SortOrder.Ascending End If ’ Save the new sort column and give it an image. m_SelectedColumn = e.Column If Me.Sorting = SortOrder.Descending Then Me.Columns(m_SelectedColumn).ImageKey = _ "sortDescending.bmp" Else Me.Columns(m_SelectedColumn).ImageKey = _ "sortAscending.bmp" End If ’ Resort. Me.Sort() End If End Sub End Class リスト1のコードには、名前空間System.ComponentModelのインポート後に、 <ToolboxBitmap(GetType(SortableListView), _ "tbxSortableListView")> _ Public Class SortableListView SortableListViewコントロールでは、 Public Enum SortStyles SortDefault SortAllColumns SortSelectedColumn End Enum また、ソートに用いる列のインデックスを保持する Public Property SortStyle() As SortStyles Get Return m_SortStyle End Get ... 一方、リスト1の Set(ByVal value As SortStyles) ’ If the current style is SortSelectedColumn, ’ remove the column sort indicator. If m_SortStyle = SortStyles.SortSelectedColumn Then If m_SelectedColumn >= 0 Then Me.Columns(m_SelectedColumn).ImageKey = Nothing m_SelectedColumn = -1 End If End If ... 次に ... ’ Save the new value. m_SortStyle = value Select Case m_SortStyle Case SortStyles.SortDefault Me.ListViewItemSorter = Nothing Case SortStyles.SortAllColumns Me.ListViewItemSorter = New AllColumnSorter() Case SortStyles.SortSelectedColumn Me.ListViewItemSorter = _ New SelectedColumnSorter() End Select ’ Resort. ’ Me.Sort() End Set End Property SortableListViewコントロールで列のソートを行うために必要な最後の部分が、列ヘッダをユーザーがクリックするたびに呼び出される Protected Overrides Sub OnColumnClick( _ ByVal e As System.Windows.Forms.ColumnClickEventArgs) MyBase.OnColumnClick(e) If Me.SortStyle = SortStyles.SortSelectedColumn Then ’ If this is the same sort column, switch the sort order. If e.Column = m_SelectedColumn Then If Me.Sorting = SortOrder.Ascending Then Me.Sorting = SortOrder.Descending Else Me.Sorting = SortOrder.Ascending End If End If ’ Remove the image from the previous sort column. If m_SelectedColumn >= 0 Then Me.Columns(m_SelectedColumn).ImageKey = Nothing End If ’ If we’re not currently sorting, sort ascending. If Me.Sorting = SortOrder.None Then Me.Sorting = SortOrder.Ascending End If ’ Save the new sort column and give it an image. m_SelectedColumn = e.Column If Me.Sorting = SortOrder.Descending Then Me.Columns(m_SelectedColumn).ImageKey = _ "sortDescending.bmp" Else Me.Columns(m_SelectedColumn).ImageKey = _ "sortAscending.bmp" End If ’ Resort. Me.Sort() End If End Sub 直前に選択されていたのと同じ列をユーザーがクリックすると、上記の 次に、新たにクリックされた列を選択した列として保存し、適切なソート方向のインジケータをその列に設定します。 著者による注釈
メインプログラムでは、SortableListViewコントロールの
SmallImageListプロパティにこうした画像を含むImageListコントロールを設定する必要があります。ImageListにおけるこれらの画像名は、「sortDescending.bmp」と「sortAscending.bmp」にします。画像のサイズを16x14ピクセルにすると最適な表示が得られるようです。 全列によるソート処理 最初に述べたように、ListViewコントロールは、デフォルトでは最初の列でしかソートを行いません。コントロールの持つすべての列を使ってListViewItemsをソートするには、1つの選択した列でソートする際に用いたのと同様の方法を使います。つまり、IComparerインターフェイスを実装したクラスを作成し、作成したクラスの新しいインスタンスをコントロールの この新しい行比較用のクラスは、先ほど説明したクラスに非常によく似ています。このクラスも 最初の文字列のソート順序が2番目の文字列より先になる場合、 この比較用クラスと先ほどのクラスとの2つ目の違いは、もう少し複雑です。ListViewコントロールでは、列ヘッダをクリックして左右にドラッグすることで、列そのものの順序を並べ替えることができます。これはこのコントロールの便利な機能の1つなので、無効にしたくはありませんでした。ただ残念ながら、列の順序の変更を許すと、アイテムのソートが非常に難しくなってしまうのです。 例として、今回のサンプルプログラムでAuthor、Year、Pages、Titleという当初の配置順で列を表示した様子を図3に示します。各行は降順でソートされており、アルファベット順では「Tom Holt」という名前が最後になるため、この名前が最初に表示されています。彼の2冊の著書はどちらも2006年出版なので、これらの著書の順序はPages列によって決まります。ページ数は『Earth, Air, and Custard』の方が多いため、降順のソートではこの著書が最初に表示されます。 図4に、同じフォームでYear列のヘッダをAuthor列の左にドラッグした後の状態を示します。ここでは、私の著書『Expert One-on-One Visual Basic 2005 Design and Development』が最初に表示されています。このリストのなかで唯一、2007年に出版されたものだからです。 リストの下の方を見ると、行のソートメソッドによるその他の影響が分かります。例えば、Sam Testの著書のうちページ数が1001のものが彼の他の著書より先に表示されているのは、ページ数が最多でソートの向きが降順だからです。同様に、Sam Testの著書のうちPagesの値がないものが彼の著書のなかで一番後に表示されているのは、降順では空の値が最後になるからです。 列の並べ替えの検出には、ベースクラスの 列の移動こそまだ完了していませんが、 リスト2は、SortableListViewコントロールがユーザーによる並べ替え後の全列の位置を把握するときに使用するコードを示しています。 リスト2
Public Class SortableListView ’ m_SortSubitems(i) is the i-th sub-item ’ in the sort order for all column sorting. Private m_SortSubitems() As Integer = Nothing ’ Initialize the sort item order to the order given by the ’ column headers. Private Sub SetSortSubitems() ReDim m_SortSubitems(Me.Columns.Count - 1) For i As Integer = 0 To Me.Columns.Count - 1 m_SortSubitems(Me.Columns(i).DisplayIndex) = i Next i End Sub ’ The user reordered the columns. Resort. Protected Overrides Sub OnColumnReordered( _ ByVal e As System.Windows.Forms.ColumnReorderedEventArgs) ’ This raises the ColumnReordered event. MyBase.OnColumnReordered(e) ’ If the main program canceled, do nothing. If e.Cancel Then Exit Sub ’ Rebuild the list of sort sub-items. SetSortSubitems() ’ Fix the list up to account for the moved column. MoveArrayItem(m_SortSubitems, e.OldDisplayIndex, _ e.NewDisplayIndex) ’ Resort. Me.Sort() End Sub ’ Move an item from position idx_fr to idx_to. Private Sub MoveArrayItem(ByVal values() As Integer, _ ByVal idx_fr As Integer, ByVal idx_to As Integer) Dim moved_value As Integer = values(idx_fr) Dim num_moved As Integer = Math.Abs(idx_fr - idx_to) If idx_to < idx_fr Then Array.Copy(values, idx_to, values, _ idx_to + 1, num_moved) Else Array.Copy(values, idx_fr + 1, values, _ idx_fr, num_moved) End If values(idx_to) = moved_value End Sub End Class リスト2の SortableListViewコントロールの SortableListViewコントロールは、 このイベントがキャンセルされない場合、SortableListViewコントロールは ’ Return a string representing this item as a ’ null-separated list of the item sub-item values. Private Function ItemString(ByVal listview_item As ListViewItem) _ As String Dim slvw As SortableListView = listview_item.ListView ’ Make sure we have the sort sub-items’ order. If slvw.m_SortSubitems Is Nothing Then slvw.SetSortSubitems() ’ Make an array to hold the sort sub-items’ values. Dim num_cols As Integer = slvw.Columns.Count Dim values(num_cols - 1) As String ’ Build the list of fields in display order. For i As Integer = 0 To slvw.m_SortSubitems.Length - 1 Dim idx As Integer = slvw.m_SortSubitems(i) ’ Get this sub-item’s value. Dim item_value As String = "" If idx < listview_item.SubItems.Count Then item_value = listview_item.SubItems(idx).Text End If ’ Align appropriately. If slvw.Columns(idx).TextAlign = _ HorizontalAlignment.Right _ Then ’ Pad so numeric values sort properly. values(i) = item_value.PadLeft(20) Else values(i) = item_value End If Next i ’ Concatenate the values to build the result. Return String.Join(vbNullChar, values) End Function 先ほどのコードでは、アイテムの列値を保持するために文字列の配列を作成し、空の文字列で各要素を初期化していました。今回のコードでは、 続いて、各列のインデックスを取得します。インデックスがListViewItem.SubItemsコレクション内のオブジェクト数よりも小さいときは、該当するサブアイテムが存在するのでその値が 列が右詰めの場合は、数値のソートが正しく行われるように、値の左側に空白が追加されます。 各フィールドを表す文字列値の作成が終わると、それらは この時点で、ソートを行うためのすべての準備が整ったことになり、残りの処理は自動的に行われます。ユーザーが列の並べ替えを行った場合は、 応用 SortableListViewコントロールのコードには、役に立つテクニックがいくつか含まれています。例えば、ベースクラスのメソッド( もっと重要なのは、 SortableListViewコントロールを習得すれば、どこでも 著者紹介Rod Stephens(Rod Stephens)
10冊以上の書籍と200点以上の雑誌記事の著者にしてコンサルタント。著作の大部分はVisual Basicに関するものである。これまで、修復ディスパッチ、燃料税トラッキング、プロフェッショナルなフットボールトレーニング、廃水処理、地図作成、チケット販売などの種々雑多なアプリケーションに従事してきた。彼のVB Helper Webサイトは、1か月に7百万以上のヒットを記録しており、Visual Basicプログラマ向けに3つのニューズレターと何千ものヒントや例を提供している。
|