2006年04月14日

ListView とアイテムとフォーカスと

以前書いた C#とNTFSストリームの甘くない関係 ですが、今のところ五本の指に入るぐらいの人気(笑)にも関わらず内容はひどく寂しいものです。寂しいだけなら良いのですが、なにせ COM のことをほぼ全く理解していなかったときに書いた記事。思いっきり勘違いした上での愚痴ですので今は見るに堪えません。IDispatch と IUnknown をごっちゃにしてるんですよね。そのうち改めて記事にしようと思っています。正直実用は微妙であるという認識が強くなってるんですが。Microsoft が配布してる Dsofile.dll 使えば済む話っぽいんですよね……。

さて、今回は某掲示板から、ListView のフォーカスが移動したときのイベントって存在するのか、という問題です。フォーカスの移動ってのは、Control キー押しながらカーソルキーを押したときに動くアレです。選択状態とは無関係に移動できます。

さて、問題のイベントですが、率直に言って存在しません。

が、もちろん一切存在していないのでは深いレベルでは色々困るわけで、Win32API レベルでは存在します。それが LVN_ITEMCHANGED です。この通知メッセージは、リストビューのアイテムが変更されたときに送られてきます。アイテムの変更にはもちろんフォーカスが当たっているアイテムの変更も含まれます。

難点はこれ、通知メッセージですのでメッセージが親フォームに送られるという点です。これではコントロール単体で完結できません。

しかし幸いなことに、System.Windows.Forms ではこの点をクリアする仕組みが用意されています。それが WM_REFLECT です。値は WM_USER (0x400) + 0x1C00。これはコントロールが原因で発生するメッセージで、かつ親フォームに送られたメッセージについて、親フォームがそのメッセージを該当コントロールに再送信するためのメッセージです。再送信するメッセージの ID は、WM_REFLECT と再送信すべきメッセージの ID を足し合わせたものになります。WM_NOTIFY (0x4E) に対するリフレクトメッセージは 0x204E になりますね。

WM_REFLECT って、VS2002 に付いてる PlatformSDK には入ってないみたいなんですがこれで正式名称正しいんでしょうか?

メッセージの捕獲は問題なさそうですので、あとは LVN_ITEMCHANGED の話……の前に WM_NOTIFY についても少々触れておきましょう。

WM_NOTIFY は名前通り通知メッセージで、基本的にコモンコントロールにおける何らかの変更などの通知のために、コモンコントロールの親ウィンドウに送信されるメッセージです。.NET 的オブジェクト指向に慣れていれば何故わざわざ親ウィンドウに送信するのかという気持ちですが、Win32API を直に使ったコーディングの場合、コントロールの方に送信されたメッセージを処理するのに該当コントロールをサブクラス化する必要が出てくるのでこういう仕様になってるんじゃないかなと思うんですが。詳しくないですけど。

WM_NOTIFY メッセージの LPARAM には NMHDR 構造体が含まれます。実際には通知の種類によって千差万別の構造体が格納されているんですが、それらの構造体はいずれも先頭に NMHDR 構造体を含むので、LPARAM は必ず NMHDR でもあるわけですね。このNMHDR に、対象のコントロールのハンドル、コントロールの ID、そして通知コードが含まれます。.NET 的にはコントロールの識別はハンドルで行えばいいので ID はどうでも良いでしょう。そして通知コードですが、これが例えば LVN_ITEMCHANGED と言うような **N_*** 形式の値になります。ちなみに LVN_ITEMCHANGED は -101。全体にマイナスの値ばっかりです。理由は知りませんけど。

さて、これでどんな通知か判断できたら、あとは通知の内容です。例えば LVN_ITEMCHANGED にしても「変わった」だけではどうしようもありませんからね。何がどう変わったかを教えてくれないと。もちろん通知内容は通知の種類によって様々です(中には通知さえあれば良くて特に内容無しってのものあるでしょう)。NMHDR で触れたように、WM_NOTIFY の LPARAM には通知の種類によって任意の構造体が格納されています。それらはいずれも先頭メンバが NMHDR で、あとは通知の種類によって様々です。例えば LVN_ITEMCHANGED なら NMLISTVIEW 構造体であり、これには変更したアイテムのインデックス・サブアイテムのインデックス・変更後の状態・変更前の状態・何が変更されたか・イベントが発生した位置、と言った情報が格納されています。

.NET でポインタを構造体として扱う場合、主に二通りの手段があります。一つは Marshal.PtrToStructure を使って構造体にコピーする方法。もう一つは C# 限定ですが、unsafe ステートメントを使用して C 言語のように扱う方法です。WM_NOTIFY においては、後者の方が圧倒的にコーディングが楽です。特にメンバの値を変更する必要がある場合、前者では対応できません。後者の難点は、アセンブリレベルで unsafe マークを付けなければならない点でしょう。そこで折衷案。Marshal クラスにはポインタに対して直に値を読み書きするメソッドが用意されています。Read... と Write... の各メソッドです。構造体の仕組みさえ分かっているのなら、これでも問題なく対応できます。コードは読みづらくなりますけど。

