(32) VB2005:ListViewに非表示列を持たせたい
検証環境 Dell PRECISION340(Pen4-2.0GHz/512MB)
Win5.1(Build 2600.xpsp_sp2_gdr.050301-1519)/VS.NET2005(8.0.50727.42(RTM.050727-4200)) & .NET Framework2.0(2.0.50727)
ソーシャルネットワーキングサイト(SNS)のmixiってところがありまして。
そこの日記で「ListViewに非表示列を持たせたいよー」とか愚痴ったら、意外にいろんな方からご意見をいただいておもしろいディスカッション(つか一方的に教えてもらいまくったというか)になりましたので、ちょっとまとめてみることにしました。
ことの発端はListViewコントロール。
簡単に表形式でデータを表示したい時に、これを .View = Details にしてよく使うんですが。
今回、主な項目だけを表示しておき、行を選択するとその明細を横に表示するような機能を作ってみたくなったんですね。
しかし表示する項目は重複要素が多く、ユニークなキーにはならないんです。
ってことは、せっかくHashTableで明細情報を保持しても、選択行からHashTableの特定のItemへひもづける方法がない、ということになります。
そこでキー引きできるようなユニークなキーをListViewの非表示列に保持しておきたいなーと思ったんですよ。
ところがどう調べても列を非表示にするプロパティもメソッドも見つからないよどーしよーと。
したっけ知恵者な方々からのナイスアイデアやらアドバイスやらが続々と。
てことで、以下サンプルを交えてのまとめです。
その1 (初級者向け)
「非表示にできないなら列幅を0にして見えなくしちゃえ」という発想ですね。
正直これが一番簡単だと思います。
実際に作ってみるとこんな感じで。

列タイトルが「1列目」「2列目」「4列目」になっているのがミソで、3列目の列幅を0にしているんですね。

表の初期値は、めんどくさいのでForm_Loadイベントの中でアルファベット大文字をキャラクタコードとループで突っ込んであります。3列目だけは区別をつけるために小文字にしてあります。
Private Sub Form1_Load( _で、好きな行をクリックすると、隠れている3列目の内容が下のLabelに表示されると。
ByVal sender As System.Object _
, ByVal e As System.EventArgs _
) Handles MyBase.Load
'*** 初期化
ListView1.Items.Clear()
For i As Integer = &H41 To &H47
ListView1.Items.Add( _
New ListViewItem(New String() {New String(Chr(i), 3) _
, New String(Chr(i), 11) _
, New String(Chr(i + &H20), 11) _
, New String(Chr(i), 5)}))
Next
End Sub
Private Sub ListView1_SelectedIndexChanged( _
ByVal sender As System.Object _
, ByVal e As System.EventArgs _
) Handles ListView1.SelectedIndexChanged
If ListView1.SelectedItems.Count > 0 Then
Label1.Text = ListView1.SelectedItems(0).SubItems(2).Text
End If
End Sub

おお、いい感じだ。
いい感じなんですけど、これ、列見出しの2列目と4列目の間をドラッグすると、隠れた3列目が見えてきちゃうんですよ。

ああっ、なんともかっこ悪い。
見せたくないからこそ非表示にしたのであって、なんかの拍子にぞろりと隠しデータが表示されたらびっくりします。
この操作は2列目の列幅を広げたい時によくやりますので、実際に充分ありえる操作ですよね。
その2 (初級者向け改)
てことで、 .Width = 0 で非表示化した次は見出しドラッグによる列幅の変更を禁止したいということになります。
これは、ColumnWidthChangingイベントで列幅の変更を検出し、むりやりもう一度列幅を0にしてやればいい、ということがわかりました。
Private Sub ListView2_ColumnWidthChanging( _実際に操作していただくとわかりますが、イベント発生ごとに抑え込んでいるなんてまったくわからないくらいにびくともしなくなります。
ByVal sender As Object _
, ByVal e As System.Windows.Forms.ColumnWidthChangingEventArgs _
) Handles ListView2.ColumnWidthChanging
If e.ColumnIndex = 2 Then
e.NewWidth = 0
e.Cancel = True
End If
End Sub

実用上はこれで十分ですね。
…わざわざ「実用上は」と言ったのは、実はマウスカーソルの挙動がちょっと変だからです。
2列目と4列目の見出しの間、ドラッグで2列目の幅を増減できる位置までマウスカーソルを持っていくと、カーソルが左右の矢印(左右へのスライドが可能、との意味を持つ)になります。

この場合は、2列目の幅を増減できるわけです。
で、ここから少しだけ右へマウスカーソルを移動させると、列幅0で非表示になっている列の幅の増減ができるマークに変わってしまうんですよ。

