japan.internet.comThe Internet & IT Network
RSS
  • ニュース
  • コラム
  • リサーチ
  • ヘッドライン
  • 特集
  • ブログ
  • プレスリリース
  • 専門チャンネル
  • イベント
  • ランキング
  • ニュースメール
2008年10月8日
文字サイズ文字サイズ小文字サイズ中文字サイズ大
デベロッパー2007年7月17日 10:00

ListViewコントロールに柔軟なソート機能を追加する

海外海外internet.com発の記事
  • このエントリーを含むはてなブックマーク
  • この記事をクリップ!
  • Buzzurlにブックマーク
  • Yahoo!ブックマークに登録
  • newsing it!

はじめに

 私は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列)に上向きの矢印が表示されています。これは、その列が昇順にソートされていることを示しています(矢印は最小のアイテムの方向を指します)。列ヘッダをクリックすると、降順のソート処理に切り換わります。

図1 ページ数によるソート結果。SortableListViewコントロールのデータがPages列で昇順にソートされている。
図1 ページ数によるソート結果。SortableListViewコントロールのデータがPages列で昇順にソートされている。

 別の列ヘッダをクリックすると、Windows Explorerと同じように、その列を基準としてソートが行われます。図2に、Title列を2回クリックした後の同じフォームの状態を示します。1回目のクリックでこの列によるソートが実行され、2回目のクリックで降順のソートに変わっています。

図2 タイトルによるソート結果。同じくSortableListViewコントロールにより、Title列で降順にソートされている。
図2 タイトルによるソート結果。同じくSortableListViewコントロールにより、Title列で降順にソートされている。

 以下に、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"よりも前に来ることになります。

著者による注釈
 データによっては、こうしたチェックをもっと念入りに行う必要があるかもしれません。例えば、このコードでは、右詰めの列にはせいぜい20桁の単純な数値しか含まれていないと仮定しています。つまり、このコードでは、"1e10"が"1E+10"と同じであることや、"April 1"が"January 1"よりも後になることは判断できないのです。ただし、一般的な考え方を示すサンプルコードにはなっています。

SelectedColumnSorterクラスを使う

 これで、SortableListViewコントロールはSelectedColumnSorterクラスを使って特定の列によるソートを実行できる状態になりました。リスト1に、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属性と、SortableListViewコントロールがListViewを継承していることを示すClassステートメントが記されています。

<ToolboxBitmap(GetType(SortableListView), _
    "tbxSortableListView")> _
Public Class SortableListView

 SortableListViewコントロールでは、SortStyles列挙体を定義しています。これは、デフォルトのメソッドを使ったソート、すべての列によるソート、選択した列によるソートのどれを行うかを指定するためのものです。

Public Enum SortStyles
   SortDefault
   SortAllColumns
   SortSelectedColumn
End Enum

 また、ソートに用いる列のインデックスを保持するm_SelectedColumn変数も宣言されています。

 SortStyleプロパティは、SortStyles列挙体の値の取得および設定に用います。Getプロシージャは、m_SortStyle変数の保持する値を返すだけのものです。

