忍者ブログ

VB.NET-TIPS などプログラミングについて

VB.NETのTIPS(小技集)を中心に、その他のプログラミングについて少し役に立つ情報を発信します。いわゆる個人的な忘備録ですが、みなさんのお役に立てれば幸いです。

スレッドタイマの使い方

標準コントロールのTimerコントロールとは異なり、タイマイベントを別スレッドとして起動する スレッドタイマの説明をします。

タイマイベントはSystem.Threadingの名前空間にある、Timerクラスで、 MSDNの説明によれば以下の様になっています。

System.Threading.Timer について

<コンストラクター(複数ある中の今回利用のもの)>
Public Sub New (
    callback As TimerCallback, :TimerCallback を実行するメソッドを表すデリゲート
    state As Object,           :コールバック メソッドで使用される情報を格納したオブジェクト
                                または nullです。
    dueTime As Integer,        :前に遅延する時間 callback ミリ秒単位で呼び出されます。 
                                指定 Timeout.Infinite 、タイマーが起動しないようにします。
                                0 を指定して、タイマーをすぐに開始します。
    period As Integer          :呼び出しの間の時間間隔 callback, 、(ミリ秒単位)。
                                指定 Timeout.Infinite 周期的なシグナル通知を無効にします。
)
<メソッド>
Public Function Change (
    dueTime As Integer,        :時に指定されたコールバック メソッドを呼び出す前に遅延する時間数、
                                Timer (ミリ秒単位) が作成されました。
                                指定 Timeout.Infinite タイマーが再開されないようにします。
                                0 を指定して、タイマーをすぐに再開します。
    period As Integer          :コールバック メソッドの呼び出し間の時間間隔が指定されたときに、
                                Timer (ミリ秒単位) が作成されました。
                                指定 Timeout.Infinite 周期的なシグナル通知を無効にします。
) As Boolean
    戻り値:true の場合は、タイマーが正常に更新されました。それ以外の場合、 falseです。

今回の例としては、スレッドタイマを作成し、 TimerCallback イベントで 出力ウインドウに現在の時刻を表示する動作をします。

スレッドタイマの使い方

Imports System.Threading
Imports System.Threading.Thread

Public Class frmTimer2

    'スレッドタイマ
    Private mtimer As System.Threading.Timer

    ''' 
    ''' フォームロードイベント
    ''' 
    Private Sub frmTimer2_Load(sender As Object, e As EventArgs) Handles Me.Load
        'タイマコールバック関数
        Dim timerDelegate As TimerCallback = New TimerCallback(AddressOf TimerEvent)
        '1000msecタイマ生成(コールバック関数の設定)
        mtimer = New Timer(timerDelegate, Nothing, 0, 1000)
    End Sub

    ''' 
    ''' System.Threading.Timer からの呼び出しを処理するメソッド
    ''' 
    ''' <param name="state">このデリゲートで呼び出されたメソッドに関連するアプリケーション固有の情報を格納するオブジェクト</param>
    Private Sub TimerEvent(ByVal state As Object)
        '出力ウインドウに時刻表示
        Console.WriteLine("Time:" & Now.ToString("hh:mm:ss"))
    End Sub

End Class

実行させると、1000msec毎に関数 TimerEvent が呼び出され、出力ウインドウに現在の時刻が表示されることが確認できます。

そこで、以下のページの様にフォーム上のラベルに時刻を表示させてみようとしました。
Timerコントロールの使い方 :[Timer,Interval]

しかし、思っていた様にラベルには時刻が表示されずに、以下の様なエラーが表示されてしまいました。

「有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'Label1' がアクセスされました。」

スレッドタイマの使い方・エラーの出たフォーム上のラベルへの表示

Imports System.Threading
Imports System.Threading.Thread

Public Class frmTimer2

    'スレッドタイマ
    Private mtimer As System.Threading.Timer

    ''' 
    ''' フォームロードイベント
    ''' 
    Private Sub frmTimer2_Load(sender As Object, e As EventArgs) Handles Me.Load
        'タイマコールバック関数
        Dim timerDelegate As TimerCallback = New TimerCallback(AddressOf TimerEvent)
        '1000msecタイマ生成(コールバック関数の設定)
        mtimer = New Timer(timerDelegate, Nothing, 0, 1000)
    End Sub

    ''' 
    ''' System.Threading.Timer からの呼び出しを処理するメソッド
    ''' 
    ''' <param name="state">このデリゲートで呼び出されたメソッドに関連するアプリケーション固有の情報を格納するオブジェクト</param>
    Private Sub TimerEvent(ByVal state As Object)
        ''出力ウインドウに時刻表示
        'Console.WriteLine("Time:" & Now.ToString("hh:mm:ss"))
        'ラベルに現在時刻表示
        Me.Label1.Text = Now.ToString("hh:mm:ss")
    End Sub

