Top > Programmingとか > VB / VB.NET > 泥Tips

(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にして見えなくしちゃえ」という発想ですね。
正直これが一番簡単だと思います。

実際に作ってみるとこんな感じで。

mud32-01.png

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

mud32-02.png

表の初期値は、めんどくさいのでForm_Loadイベントの中でアルファベット大文字をキャラクタコードとループで突っ込んであります。3列目だけは区別をつけるために小文字にしてあります。

Private Sub Form1_Load( _
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
で、好きな行をクリックすると、隠れている3列目の内容が下のLabelに表示されると。
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
mud32-03.png

おお、いい感じだ。

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

mud32-04.png

ああっ、なんともかっこ悪い。
見せたくないからこそ非表示にしたのであって、なんかの拍子にぞろりと隠しデータが表示されたらびっくりします。
この操作は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
実際に操作していただくとわかりますが、イベント発生ごとに抑え込んでいるなんてまったくわからないくらいにびくともしなくなります。

mud32-05.png

実用上はこれで十分ですね。

…わざわざ「実用上は」と言ったのは、実はマウスカーソルの挙動がちょっと変だからです。

2列目と4列目の見出しの間、ドラッグで2列目の幅を増減できる位置までマウスカーソルを持っていくと、カーソルが左右の矢印(左右へのスライドが可能、との意味を持つ)になります。

mud32-06.png

この場合は、2列目の幅を増減できるわけです。

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

mud32-07.png

びみょーな違いですが、2列目増減の場合は左右矢印を貫く縦線が1本、3列目増減の場合は2本になります。
しかし2本線左右矢印のマウスカーソルに変わっても、列幅の変更はコードで抑え込んでいますので、実際にドラッグしても何も起こりません。

つまり、画面に表示される補助情報と実際の動作が食い違うという状態になってしまうわけです。

これはユーザにとまどいを感じさせますし、なによりここに隠し列があるのがバレバレで、これはこれでかっこ悪いす。



その3 (ちょっと中級者向け)

以上のような推敲の末、ゆっきさん(荒野の喫茶店)に教えていただいたベストリザルト。

ListViewの行に、表示させない変数を追加しちゃえ。

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( _
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
参照する時はListView.SelectedItems(0)でいいんですけど、これはListViewItemクラスですのでいったんListViewItemEXに型変換してやってからKeyプロパティを見てやればいいということになります。
Private Sub ListView3_SelectedIndexChanged( _
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
非表示列がそもそもなく、でも行選択すると表の中にはないデータがLabelに表示される機能が、実にすっきり実装できます。

mud32-08.png

わーい。



VBは、2005になってかなりお気楽プログラミングができるようになってきていて、オブジェクト指向プログラミングとかの理解なしに済む簡単ルールでそこそこイケるのではないかと、そっち方面をいろいろ模索していたわけですが。
.NET Frameworkから提供されるコントロールは他言語と共通で、ほしい機能は自分でクラス拡張してこなしていくのがスタンダードな手法であるなら、「簡単ルール」なんて無理だよなぁと実感しました。
出来合いのコントロールの組み合わせと裏でのチョロっとコーディングで楽しいことができるよーと言いたかったんですけどね。やはり素直に動くものを作ろうとするなら、クラスの理解と応用は避けられそうにありません。

やはり.NETになって、RADツールは死んだのかもしれません。くそぉ。



元ネタの日記ディスカッションでは、羅樹さん、ゆっきさん、[弁士]さん、Kok.Wishさん(スレッド参加順)に大変お世話になりました。どうもありがとうございました。
[弁士]さんご提供の手法は、まだちょい自分の中で整理がついていないので、今回はペンディングにさせてください。いずれきちんと応用できるようになったら、本エントリ末に追記したいと思います。

動作検証に使ったPgはこちら→

トラックバック

このエントリーのトラックバック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にも定義されているので応用範囲がひろいです。

> Public Property Tag As Object

おおっ、その手もありましたね。
統一したフォーマットの構造体でも入れてやれば、これはこれで何でもありだよなぁ。
フォロー、ありがとうございます(^^)ノ。

> 統一したフォーマットの構造体でも入れてやれば、これはこれで何でもありだよなぁ。

Objectからのダウンキャストが必要になってしまいますね。
継承なら、キーの型が決まるので何かとよさげです。
継承が嫌いなので、脊髄反射してしまいました、反省。

まぁ本エントリでは、「どうすべき」ではなく「こんな方法もあんな方法もあるんですぜお客さん」ってのを言いたかったんですね。
個々の状況やら都合やら仕様やら嗜好やらで選択肢も最適解も変わると思うので。

てことで本文で言及しそこなったアプローチをご提示くださったこと、大変感謝しております。なんも反省するようなことじゃないすよ?

お言葉恐縮です。

そういうことなら調子にのって、このようなものはいかがでしょう
手抜きコードですが

' formレベルで
Private h As Hashtable = New Hashtable
...

' 値を設定
Dim lvi As ListViewItem
...
h(lvi) = "hogehoge"
...

' 取得するなら対象のListViewItemを渡す
h(listView1.SelectedItems(0))

ListViewには同じListViewItemは入れられないので、大丈夫なはず

コメントを投稿