Public Property SortStyle() As SortStyles
   Get
      Return m_SortStyle
   End Get
   ...

 一方、リスト1のSetプロシージャは、もう少し複雑な働きをします。まず、現時点でこのコントロールが「選択した列によるソート」を行おうとしているかどうかをチェックします。その場合は、その列に画像が一切表示されないように、列のImageKeyプロパティをNothingにして、ソート方向インジケータ(ソート方向を示す矢印)を取り除きます。

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
   ...

 次にSetプロシージャは、新しいソート形式を保存したうえで、Select Caseステートメントによって適切な処理に分岐します。新しいソート形式がSortDefaultのときは、ListViewItemSorterプロパティにNothingが設定され、ListViewのデフォルトのソート処理がそのまま実行されることになります。ソート形式がSortAllColumnsのときは、ListViewItemSorterプロパティに新しいAllColumnSorterオブジェクト(後述)が設定されます。ソート形式がSortSelectedColumnのときは、ListViewItemSorterプロパティに新たなSelectedColumnSorterが設定されます。

   ...
   ’ 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

 Setプロシージャでは、この時点でSortableListViewコントロールのSortメソッドを呼び出し、この新たなソータを使って各アイテムを再ソートすることも可能です。ただし、このやり方は分かりにくいので、サンプルプログラムではその部分をコメントアウトしてメインプログラムからSortableListViewコントロールのSortメソッドを呼び出すようにしています。

 SortableListViewコントロールで列のソートを行うために必要な最後の部分が、列ヘッダをユーザーがクリックするたびに呼び出されるOnColumnClickメソッドです。このメソッドのコードは、ベースクラスのOnColumnClickメソッドの呼び出しから始まります。続いて、現在のソート形式がSortSelectedColumnかどうかの判断を行います。

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

 直前に選択されていたのと同じ列をユーザーがクリックすると、上記のOnColumnClickメソッドのコードによってSortableListViewコントロールのソート方向が切り換わり、ソート方向インジケータが消去されます。現在のソート方向が選択されていない場合は、デフォルトである昇順が用いられます。

 次に、新たにクリックされた列を選択した列として保存し、適切なソート方向のインジケータをその列に設定します。

著者による注釈
 メインプログラムでは、SortableListViewコントロールのSmallImageListプロパティにこうした画像を含むImageListコントロールを設定する必要があります。ImageListにおけるこれらの画像名は、「sortDescending.bmp」と「sortAscending.bmp」にします。画像のサイズを16x14ピクセルにすると最適な表示が得られるようです。

 OnColumnClickのコードでは、最後にSortableListViewコントロールのSortメソッドを呼び出し、新たに選択した列とソート方向に従ってアイテムの再ソートを行います。

全列によるソート処理

 最初に述べたように、ListViewコントロールは、デフォルトでは最初の列でしかソートを行いません。コントロールの持つすべての列を使ってListViewItemsをソートするには、1つの選択した列でソートする際に用いたのと同様の方法を使います。つまり、IComparerインターフェイスを実装したクラスを作成し、作成したクラスの新しいインスタンスをコントロールのListViewItemSorterプロパティに設定するわけです。

 この新しい行比較用のクラスは、先ほど説明したクラスに非常によく似ています。このクラスもItemStringメソッドを使って各ListViewItemの文字列表現を作成し、String.Compareを使ってそれらの比較を行います。しかし、このクラスと先に説明したクラスとの間には大きな違いが2つあります。単純な違いは、新しいクラスでは選択した列だけでなくすべての列を比較できるように、ItemStringメソッドに別のコードを用いていることです。新しいバージョンのメソッドでは、すべてのアイテムの値をnull文字で区切ってつなげた1つの文字列を作成することによって、String.Compareを使った2つのアイテムの比較を容易にしています。

 最初の文字列のソート順序が2番目の文字列より先になる場合、String.Compareメソッドは-1を返します。フィールド間の区切りとなるnull文字はASCIIコードが0なので、アルファベット順では他のどんな文字よりも前に来ます。これは、最初の部分が一致していて長さの異なる2つの文字列がある場合、短いフィールドを含む文字列の方がアルファベット順で先になることを意味します(もう一方のフィールドの次の文字が何であろうと、アルファベット順でnull文字の方が前に来るからです)。2つのフィールドが完全に一致する場合は、両文字列のそれ以降の部分によってどちらが先になるかが決まります。

 この比較用クラスと先ほどのクラスとの2つ目の違いは、もう少し複雑です。ListViewコントロールでは、列ヘッダをクリックして左右にドラッグすることで、列そのものの順序を並べ替えることができます。これはこのコントロールの便利な機能の1つなので、無効にしたくはありませんでした。ただ残念ながら、列の順序の変更を許すと、アイテムのソートが非常に難しくなってしまうのです。

 例として、今回のサンプルプログラムでAuthor、Year、Pages、Titleという当初の配置順で列を表示した様子を図3に示します。各行は降順でソートされており、アルファベット順では「Tom Holt」という名前が最後になるため、この名前が最初に表示されています。彼の2冊の著書はどちらも2006年出版なので、これらの著書の順序はPages列によって決まります。ページ数は『Earth, Air, and Custard』の方が多いため、降順のソートではこの著書が最初に表示されます。