びみょーな違いですが、2列目増減の場合は左右矢印を貫く縦線が1本、3列目増減の場合は2本になります。
しかし2本線左右矢印のマウスカーソルに変わっても、列幅の変更はコードで抑え込んでいますので、実際にドラッグしても何も起こりません。
つまり、画面に表示される補助情報と実際の動作が食い違うという状態になってしまうわけです。
これはユーザにとまどいを感じさせますし、なによりここに隠し列があるのがバレバレで、これはこれでかっこ悪いす。
その3 (ちょっと中級者向け)
以上のような推敲の末、ゆっきさん(荒野の喫茶店)に教えていただいたベストリザルト。
ListViewの行はListViewItemってクラスですので、これに表示と関係ない変数を追加した派生クラスを作ってそっちを使っちゃえばいいんだと。
Public Class ListViewItemEXこんな感じで。
Inherits System.Windows.Forms.ListViewItem
Dim KeyData As String
Public Property Key() As String
Get
Return KeyData
End Get
Set(ByVal value As String)
KeyData = value
End Set
End Property
Public Sub New(ByVal subItems As String(), ByVal Key As String)
MyBase.New(subItems)
KeyData = Key
End Sub
End Class
InheritsでListViewItemからの派生を指定して、Newの中にMyBase.NewでListViewItemオリジナルの構造を引き継いで、ついでにString変数Keyも追加して。
Keyは内部的には変数KeyDataで保持しておいて、プロパティでGet/Setできるような出入り口を用意して。
ListViewItemの派生クラスですので、これはこのままふつーにListViewに突っ込めます。
Private Sub Form1_Load( _参照する時はListView.SelectedItems(0)でいいんですけど、これはListViewItemクラスですのでいったんListViewItemEXに型変換してやってからKeyプロパティを見てやればいいということになります。
ByVal sender As System.Object _
, ByVal e As System.EventArgs _
) Handles MyBase.Load
'*** 初期化
ListView1.Items.Clear()
For i As Integer = &H41 To &H47
ListView1.Items.Add( _
New ListViewItem(New String() {New String(Chr(i), 3) _
, New String(Chr(i), 11) _
, New String(Chr(i + &H20), 11) _
, New String(Chr(i), 5)}))
Next
ListView2.Items.Clear()
For i As Integer = &H41 To &H47
ListView2.Items.Add( _
New ListViewItem(New String() {New String(Chr(i), 3) _
, New String(Chr(i), 11) _
, New String(Chr(i + &H20), 11) _
, New String(Chr(i), 5)}))
Next
ListView3.Items.Clear()
For i As Integer = &H41 To &H47
Dim ItemEx As New ListViewItemEX( _
New String() {New String(Chr(i), 3) _
, New String(Chr(i), 11) _
, New String(Chr(i), 5)} _
, New String(Chr(i + &H20), 11))
ListView3.Items.Add(ItemEx)
Next
End Sub
Private Sub ListView3_SelectedIndexChanged( _非表示列がそもそもなく、でも行選択すると表の中にはないデータがLabelに表示される機能が、実にすっきり実装できます。
ByVal sender As System.Object _
, ByVal e As System.EventArgs _
) Handles ListView3.SelectedIndexChanged
If ListView3.SelectedItems.Count > 0 Then
Label3.Text = CType(ListView3.SelectedItems(0), ListViewItemEX).Key
End If
End Sub

わーい。
VBは、2005になってかなりお気楽プログラミングができるようになってきていて、オブジェクト指向プログラミングとかの理解なしに済む簡単ルールでそこそこイケるのではないかと、そっち方面をいろいろ模索していたわけですが。
.NET Frameworkから提供されるコントロールは他言語と共通で、ほしい機能は自分でクラス拡張してこなしていくのがスタンダードな手法であるなら、「簡単ルール」なんて無理だよなぁと実感しました。
出来合いのコントロールの組み合わせと裏でのチョロっとコーディングで楽しいことができるよーと言いたかったんですけどね。やはり素直に動くものを作ろうとするなら、クラスの理解と応用は避けられそうにありません。
やはり.NETになって、RADツールは死んだのかもしれません。くそぉ。
元ネタの日記ディスカッションでは、羅樹さん、ゆっきさん、[弁士]さん、Kok.Wishさん(スレッド参加順)に大変お世話になりました。どうもありがとうございました。
[弁士]さんご提供の手法は、まだちょい自分の中で整理がついていないので、今回はペンディングにさせてください。いずれきちんと応用できるようになったら、本エントリ末に追記したいと思います。
トラックバック
このエントリーのトラックバックURL:
http://salv.miscnotes.com/mt/mt-tb.cgi/558

コメント
つ その3
System.Windows.Forms.ListViewItemには
Public Property Tag As Object
というプロパティがあって、MSDNを読む限り勝手に使っていいようです。
System.Windows.Forms.Controlにも定義されているので応用範囲がひろいです。
投稿者: とおりすがり | 2006年06月28日 12:30
> Public Property Tag As Object
おおっ、その手もありましたね。
統一したフォーマットの構造体でも入れてやれば、これはこれで何でもありだよなぁ。
フォロー、ありがとうございます(^^)ノ。
投稿者: さるべーじ | 2006年06月28日 14:08
> 統一したフォーマットの構造体でも入れてやれば、これはこれで何でもありだよなぁ。
Objectからのダウンキャストが必要になってしまいますね。
継承なら、キーの型が決まるので何かとよさげです。
継承が嫌いなので、脊髄反射してしまいました、反省。
投稿者: さっきのとおりすがり | 2006年06月28日 15:55
まぁ本エントリでは、「どうすべき」ではなく「こんな方法もあんな方法もあるんですぜお客さん」ってのを言いたかったんですね。
個々の状況やら都合やら仕様やら嗜好やらで選択肢も最適解も変わると思うので。
てことで本文で言及しそこなったアプローチをご提示くださったこと、大変感謝しております。なんも反省するようなことじゃないすよ?
投稿者: さるべーじ | 2006年06月28日 16:20
お言葉恐縮です。
そういうことなら調子にのって、このようなものはいかがでしょう
手抜きコードですが
' formレベルで
Private h As Hashtable = New Hashtable
...
' 値を設定
Dim lvi As ListViewItem
...
h(lvi) = "hogehoge"
...
' 取得するなら対象のListViewItemを渡す
h(listView1.SelectedItems(0))
ListViewには同じListViewItemは入れられないので、大丈夫なはず
投稿者: とおりすがり | 2006年06月28日 17:26