End Class

「有効ではないスレッド間の操作...」エラーは、 あるスレッドから異なるスレッド上のコントロールにアクセスしようとしたことが原因でした。
つまりスレッドタイマの TimerEvent と、 フォームのラベルがあるスレッドとは全く別にスレッド上で動作していることになります。

これを解決するには、デリゲートという方法を使います。 デリゲートを使えば別スレッドからフォーム上のコントロールへのアクセスが出来る様になります。

デリゲートの方法ですがソースを見た方が分かりやすいと思いますので、 以下に示します。

スレッドタイマの使い方・デリゲート宣言とInvoke関数

Imports System.Threading
Imports System.Threading.Thread

Public Class frmTimer2

    'スレッドタイマ
    Private mtimer As System.Threading.Timer

    ''' 
    ''' フォームロードイベント
    ''' 
    Private Sub frmTimer2_Load(sender As Object, e As EventArgs) Handles Me.Load
        'タイマコールバック関数
        Dim timerDelegate As TimerCallback = New TimerCallback(AddressOf TimerEvent)
        '1000msecタイマ生成(コールバック関数の設定)
        mtimer = New Timer(timerDelegate, Nothing, 0, 1000)
    End Sub

    ''' 
    ''' System.Threading.Timer からの呼び出しを処理するメソッド
    ''' 
    ''' <param name="state">このデリゲートで呼び出されたメソッドに関連するアプリケーション固有の情報を格納するオブジェクト</param>
    Private Sub TimerEvent(ByVal state As Object)
        'ラベルに現在時刻表示
        Dim strDate As String = Now.ToString("hh:mm:ss")
        Invoke(New SetLabelTextDelegate(AddressOf SetLabelText),
               New Object() {strDate})
    End Sub

    ' ラベル表示デリゲート宣言
    Private Delegate Sub SetLabelTextDelegate(ByVal strText As String)

    ''' 
    ''' ラベル表示
    ''' 
    ''' <param name="strText">表示文字列</param>
    Private Sub SetLabelText(ByVal strText As String)
        Label1.Text = strText
    End Sub
End Class

このソースで一応ラベルへの表示は以下図の様に出来る様になりました。

ここで重要なのは、ラベル表示用関数のデリゲート宣言と Invoke 関数です。 デリゲート(delegate)という動詞には「委譲する、委任する」、名詞では「代理人」などという意味が有りますが、 デリゲートの呼び出し側からすれば、関数の引数を合わせて、デリゲートされたものを Invoke 関数で 呼出すだけのことになります。
デリゲートとして宣言されている中身の処理については、そちらままかせの感じになります。

デリゲートとして宣言された関数を Invoke で呼出してやれば、スレッドを超えて処理が可能になります。 さて、 Invoke ですがMSDNの説明では以下の様になっています。

Public Function Invoke(method As System.Delegate, ParamArray args() As Object) As Object
概要:
  コントロールの基になるウィンドウ ハンドルを所有するスレッド上で、
  指定した引数リストを使用して、指定したデリゲートを実行します。

パラメーター:
  method: args パラメーターに指定されている数および型と同じ数
          および型のパラメーターをとるメソッドへのデリゲート。
  args:   指定したメソッドに引数として渡すオブジェクトの配列。
          メソッドが引数をとらない場合、このパラメーターは null になります。
PR

コメント

コメントを書く