図3 行のソート。すべての列を対象とした降順のソートが行われている。
図3 行のソート。すべての列を対象とした降順のソートが行われている。

 図4に、同じフォームでYear列のヘッダをAuthor列の左にドラッグした後の状態を示します。ここでは、私の著書『Expert One-on-One Visual Basic 2005 Design and Development』が最初に表示されています。このリストのなかで唯一、2007年に出版されたものだからです。

図4 列の並べ替え。Year列がAuthor列の左側にドラッグされたため、最初にYear列でソートされている。
図4 列の並べ替え。Year列がAuthor列の左側にドラッグされたため、最初にYear列でソートされている。

 リストの下の方を見ると、行のソートメソッドによるその他の影響が分かります。例えば、Sam Testの著書のうちページ数が1001のものが彼の他の著書より先に表示されているのは、ページ数が最多でソートの向きが降順だからです。同様に、Sam Testの著書のうちPagesの値がないものが彼の著書のなかで一番後に表示されているのは、降順では空の値が最後になるからです。

 列の並べ替えの検出には、ベースクラスのOnColumnReorderedメソッドがうってつけに思えるかもしれませんが、実はそうでもありません。このメソッドはまるで列の並べ替え完了後に実行されるかのような名前になっていますが、実際のところ、ベースクラスは列の移動前にこのメソッドを呼び出して対応するColumnReorderedイベントをトリガーさせるため、コントロールが持つ列の新しい並び順まではまだ分かりません(本来、このメソッドとイベントの名前は、それぞれ「OnColumnReordering」と「ColumnReordering」にすべきです。我々の考えでは、本当の意味でのOnColumnReorderedとColumnReordered、あるいはOnColumnReorderStartとOnColumnReorderEndが実現されるとさまざまなメリットがあるはずなのですが)。

 列の移動こそまだ完了していませんが、OnColumnReorderedメソッド^^は、移動することになる列とその移動先は教えてくれます。そのため、少し処理を追加しさえすれば、移動完了後のすべての列の位置が分かります。

 リスト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のm_SortSubitems配列には、SortableListViewコントロールの各アイテムがどんな順序でソートに用いられるかを示すリストが格納されています。例えば、このコントロールの列がAuthor、Year、Pages、Titleという順で並んでいるとします。この場合、各列は0、1、2、3というインデックスを持ちます。ここで、ユーザーがこれらの列をYear、Author、Title、Pagesという順に並び替えたとします。すると、m_SortSubitemsでは、これらの列のインデックスが新しい表示順に従った値(1、0、3、2)に変わるわけです。

 SortableListViewコントロールのSetSortSubitemsメソッドは、このコントロールの現在の列の順序を使ってm_SortSubitems配列を初期化します。各列のループ処理により、その列のDisplayIndexプロパティに対応したm_SortSubitemsエントリのインデックスを取得します。先ほどの例では、列の並べ替えが完了したときにAuthor列(列0)が2番目の列(表示位置1)になるため、このコードによってm_SortSubitems(1)=0に設定されます。同様に、Year列(列1)は表示位置が0になるため、このコードによってm_SortSubitems(0)=1に設定されます。

 SortableListViewコントロールは、m_SortSubitemsを最新の状態に保つために、ベースクラスのOnColumnReorderedメソッドをオーバーライドしています。SortableListViewのコードが呼び出すのはベースクラスのメソッドであるため、通常の動作を実行することができます。この動作にはColumnReorderedイベントの発生が含まれるので、メインプログラムはこのイベントに応答し、場合によってはe.CancelをTrue(真)に設定して列の並べ替えを取り消すことができます。e.Cancelが真の場合、OnColumnReorderedメソッドはそれ以上の動作を行わずに終了します。

 このイベントがキャンセルされない場合、SortableListViewコントロールはSetSortSubitemsを呼び出してm_SortSubitems配列を初期化します。続いて、MoveArrayItemメソッドを使って、移動する列のインデックスをこの配列内の新しい位置に移動させ、配列のSortメソッドを呼び出して新しい列の並び順に基づいて各アイテムの並べ替えを行います。

 MoveArrayItemは、整数値を配列内のある場所から別の場所へと移動させるだけのメソッドです。このメソッドは、まず移動させる値を保持し、その新しい位置と古い位置との間にある各要素の位置をArray.Copyを使って1つずつシフトさせたうえで、移動させる値を新しい位置に再び挿入します。MoveArrayItemは、それ単体でも便利なメソッドです。

 OnColumnReorderedメソッドによってSortableListViewコントロールのm_SortSubitems配列の更新が終わったら、比較用クラスのItemStringメソッドを使ってこの配列から各要素の比較用文字列を作成できます。以下に、新しいバージョンのItemStringのコードを示します。