ここまでくれば、あとは適当にイベントを配置してやれば完成です。お疲れ様でした。

// C#
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace HongliangSoft.Utilities.Gui {
    public delegate void IndexChangedEventHandler(
                 object sender, IndexChangedEventArgs e);
    public class IndexChangedEventArgs : EventArgs {
        public IndexChangedEventArgs(int index) {
            this.index = index;
        }
        private int index;
        public int Index {get {return index; } }
    }
    public class UsefulListView : ListView {
        public event IndexChangedEventHandler FocusedIndexChanged {
            add {
                this.Events.AddHandler(EventFocusedIndexChanged, value);
            }
            remove {
                this.Events.RemoveHandler(EventFocusedIndexChanged, value);
            }
        }
        private static readonly object EventFocusedIndexChanged = new object();
        protected virtual void OnFocusedIndexChanged(IndexChangedEventArgs e) {
            IndexChangedEventHandler handler
                = this.Events[EventFocusedIndexChanged] as IndexChangedEventHandler;
            if (handler != null)
                handler(this, e);
        }
        protected override void WndProc(ref Message m) {
            // LVN_ITEMCHANGED は親フォームに WM_NOTIFY(0x4E)で送信され、
            //    親フォームはそれを該当コントロールに
            //    WM_REFLECT(WM_USER(0x400)+0x1C00) を追加したメッセージコードで通知する。
            const int ReflectedItemChanged = 0x400 + 0x1c00 + 0x4e;
            // LVN_ITEMCHANGED
            const int ItemChanged = -101;
            // NMHDR / NMLISTVIEW 構造体におけるメンバのオフセット(先頭の hdr 除く)
            const int OffsetCode = 4, OffsetIndex = 8, OffsetState = 16;
            if (m.Msg == ReflectedItemChanged) {
                // LPARAM を NMHDR に見立てて通知コード(code)を取得
                int code = Marshal.ReadInt32(m.LParam, IntPtr.Size + OffsetCode);
                if (code == ItemChanged) {
                    // LPARAM を NMLISTVIEW に見立ててアイテムの状態(uNewState)を取得
                    int state = Marshal.ReadInt32(m.LParam, IntPtr.Size + OffsetState);
                    const int Focused = 1;
                    // 最下位ビットが立っている場合フォーカスが存在すると言う意味になる
                    // フォーカスがアイテム 1 からアイテム 2 に動いた場合、
                    //    ItemChanged は複数回送られる。
                    //    一般的に、アイテム 1 の選択解除、アイテム 1 のフォーカス解除、
                    //    アイテム 2 のフォーカスと選択状態の獲得、の順である。
                    //    アイテム 1 の状態については uOldState の方に現れるので、
                    //    state (uNewState) に出ることはない
                    if ((state & Focused) != 0) {
                        // LPARAM を NMLISTVIEW に見立ててインデックス(iItem)を取得
                        int index = Marshal.ReadInt32(m.LParam, IntPtr.Size + OffsetIndex);
                        this.OnFocusedIndexChanged(new IndexChangedEventArgs(index));
                    }
                }
            }
            base.WndProc(ref m);
        }
    }
}

'VB.NET
Imports System
Imports System.ComponentModel
Imports System.Runtime.InteropServices
Imports System.Windows.Forms

Namespace HongliangSoft.Utilities.Gui
    Public Delegate Sub IndexChangedEventHandler( _
         ByVal sender As Object, ByVal e As IndexChangedEventArgs)
    Public Class IndexChangedEventArgs
        Inherits EventArgs
        Public Sub New(ByVal index As Integer)
            Me.m_index = index
        End Sub
        Private m_index As Integer
        Public ReadOnly Property Index() As Integer
            Get
                Return m_index
            End Get
        End Property
    End Class
    Public Class UsefulListView
        Inherits ListView
#If Not(V = 10 OrElse V = 11) Then
        Public Custom Event FocusedIndexChanged As IndexChangedEventHandler
            AddHandler(ByVal value As IndexChangedEventHandler)
                Me.Events.AddHandler(EventFocusedIndexChanged, value)
            End AddHandler
            RemoveHandler(ByVal value As IndexChangedEventHandler)
                Me.Events.RemoveHandler(EventFocusedIndexChanged, value)
            End RemoveHandler
            RaiseEvent(ByVal sender As Object, ByVal e As IndexChangedEventArgs)
                Dim handler As IndexChangedEventHandler _
                  = TryCast(Me.Events(EventFocusedIndexChanged), _
                                IndexChangedEventHandler)
                If handler IsNot Nothing Then
                    handler(Me, e)
                End If
            End RaiseEvent
        End Event
