[C#]WMIイベントをManagementEventWatcherクラスで処理する

Windowsでは、WMI(Windows Management Instrumentation)のAPIを使用することで、OSを構成しているコンポーネントを操作出来ます。

WMIでは通常データの取得やセットを行う事が多いのですが、イベントハンドラを登録しておく事で、指定した状況が発生したかをプログラムで検出することが出来ます。


今回は、C#より.Net FrameworkのManagementEventWatcherクラスを使用して、イベントハンドラを取れるようにします。サンプルとして、PCに接続されているディスプレイの輝度が変更された事をイベントとして検出し、イベント情報を画面に出力するプログラムを作成します。


イベントハンドラの登録


WMIイベントを受信する上で、最初に行う事はイベントの監視処理です。
下記のコードは、監視の開始処理のサンプルです。

ManagementEventWatcher watcher;
 
public Form1() {
    InitializeComponent();
    SetEventHandler();
}
 
//*********************************************************************
/// <summary> ディスプレイの輝度が変わったときのイベントハンドラを登録する
/// </summary>
//*********************************************************************
void SetEventHandler() {
    //-------------------------------------------------------------------
    // ディスプレイの輝度が変わったときのイベントオブジェクトを取得する
    //-------------------------------------------------------------------
    string              scope    = @"root\wmi";
    string              queryStr = "SELECT * FROM WmiMonitorBrightnessEvent";
    EventWatcherOptions option   = new EventWatcherOptions( null, new TimeSpan( 0, 0, 1 ), 1 );
    watcher = new ManagementEventWatcher( scope, queryStr, option );
 
    //--------------------------------
    // イベント受信時のハンドラを定義
    //--------------------------------
    watcher.EventArrived += new EventArrivedEventHandler( BrightnessEventArrived );
 
    //--------------------------------
    // イベントの監視を開始する
    //--------------------------------
    watcher.Start();
}



まず、SetEventHandler()メソッド内で、ManagementEventWatcherオブジェクトを生成しています。

string              scope    = @"root\wmi";
string              queryStr = "SELECT * FROM WmiMonitorBrightnessEvent";
EventWatcherOptions option   = new EventWatcherOptions( null, new TimeSpan( 0, 0, 1 ), 1 );
watcher = new ManagementEventWatcher( scope, queryStr, option );



コンストラクタの第一引数ではWMIのスコープを定義しています。
例では、プログラムが起動しているPCに対してWMI APIをコールしていますが、他のPCを対象としたい場合は、@”\\hostname\root\wmi” の形式で指定します。
第二引数にて、何のイベントを取りたいかをWQLで指定します。
第三引数ではイベント監視のオプションを指定します(省略可)。




watcher.EventArrived += new EventArrivedEventHandler( BrightnessEventArrived );


オブジェクトを生成したら、イベントが発生したときのハンドラメソッドを登録します。
BrightnessEventArrived()の中身は後述します。




watcher.Start();


その後、Start()メソッドをコールする事で、イベントの監視が行われます。


イベント発生時の処理


イベントハンドラを登録したら、次は実際のコールバック処理を記述します。

下記のコードは、EventArrivedで指定したコールバックメソッドです。

//*********************************************************************
/// <summary>   ディスプレイの輝度が変わったときのイベント
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
//*********************************************************************
public void BrightnessEventArrived( object sender, EventArrivedEventArgs e ) {
    ManagementNamedValueCollection ctx      = e.Context  as ManagementNamedValueCollection;
    ManagementBaseObject           newEvent = e.NewEvent;
 
    StringBuilder tmpBuff = new StringBuilder();
 
 
    //----------------------------
    // イベントの情報を取得
    //----------------------------
 
    // イベントプロパティの情報をダンプ
    tmpBuff.AppendLine( "[Properties]" );
    foreach ( PropertyData prop in newEvent.Properties ) {
        tmpBuff.Append( prop.Name + "=" );
        tmpBuff.AppendLine( prop.Value == null ? "null" : prop.Value.ToString() );
    }
 
    // WMIシステムプロパティの情報をダンプ
    tmpBuff.AppendLine( "[SystemProperties]" );
    foreach( PropertyData sysProp in newEvent.SystemProperties ) {
        tmpBuff.Append( sysProp.Name + "=" );
        tmpBuff.AppendLine( sysProp.Value == null ? "null" : sysProp.Value.ToString() );
    }
 
    // WMI修飾子の情報をダンプ
    tmpBuff.AppendLine( "[Qualifier]" );
    foreach( QualifierData qualifier in newEvent.Qualifiers ) {
        tmpBuff.Append( qualifier.Name + "=" );
        tmpBuff.AppendLine( qualifier.Value == null ? "null" : qualifier.Value.ToString() );
    }
 
    //--------------------------------------------
    // イベント情報を画面に表示
    // (UIスレッド側で、UpdateText()をコールする)
    //--------------------------------------------
    UpdateTextDelegate updateTextDelegate = new UpdateTextDelegate( UpdateText );
    this.Invoke( updateTextDelegate, new object[] { tmpBuff.ToString() });
 
    // これはNG
    textBox1.Text = tmpBuff.ToString();
}



指定したイベントが発生すると、本メソッドが実行されます。
イベントの内容は、第二引数のEventArrivedEventArgsで渡されます。
今回の例では、この内容を文字列としてダンプしています。


手元の環境で本処理を実行した結果は、以下のようになりました。


[Properties]
Active=True
Brightness=63
InstanceName=DISPLAY\LEN4014\4&33df4d60&0&UID67568640_0
SECURITY_DESCRIPTOR=null
TIME_CREATED=129823120747750013
 
[SystemProperties]
__GENUS=2
__CLASS=WmiMonitorBrightnessEvent
__SUPERCLASS=WMIEvent
__DYNASTY=__SystemClass
__RELPATH=WmiMonitorBrightnessEvent.InstanceName="DISPLAY\\LEN4014\\4&33df4d60&0&UID67568640_0"
__PROPERTY_COUNT=5
__DERIVATION=System.String[]
__SERVER=null
__NAMESPACE=null
__PATH=null
 
[Qualifier]
abstract=True
dynamic=True



取得したデータを画面に表示する場合、以下のようにUIのプロパティにアクセスしたいのですが、残念ながらこれは出来ません.

textBox1.Text = tmpBuff.ToString();



試しに実行してみると、以下のようにInvalidOperationExceptionが発生します。



これは、イベントハンドラの処理が、UIスレッドとは別のスレッドで実行されているためです。スレッド一覧のウィンドウで確認すると、確かにイベントハンドラ用のワーカースレッドが走っています。


このような状況で、GUI上のコントロールを操作したい場合は、Form.Invoke()メソッドを使用する解決方法が一般的です。下記のように、Invoke()をコールする事で、指定したメソッド(のdelegate)を、UIスレッド側で実行する事が出来ます。

//--------------------------------------------
// イベント情報を画面に表示
// (UIスレッド側で、UpdateText()をコールする)
//--------------------------------------------
UpdateTextDelegate updateTextDelegate = new UpdateTextDelegate( UpdateText );
this.Invoke( updateTextDelegate, new object[] { tmpBuff.ToString() });




実際にコールされるUpdateText()は、以下のように定義します。

//*********************************************************************
/// <summary>   textBoxにメッセージを表示させる
/// </summary>
/// <param name="text"></param>
//*********************************************************************
private void UpdateText( string text ) {
    textBox1.Text = text;
}
 
delegate void UpdateTextDelegate( string text );





WMIイベント監視の終了処理


ManagementEventWatcherを使用して、イベントの監視を行った場合は、プログラムの終了前(一般的にはForm_Closedイベント等)で、Stop()メソッドをコールする必要があります。

//*********************************************************************
/// <summary>   フォームが閉じたときのハンドラ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
//*********************************************************************
private void Form1_FormClosed( object sender, FormClosedEventArgs e ) {
    //----------------------
    // WMIの監視を停止する
    //----------------------
    if ( watcher != null ) {
        watcher.Stop();
        watcher = null;
    }
}



もしStop()を呼び忘れると、終了時に以下の例外が発生します。
この例外、メッセージが非常に分かり辛く原因の特定が難しいので、忘れずに終了処理を行ってください。


System.Runtime.InteropServices.InvalidComObjectException はハンドルされませんでした。
  Message=基になる RCW から分割された COM オブジェクトを使うことはできません。
  Source=mscorlib
  StackTrace:
       場所 System.StubHelpers.StubHelpers.StubRegisterRCW(Object pThis, IntPtr pThread)
       場所 System.Management.IWbemServices.CancelAsyncCall_(IWbemObjectSink pSink)
       場所 System.Management.SinkForEventQuery.Cancel()
       場所 System.Management.ManagementEventWatcher.Stop()
       場所 System.Management.ManagementEventWatcher.Finalize()
  InnerException:




4789818454
WMIシステム管理開発テクニック

関連記事

コメントを残す

メールアドレスが公開されることはありません。