’ 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

 先ほどのコードでは、アイテムの列値を保持するために文字列の配列を作成し、空の文字列で各要素を初期化していました。今回のコードでは、m_SortSubitems配列の各要素をループで処理しているため、現在コントロール上に現れている順序で各要素を扱います。

 続いて、各列のインデックスを取得します。インデックスがListViewItem.SubItemsコレクション内のオブジェクト数よりも小さいときは、該当するサブアイテムが存在するのでその値がitem_value変数に保存されます。インデックスがSubItemsの要素数と同じかそれより大きいときは、このアイテムには対応するサブアイテムがない(例えば、Titleエントリが存在しない)のでアイテムの値として空の文字列が使われます。

 列が右詰めの場合は、数値のソートが正しく行われるように、値の左側に空白が追加されます。

 各フィールドを表す文字列値の作成が終わると、それらはString.Joinによって、null文字で区切られた1つの文字列に連結され、その結果が返されます。

 この時点で、ソートを行うためのすべての準備が整ったことになり、残りの処理は自動的に行われます。ユーザーが列の並べ替えを行った場合は、OnColumnReorderedメソッドにより、m_SortSubitems配列が再構築され、SortableListViewコントロールのSortメソッドが呼び出されます。Sortメソッドは、比較用オブジェクトを使ってアイテムのソートを行います。この際、前述のItemStringメソッドが使われ、このメソッドはm_SortSubitems配列を使って正しい列順でアイテムの文字列を作成します。その結果、現在表示されている列の順序を考慮してソートされたアイテムのリストが得られます。

応用

 SortableListViewコントロールのコードには、役に立つテクニックがいくつか含まれています。例えば、ベースクラスのメソッド(OnColumnClickおよびOnColumnReordered)のオーバーライド、配列内の要素の移動(MoveArrayItemメソッド)、ListViewコントロールにおける現在の列表示順の判断といった部分です。

 もっと重要なのは、IComparerクラスを使ってカスタムのソート順を実現する方法の部分です。比較用オブジェクトを使ってさまざまなソート順に対応できるオブジェクトは、ListViewコントロールだけではありません。例えば、IComparerオブジェクトを使ってTreeViewコントロールまたはDataGridViewコントロール内のアイテムをソートすることもできます。List、ArrayList、Hashtable、NameValueCollection、SortedDictionaryといったさまざまなコレクションクラスも、IComparerオブジェクトによるソート処理に対応しています。

 SortableListViewコントロールを習得すれば、どこでもIComparerオブジェクトを使いこなせるようになります。例えば、いろいろなオブジェクトを使って、顧客データを名前、ID、未払い残高、支払い期限などの基準でソートすることが可能になります。

著者紹介