#Else
        Public Event FocusedIndexChanged As IndexChangedEventHandler
#End If
        Private Shared ReadOnly EventFocusedIndexChanged As New Object()
        Protected Overridable Sub OnFocusedIndexChanged(ByVal e As IndexChangedEventArgs)
            RaiseEvent FocusedIndexChanged(Me, e)
        End Sub
        Protected Overrides Sub WndProc(ByRef m As Message)
            ' LVN_ITEMCHANGED は親フォームに WM_NOTIFY(&H4E)で送信され、
            '    親フォームはそれを該当コントロールに
            '    WM_REFLECT(WM_USER(&H400)+&H1C00) を追加したメッセージコードで通知する。
            Const ReflectedItemChanged As Integer = &H400 + &H1c00 + &H4e
            ' LVN_ITEMCHANGED
            Const ItemChanged As Integer = -101
            ' NMHDR / NMLISTVIEW 構造体におけるメンバのオフセット(先頭の hdr 除く)
            Const OffsetCode As Integer = 4
            Const OffsetIndex As Integer = 8
            Const OffsetState As Integer = 16
            If m.Msg = ReflectedItemChanged Then
                ' LPARAM を NMHDR に見立てて通知コード(code)を取得
                Dim code As Integer = Marshal.ReadInt32( _
                                          m.LParam, IntPtr.Size + OffsetCode)
                If code = ItemChanged Then
                    ' LPARAM を NMLISTVIEW に見立ててアイテムの状態(uNewState)を取得
                    Dim state As Integer = Marshal.ReadInt32( _
                                               m.LParam, IntPtr.Size + OffsetState)
                    Const Focused As Integer = 1
                    ' 最下位ビットが立っている場合フォーカスが存在すると言う意味になる
                    ' フォーカスがアイテム 1 からアイテム 2 に動いた場合、
                    '    ItemChanged は複数回送られる。
                    '    一般的に、アイテム 1 の選択解除、アイテム 1 のフォーカス解除、
                    '    アイテム 2 のフォーカスと選択状態の獲得、の順である。
                    '    アイテム 1 の状態については uOldState の方に現れるので、
                    '    state (uNewState) に出ることはない
                    If Not((state And Focused) = 0) Then
                        ' LPARAM を NMLISTVIEW に見立ててインデックス(iItem)を取得
                        Dim index As Integer = Marshal.ReadInt32( _
                                                   m.LParam, IntPtr.Size + OffsetIndex)
                        Me.OnFocusedIndexChanged(New IndexChangedEventArgs(index))
                    End If
                End If
            End If
            MyBase.WndProc(m)
        End Sub
    End Class
End Namespace
posted by Hongliang at 01:29| Comment(6) | TrackBack(0) | .NET | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
フォーカスがある項目の取得の仕方がわからずにいたので大変参考になりました。
ところで、メッセージを送ればフォーカスを設定することもできるのではと考え、SendMessageを利用しようとしましたが、引数に与える値がわからずつまずきました。方法をご存じでしたらご解説をお願いできないでしょうか。
Posted by 田中 at 2006年08月17日 08:43
お返事遅くなってすいません。
ご質問の件ですが、.NET を使う限り SendMessage なぞ不要です。何故なら、ListViewItem に Focused プロパティがあり、これは { get; set; } なのですから。

あ、こう言う一般的な質問は掲示板で聞いた方が早いと思いますよ。
Posted by Hongliang at 2006年08月19日 10:45
ListViewItemのFocusedを使えばよかったんですね。気がつきませんでした。
掲示板の方への誘導も了解です。ご回答ありがとうございました。
Posted by 田中 at 2006年08月26日 15:44
当方C#初心者でプログラム作成中です。ListView のフォーカスが移動したときのソースはありますが呼び出し方がわかりません。よろしければ教えていただけませんでしょうか?。希望としてはClass1でイベントを起こしForm1で受けたいのですがよろしくお願いします。
Posted by kurimu at 2011年01月28日 03:34
> Class1でイベントを起こし
意味が分かりません。Class1 ってのはどこから出てきましたか?
とりあえず、この UsefulListView をコンパイルすればツールボックスに出てくるので、それを普通の ListView の代わりにフォームにドロップして、あとは普通のイベントと同じくプロパティグリッドの雷アイコンから FocusedIndexChanged をってやればいけるはずですけど。
Posted by Hongliang at 2011年01月29日 00:32
とんでもない勘違いをしていました。
助言どおりにやってみましたなら無事確認できました。
どうもありがとうございました。
Posted by kurimu at 2011年01月30日 04:35
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。

×

この広告は1年以上新しい記事の投稿がないブログに表示されております。