tagCANDY CGI VBレスキュー(花ちゃん) の Visual Basic 2010 用 掲示板(VB.NET 掲示板) [ツリー表示へ]   [Home]
一括表示(VB.NET VB2005)
タイトルマルチスレッドでコントロールを操作する方法
記事No6297
投稿日: 2007/09/13(Thu) 15:58
投稿者ちゃら

環境 : Visual Basic 2005

実現したい処理:
  フォーム上に、 DataGridView1 と Button1 を配置。
  フォーム起動時に、 DataGridView1 に50レコード追加し、表示。
  Button1 を押すと、 DataGridView1 のレコードを上から
  1行ずつ参照し、特定の処理を行う。
  処理している行が分かるように、
  その行の処理を開始した時、セルに "処理中..." と表示し
  その行の処理が終了した時、セルに "処理完了" と表示したい。

これを実現する為に次のようなコードを作成しました。(コードA参照)

しかし、これではシングルスレッドである為、
処理が完了するまで、何も反応しません。
DataGridView に "処理中..." などの状況も表示されません。

# MainTable.Rows(i)("column1") = "処理中..."
# の直後に DataGridView1.Refresh を入れると
# とりあえず、思っていた通りの処理にはなりますが、
# 処理が遅くなってしまいます。
# このサンプルでは、遅くなることを実感できないかもしれませんが・・・

そこで、過去スレなどを参照し、
非同期で実行できるように改良しました。(コードB参照)
# Form_Load の処理は変更なしです。
とりあえず、思っていた通りの処理になったのですが、
コードBで問題ないでしょうか?

一番不安なのが、 BackgroundWorker1_DoWork の処理で、
Invoke メソッドって使用してしまってよいのでしょうか?
またそのオブジェクトは Me にしていますが、それでよいのでしょうか?

================================================================================
'' (コードA)同期版
Public Class Form1
    Dim MainTable As New System.Data.DataTable

    ' フォームロード時
    Private Sub Form1_Load(ByVal sender As System.Object, _
                           ByVal e As System.EventArgs) Handles MyBase.Load
        MainTable.Columns.Add("column1", System.Type.GetType("System.String"))
        For i As Integer = 0 To 50 - 1
            Dim row As System.Data.DataRow = MainTable.NewRow
            row("column1") = ""
            MainTable.Rows.Add(row)
        Next
        DataGridView1.DataSource = MainTable
    End Sub

    ' 実行ボタン押下時
    Private Sub Button1_Click(ByVal sender As System.Object, _
                              ByVal e As System.EventArgs) Handles Button1.Click
        ' グリッドの初期化
        For i As Integer = 0 To 50 - 1
            MainTable.Rows(i)("column1") = ""
        Next
        ' 1行ずつ処理
        For i As Integer = 0 To 50 - 1
            ' DataGridView のセル値更新
            MainTable.Rows(i)("column1") = "処理中..."
            ' 重たい処理(ここでは 0.1 秒待機)
            System.Threading.Thread.Sleep(100)
            ' DataGridView のセル値更新
            MainTable.Rows(i)("column1") = "処理完了"
        Next
        MsgBox("処理が完了しました")
    End Sub
End Class

'' (コードB)非同期版
Public Class Form1
    Dim MainTable As New System.Data.DataTable

    ' 別スレッドからメインスレッドのプロシージャを呼ぶ為のデリゲート宣言
    Private Delegate Sub DelegateGridRefresh(ByVal row As Integer, ByVal value As String)

    ' フォームロード時
    Private Sub Form1_Load(ByVal sender As System.Object, _
                           ByVal e As System.EventArgs) Handles MyBase.Load
        MainTable.Columns.Add("column1", System.Type.GetType("System.String"))
        For i As Integer = 0 To 50 - 1
            Dim row As System.Data.DataRow = MainTable.NewRow
            row("column1") = ""
            MainTable.Rows.Add(row)
        Next
        DataGridView1.DataSource = MainTable
    End Sub

    ' 実行ボタン押下時
    Private Sub Button1_Click(ByVal sender As System.Object, _
                              ByVal e As System.EventArgs) Handles Button1.Click
        ' グリッドの初期化
        For i As Integer = 0 To 50 - 1
            MainTable.Rows(i)("column1") = ""
        Next
        ' 非同期処理の開始
        BackgroundWorker1.RunWorkerAsync()
    End Sub

    ' 別スレッドの処理
    Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, _
                                         ByVal e As System.ComponentModel.DoWorkEventArgs) _
                                         Handles BackgroundWorker1.DoWork
        Dim worker As System.ComponentModel.BackgroundWorker = _
                      TryCast(sender, System.ComponentModel.BackgroundWorker)
        ' 1行ずつ処理
        For i As Integer = 0 To 50 - 1
            ' DataGridView のセル値更新
            Me.Invoke(New DelegateGridRefresh(AddressOf GridRefresh), New Object() {i, "処理中..."})
            ' 重たい処理(ここでは 0.1 秒待機)
            System.Threading.Thread.Sleep(100)
            ' DataGridView のセル値更新
            Me.Invoke(New DelegateGridRefresh(AddressOf GridRefresh), New Object() {i, "処理完了"})
        Next
    End Sub

    ' 別スレッド終了時
    Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As Object, _
                          ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
                          Handles BackgroundWorker1.RunWorkerCompleted
        MsgBox("処理が完了しました")
    End Sub

    ' DataGridView のセル値変更プロシージャ
    Private Sub GridRefresh(ByVal row As Integer, ByVal value As String)
        MainTable.Rows(row)("column1") = value
    End Sub