Rod Stephens(Rod Stephens)
10冊以上の書籍と200点以上の雑誌記事の著者にしてコンサルタント。著作の大部分はVisual Basicに関するものである。これまで、修復ディスパッチ、燃料税トラッキング、プロフェッショナルなフットボールトレーニング、廃水処理、地図作成、チケット販売などの種々雑多なアプリケーションに従事してきた。彼のVB Helper Webサイトは、1か月に7百万以上のヒットを記録しており、Visual Basicプログラマ向けに3つのニューズレターと何千ものヒントや例を提供している。
最新トップニュース
  • 国内DNP、多機能 IC カードの新タイプを開発(Webテクノロジー 10月8日 17:20)
    大日本印刷株式会社(DNP)は2008年10月8日、Java Card 版 FeliCa デュアルインターフェイスカードの新タイプを開発し、金融機関向けに10月中旬より販売を開始すると発表した。
  • 株式会社サンゼロミニッツは2008年10月8日、同社が運営するタウン情報検索サイト「30min.」のランチマップ機能が、米国時間10月7日に公開された Firefox3 のアドオン「Geode」に対応した、と発表した。
  • エアネットの「マネージド専用サーバサービス」で、トランスウエアの高機能 Web メールソフトウェア「Active! mail 6」を提供する。
  • 株式会社アイシェアは携帯電話ユーザーに対し「健全サイトに関する意識調査」を実施、2008年10月8日、調査結果を発表した。
  • 国内「goo」月間キーワードランキング(2008/9/1〜9/30)(Webマーケティング 10月8日 16:30)
    NTT レゾナント株式会社は2008年10月7日、インターネットポータルサイト「goo」で提供中の、各種ランキングを紹介するコーナー「goo ランキング」にて、2008年9月1日から9月30日までの goo で検索されたキーワードの検索回数に基づく「2008年9月 月間キーワードランキング」を発表した。
Graphic Design Forum
【Graphic Design Forum】
活気に満ちた誕生日をどうぞ (10月8日)
データメーション
【データメーション】
Google 版酒気検知機能が間もなく登場(10月8日)
ベンチャー専門家の目利きブログ「なぜこの企業は伸びるのか?」
【ベンチャー専門家の目利きブログ「なぜこの企業は伸びるのか?」】
「ITを活用し、消費生活における意思決定の支援、悩み・迷いを解決する!」/株式会社ALBERT(10月8日)
エンジニアの独り言
【エンジニアの独り言】
得体の知れない情報(?)との向き合い方(9月17日)
最新テクノロジーの意外な処方箋
【最新テクノロジーの意外な処方箋】
昆虫と退屈なことについて(9月16日)
気になるトレンド用語
気になるトレンド用語
はてなブックマークが変わる!そもそもブラウザのお気に入りと何が違うの?(10月8日)
e-Japan 先端テクノロジー解説
e-Japan 先端テクノロジー解説
行政サービスのマルチチャネル化について(10月8日)
ウチのサイトを SEO
ウチのサイトを SEO
ちゃんと title つけていますか?(10月8日)
百式のネットビジネス研究
百式のネットビジネス研究
Blog 記事の編集を読者に任せることができる「gooseGrade」(10月8日)
「IT の耳」
「IT の耳」
【書評】ニコ動から RMT まで〜『人はなぜ形のないものを買うのか―仮想世界のビジネスモデル』(10月7日)
DevX
DevX
アジャイルソフトウェアプロジェクトを管理する(10月7日)
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
SEって、デジタル製品は判官びいきで選ぶよね?(10月7日)
アイレップの SEM フロンティア
アイレップの SEM フロンティア
フル CSS でサイトを構築する SEO のメリット(10月7日)
モバイルSEO@フラクタリスト
モバイルSEO@フラクタリスト
応用的な SEO 施策(3)(10月6日)
サーチからはじまるインタラクティブエージェンシー
サーチからはじまるインタラクティブエージェンシー
DB マーケティングと Web マーケティング 〜ビールとオムツの伝説から〜(10月6日)
海外のインターネットコムアメリカ韓国ドイツトルコ
Copyright 2008 Jupitermedia Corporation All Rights Reserved.http://www.internet.com/