none
すべてのフォームのChangeイベントの監視 RRS feed

  • 質問

  • こんにちは。
    VB.NETでアプリケーションの設定画面を作成しています。
    フォーム上の任意のコントロールの値が変更されたら、適用ボタンが押せる用にしたいと考えています。
    コントロールの種類はテキストボックス、コンボボックス、ラジオボタン、リストボックス、グリッドヴューなど様々あります。
    また画面もたくさんあります。
    一つ一つのコントロールにchangeイベントを仕込むのは大変だし、ミスってイベントを入れ忘れることがあるといけないので、
    値の変更を統一的に管理をしたいのですが何かいい方法はありませんでしょうか。

    よろしくお願いします。

    2011年3月7日 8:25

回答

  • ユーザーが再度変更して元の値に戻したら適用ボタンを再度Enabled=falseにしないといけないんですよね。たぶん・・・
    こういった場合(というかほとんどいつも)、私はUIバインドオブジェクトを用意し、それとデーターテーブルの値を比較することによって実現しています。
    このようにしておくと、ユーザーが値を変更して更新ボタンを押さずに画面を閉じようとした時のみ、「本当に閉じてもいいですか?」というメッセージも出せるようになります。
    UIバインドオブジェクトは以下を参考にして下さい。

    Part 2. スマートクライアントにおける単体入力データ検証
    http://blogs.msdn.com/b/nakama/archive/2009/02/26/part-2.aspx

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    • 回答としてマーク 山本春海 2011年4月1日 5:46
    2011年3月7日 9:09
    モデレータ
  • 相性というのは特に無いと思います。例えばデーターテーブルをDataGridViewに直接バインドする代わりにUIオブジェクトを作成し、それをDataGridViewにバインドさせます。このUIオブジェクトとデーターテーブルの差異を見れば、ユーザーが変更したかとうかがわかります。
    もしくは、ユーザーが変更したかどうかだけに的を絞れば、バインドした直後の各値を覚えておき、それとデーターテーブルの値を比較してもユーザーによる変更を知ることができます。うろ覚えですが、データーテーブルは同じ値で更新してもDataRowVersionが変わらなかったと思いますので、そうであればDataRowVersionによる判断もできそうです。
    以上とSelectedChangedなどと組み合わせてチェックすれば良いと思います。

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    • 回答としてマーク 山本春海 2011年4月1日 5:46
    2011年3月7日 11:32
    モデレータ
  • >親のクラスをつくって、そこでコントロールの管理はできないでしょうか。

    >各コントロールの差分は子供のクラスに書くってことは無理でしょうか。

    親クラスというか、コントロールを継承したユーザコントロールをつくり、そのコントロール

    それぞれの値変更と思われる動作に対して、コントロールが配置されるフォームオブジェクト

    に対してWindowsメッセージを送り、フォーム側ではそのメッセージを受け取ったら

    変更が行われたとみなす、というような感じはどうでしょう。

    イメージとしては、以下のようなものです。

    ' Form1.vb
    Public Class Form1
      Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
        MyBase.WndProc(m)
        If m.Msg = WM_APP_CHANGEDATA Then
          Console.WriteLine("変更処理をここに書く")
        End If
      End Sub
    End Class
    
    ' Module1.vb
    Module Module1
      Public Const WM_APP_CHANGEDATA As Integer = &H8000
      Public Declare Function SendMessage Lib "user32.dll" Alias "SendMessageA" _
          (ByVal hwnd As Integer, ByVal msg As Integer, _
          ByVal wParam As Integer, ByVal lParam As Integer) As Integer
      Public Function GetFormHandle(ByRef ctrl As Control) As IntPtr
        Dim handle As IntPtr
        Dim wkctrl As Control = ctrl
        Do While wkctrl.Parent IsNot Nothing
          handle = wkctrl.Parent.Handle
          wkctrl = wkctrl.Parent
        Loop
        Return handle
      End Function
    End Module
    
    ' MyControls.vb
    Imports System.Windows.Forms
    Public Class MyTextBox : Inherits System.Windows.Forms.TextBox
      Protected Overrides Sub OnTextChanged(ByVal e As System.EventArgs)
        MyBase.OnTextChanged(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
    End Class
    Public Class MyCheckBox : Inherits System.Windows.Forms.CheckBox
      Protected Overrides Sub OnCheckedChanged(ByVal e As System.EventArgs)
        MyBase.OnCheckedChanged(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
    End Class
    Public Class MyComboBox : Inherits System.Windows.Forms.ComboBox
      Protected Overrides Sub OnSelectedIndexChanged(ByVal e As System.EventArgs)
        MyBase.OnSelectedIndexChanged(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
    End Class
    Public Class MyDataGridView : Inherits System.Windows.Forms.DataGridView
      Protected Overrides Sub OnCellValueChanged(ByVal e As DataGridViewCellEventArgs)
        MyBase.OnCellValueChanged(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
      Protected Overrides Sub OnColumnAdded(ByVal e As DataGridViewColumnEventArgs)
        MyBase.OnColumnAdded(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
      Protected Overrides Sub OnColumnRemoved(ByVal e As DataGridViewColumnEventArgs)
        MyBase.OnColumnRemoved(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
      Protected Overrides Sub OnRowsAdded(ByVal e As DataGridViewRowsAddedEventArgs)
        MyBase.OnRowsAdded(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
      Protected Overrides Sub OnRowsRemoved(ByVal e As DataGridViewRowsRemovedEventArgs)
        MyBase.OnRowsRemoved(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
    End Class
    
    
    • 回答としてマーク 山本春海 2011年4月1日 5:46
    2011年3月8日 2:46
  • フォームのロード時にフォーム上のコントロールを全て取得して、イベントをセットしてしまえば良いと思います。

    ちなみに

    		private void CreateControlList( Control parent )
    		{
    			foreach( Control control in parent.Controls )
    			{
    				m_controlList.Add(control);
    
    				// 再起処理(パネルやグループボックス用)
    				CreateControlList(control);
    			}
    		}
    

    こんなメソッドを用意して、ロード時に読んであげる事でコントロール一覧を取得して、

    				foreach( Control control in m_controlList )
    				{
              if (control is TextBox)
              {
                control.KeyDown += new System.Windows.Forms.KeyEventHandler(Common_KeyDown);
              }
            }
    こんな感じでコントロールが何なのかを判断しながらハンドリングするってのはどうでしょう。
    • 回答としてマーク 山本春海 2011年4月1日 5:46
    2011年3月13日 13:25
  • 基底フォームを作成し、ControlAdded イベント をハンドルして全てのコントロールについてTextChangedなりSelectedIndexChangedなりのイベントハンドラを追加してやるといいかもしれません。

    もしくは、専用のメソッドを作成して任意のタイミング(フォームロードとか)で呼んでやる等が考えられます。

    • 回答としてマーク 山本春海 2011年4月1日 5:46
    2011年3月29日 1:48

すべての返信

  • 統一的に管理をしたいということですが、コントロールの種類単位で、たとえばテキストボックス、

    チェックボックス、コンボボックスといった単位での統括管理であれば、以下でどうでしょうか。

    一つのイベント処理に対して、複数のコントロールのイベントを割り当てることが出来ます。

    Handlesの後に、カンマ区切りで指定します。

    ' テキストボックス
    Private Sub TextBox_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TextBox1.TextChanged, TextBox2.TextChanged
      ButtonEnabledTrue()
    End Sub
    ' チェックボックス
    Private Sub CheckBox_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CheckBox1.CheckedChanged, CheckBox2.CheckedChanged
      ButtonEnabledTrue()
    End Sub
    ' コンボボックス
    Private Sub ComboBox_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ComboBox1.SelectedIndexChanged, ComboBox2.SelectedIndexChanged
      ButtonEnabledTrue()
    End Sub
    ' 共通のボタンの有効処理
    Private Sub ButtonEnabledTrue()
      Me.Button1.Enabled = True
    End Sub
    
    
    2011年3月7日 8:54
  • ユーザーが再度変更して元の値に戻したら適用ボタンを再度Enabled=falseにしないといけないんですよね。たぶん・・・
    こういった場合(というかほとんどいつも)、私はUIバインドオブジェクトを用意し、それとデーターテーブルの値を比較することによって実現しています。
    このようにしておくと、ユーザーが値を変更して更新ボタンを押さずに画面を閉じようとした時のみ、「本当に閉じてもいいですか?」というメッセージも出せるようになります。
    UIバインドオブジェクトは以下を参考にして下さい。

    Part 2. スマートクライアントにおける単体入力データ検証
    http://blogs.msdn.com/b/nakama/archive/2009/02/26/part-2.aspx

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    • 回答としてマーク 山本春海 2011年4月1日 5:46
    2011年3月7日 9:09
    モデレータ
  • honefaiさん、ありがとうございます。
    イベントの処理を集中管理するとなるとこのような感じになるのですね。
    素人考えでお伺いするため、まったくこんなことできないかもしれませんが、
    親のクラスをつくって、そこでコントロールの管理はできないでしょうか。
    各コントロールの差分は子供のクラスに書くってことは無理でしょうか。。。。

    2011年3月7日 10:54
  • trapemiyaさん、ありがとうございます。
    今の機能を整理するとき参考になりそうです。
    気になる点といえば、
    SPREADやDataGridViewやリスト、コンボボックスとの相性ですが、
    ご存知ないでしょうか?

    2011年3月7日 11:03
  • 相性というのは特に無いと思います。例えばデーターテーブルをDataGridViewに直接バインドする代わりにUIオブジェクトを作成し、それをDataGridViewにバインドさせます。このUIオブジェクトとデーターテーブルの差異を見れば、ユーザーが変更したかとうかがわかります。
    もしくは、ユーザーが変更したかどうかだけに的を絞れば、バインドした直後の各値を覚えておき、それとデーターテーブルの値を比較してもユーザーによる変更を知ることができます。うろ覚えですが、データーテーブルは同じ値で更新してもDataRowVersionが変わらなかったと思いますので、そうであればDataRowVersionによる判断もできそうです。
    以上とSelectedChangedなどと組み合わせてチェックすれば良いと思います。

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    • 回答としてマーク 山本春海 2011年4月1日 5:46
    2011年3月7日 11:32
    モデレータ
  • いろいろ教えて頂きまして、ありがとうございます!

    2011年3月7日 12:33
  • >親のクラスをつくって、そこでコントロールの管理はできないでしょうか。

    >各コントロールの差分は子供のクラスに書くってことは無理でしょうか。

    親クラスというか、コントロールを継承したユーザコントロールをつくり、そのコントロール

    それぞれの値変更と思われる動作に対して、コントロールが配置されるフォームオブジェクト

    に対してWindowsメッセージを送り、フォーム側ではそのメッセージを受け取ったら

    変更が行われたとみなす、というような感じはどうでしょう。

    イメージとしては、以下のようなものです。

    ' Form1.vb
    Public Class Form1
      Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
        MyBase.WndProc(m)
        If m.Msg = WM_APP_CHANGEDATA Then
          Console.WriteLine("変更処理をここに書く")
        End If
      End Sub
    End Class
    
    ' Module1.vb
    Module Module1
      Public Const WM_APP_CHANGEDATA As Integer = &H8000
      Public Declare Function SendMessage Lib "user32.dll" Alias "SendMessageA" _
          (ByVal hwnd As Integer, ByVal msg As Integer, _
          ByVal wParam As Integer, ByVal lParam As Integer) As Integer
      Public Function GetFormHandle(ByRef ctrl As Control) As IntPtr
        Dim handle As IntPtr
        Dim wkctrl As Control = ctrl
        Do While wkctrl.Parent IsNot Nothing
          handle = wkctrl.Parent.Handle
          wkctrl = wkctrl.Parent
        Loop
        Return handle
      End Function
    End Module
    
    ' MyControls.vb
    Imports System.Windows.Forms
    Public Class MyTextBox : Inherits System.Windows.Forms.TextBox
      Protected Overrides Sub OnTextChanged(ByVal e As System.EventArgs)
        MyBase.OnTextChanged(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
    End Class
    Public Class MyCheckBox : Inherits System.Windows.Forms.CheckBox
      Protected Overrides Sub OnCheckedChanged(ByVal e As System.EventArgs)
        MyBase.OnCheckedChanged(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
    End Class
    Public Class MyComboBox : Inherits System.Windows.Forms.ComboBox
      Protected Overrides Sub OnSelectedIndexChanged(ByVal e As System.EventArgs)
        MyBase.OnSelectedIndexChanged(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
    End Class
    Public Class MyDataGridView : Inherits System.Windows.Forms.DataGridView
      Protected Overrides Sub OnCellValueChanged(ByVal e As DataGridViewCellEventArgs)
        MyBase.OnCellValueChanged(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
      Protected Overrides Sub OnColumnAdded(ByVal e As DataGridViewColumnEventArgs)
        MyBase.OnColumnAdded(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
      Protected Overrides Sub OnColumnRemoved(ByVal e As DataGridViewColumnEventArgs)
        MyBase.OnColumnRemoved(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
      Protected Overrides Sub OnRowsAdded(ByVal e As DataGridViewRowsAddedEventArgs)
        MyBase.OnRowsAdded(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
      Protected Overrides Sub OnRowsRemoved(ByVal e As DataGridViewRowsRemovedEventArgs)
        MyBase.OnRowsRemoved(e)
        SendMessage(GetFormHandle(Me), WM_APP_CHANGEDATA, 0, 0)
      End Sub
    End Class
    
    
    • 回答としてマーク 山本春海 2011年4月1日 5:46
    2011年3月8日 2:46
  • フォームのロード時にフォーム上のコントロールを全て取得して、イベントをセットしてしまえば良いと思います。

    ちなみに

    		private void CreateControlList( Control parent )
    		{
    			foreach( Control control in parent.Controls )
    			{
    				m_controlList.Add(control);
    
    				// 再起処理(パネルやグループボックス用)
    				CreateControlList(control);
    			}
    		}
    

    こんなメソッドを用意して、ロード時に読んであげる事でコントロール一覧を取得して、

    				foreach( Control control in m_controlList )
    				{
              if (control is TextBox)
              {
                control.KeyDown += new System.Windows.Forms.KeyEventHandler(Common_KeyDown);
              }
            }
    こんな感じでコントロールが何なのかを判断しながらハンドリングするってのはどうでしょう。
    • 回答としてマーク 山本春海 2011年4月1日 5:46
    2011年3月13日 13:25
  • 基底フォームを作成し、ControlAdded イベント をハンドルして全てのコントロールについてTextChangedなりSelectedIndexChangedなりのイベントハンドラを追加してやるといいかもしれません。

    もしくは、専用のメソッドを作成して任意のタイミング(フォームロードとか)で呼んでやる等が考えられます。

    • 回答としてマーク 山本春海 2011年4月1日 5:46
    2011年3月29日 1:48