End Class

[ツリー表示へ]
タイトルRe: マルチスレッドでコントロールを操作する方法
記事No6298
投稿日: 2007/09/13(Thu) 16:07
投稿者Hongliang
> 一番不安なのが、 BackgroundWorker1_DoWork の処理で、
> Invoke メソッドって使用してしまってよいのでしょうか?
> またそのオブジェクトは Me にしていますが、それでよいのでしょうか?

別に Invoke を使用すること自体はかまいませんが、それよりもせっかく使っているのだから BackgroundWorker が提供する経過報告用のイベントである ProgressChanged イベントと、それを起こすための ReportProgress メソッドの使用を検討なさってはいかがでしょうか。

[ツリー表示へ]
タイトルRe^2: マルチスレッドでコントロールを操作する方法
記事No6300
投稿日: 2007/09/13(Thu) 18:10
投稿者ちゃら
> BackgroundWorker が提供する経過報告用のイベントである ProgressChanged イベントと、
> それを起こすための ReportProgress メソッドの使用を検討なさってはいかがでしょうか。

ネットで調べたのですが、使い方が分かりません。
ネットのサンプルはほとんどが、
Percentage (数値)を ProgressBar に表示するというサンプルばかりです。

今回のように、数字と文字列を ReportProgress メソッドで渡すには、
どのようにすれば良いでしょうか?

[ツリー表示へ]
タイトルRe^3: マルチスレッドでコントロールを操作する方法
記事No6303
投稿日: 2007/09/13(Thu) 19:03
投稿者Hongliang
> ネットで調べたのですが、使い方が分かりません。
> ネットのサンプルはほとんどが、
>  Percentage (数値)を ProgressBar に表示するというサンプルばかりです。
> 今回のように、数字と文字列を ReportProgress メソッドで渡すには、
> どのようにすれば良いでしょうか?

ReportProgress メソッドを調べれば、数値とともに任意のオブジェクトを渡すことのできるオーバーロードがあるのは分かると思います。
ProgressChanged イベントのイベント引数である ProgressChangedEventArgs クラスを調べれば、それぞれに対応するプロパティがあることが分かると思います。

ちなみに、このときに使う数値の方は一応任意の値を使用できるのですが、パーセンテージという前提なので、好き勝手に任意の値を渡すには抵抗があるかもしれません。
// イベントハンドラをもう一つ作ってこっちではプログレスバーを、となった場合混乱しますしね。
ですので、Integer と String を持つクラスを自作し、それを userState としてやりとりするのがいいでしょう。

[ツリー表示へ]
タイトルRe^4: マルチスレッドでコントロールを操作する方法
記事No6305
投稿日: 2007/09/14(Fri) 10:20
投稿者ちゃら
あらかじめ WorkerReportsProgress を True に設定しておき、

Me.Invoke(New DelegateGridRefresh(AddressOf GridRefresh), New Object() {i, "xxxx"})
        ↓
worker.ReportProgress(1, New Object() {i, "xxxx"})
# 第一引数 (percentProgress) は適当な値 (1)


Private Sub GridRefresh(ByVal row As Integer, ByVal value As String)
    ....
End Sub
    ↓
Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As Object, _
                  ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
                  Handles BackgroundWorker1.ProgressChanged
    Dim obj() As Object = TryCast(e.UserState, Object())
    Dim row As Integer = CInt(obj(0))
    Dim value As String = obj(1).ToString
    MainTable.Rows(row)("column1") = value
End Sub

とすることで出来ました。

userState ってこういう時の為に使用するのですね。
ありがとうございました。


# Integer 型と String 型のクラスを作ると
# ファイルも増える為、とりあえず Object() で
# コーディングしてしまいました。

# まだ、ヘンな部分がありましたら、ご指摘ください。

[ツリー表示へ]