Excel生成時にProgressBarを使って進捗状態を把握したい

タグの編集
投稿者 ロト君  (社会人) 投稿日時 2020/3/12 08:55:05
環境:
VB2019

内容:
AccessデータをVB2019を使ってExcelに変換するソフトを作っています。
VB側のDataGridViewに検索項目を出力してExcelの各セルに埋め込んで行く感じのソフトです。

その際に、大きな検索範囲だと動いているのか分からないので、ProgressBarを使って進捗状況を把握したいと思っています。
サンプルで作成した下記のモノは動きました。

    Private Sub Button4_Click(sender As Object, e As EventArgs) Handles Button4.Click

        pb_output.Value = 0
        pb_output.Minimum = 0
        pb_output.Maximum = 100

        Dim i As Int32
        For i = 0 To 100 Step 1
            System.Threading.Thread.Sleep(1000)
            pb_output.Value = i
        Next

    End Sub

しかし、実際にForの部分でExcelを生成させると、今まで通り、ProgressBarが止まったままで、Excel完成時に100%となってしまいます・・・。

Excel生成中にPregressBarの進捗を進めるためにはどうすればよいでしょうか?

教えてもらえると、有難いです。
宜しくお願い致します。
投稿者 kiku  (社会人) 投稿日時 2020/3/12 09:29:20

現在の変換処理は、ボタンの中で実行してると仮定して回答します。
ボタンの中でプログレスバーの状態を変化させていると思うのですが、
この場合、プログレスバーが表示が更新されるのは、
ボタンの処理がすべて終了した後になります。
なので、いきなり100%になります。

そもそも、ボタンの中で時間のかかる処理を行うべきではありません。
このようなことをした場合、ボタンの処理が終了しないと、
例えば、アプリ終了ボタンを押下しても、フリーズのような状態になり、
実行されません。

どのような構造が良いかということですが、
時間のかかる処理は、UIスレッドとは別のスレッドで実行する方が良いです。
バックグラウンドワーカーを使う例ですが、その方法のサンプルは下記になります。
いろいろとぐぐってみると良いです。
http://hensa40.cutegirl.jp/archives/2971

上記の例ですと、
BackgroundWorker1_DoWorkの中に
時間のかかる処理を記述すれば
プログレスバーが表示できるようになります。

まずは、上記のサンプルをそのまま記述し、
動作を確認してみてください。
投稿者 kiku  (社会人) 投稿日時 2020/3/12 10:28:52
下記にはもっと詳しくかかれています。
参考になると思うので貼っておきます。
https://dobon.net/vb/dotnet/programing/displayprogress.html
投稿者 ロト君  (社会人) 投稿日時 2020/3/12 11:26:35
kiku さん。ありがとうございます。

サンプルコードを試してみます。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2020/3/12 11:38:57
> ProgressBarを使って進捗状況を把握したいと思っています。

ProgressBar は通常、全体進捗と現在進捗率が分かる場合に
使われますが、その点は大丈夫でしょうか?

進捗率が不明な場合はマーキースタイルにする手もありますが、
それではあまり意味が無いでしょうし。


> サンプルで作成した下記のモノは動きました。

UI スレッドである Button1_Click 中に Sleep を呼び出すことは避けてください。
提示頂いた例だと、最低でも 1分41秒間はビジー状態になってしまう計算であり、
処理が終わるまで、フォームの移動や操作が阻害されることになります。


VB に限った話ではありませんが、Windows のメッセージ処理の設計上、
処理結果の多くは空き時間(アイドルタイム)に画面に反映されるようになっています。
つまりイベント処理が終わって End Sub に到達した後で描画されたりします。

中には即時反映されるものもありますが、基本的には長い処理は UI スレッドではなく
ワーカースレッドで実施する設計にするのが望ましいかと。

VB2019 とのことなので、Async / Await を使う方法をお奨めしておきます。
あるいは昔ながらの BackgroundWorker を使う方法でも実装できますね。


以下のサンプルでは、処理の中断や二重実行の制御は省略しています。

Imports System.ComponentModel
Imports System.Threading
Imports System.Threading.Tasks

Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Button1.Text = "元の実装"
        Button2.Text = "Aync/Await版"
        Button3.Text = "BackgrouneWorker版"
        BackgroundWorker1.WorkerReportsProgress = True
        ResetProgressBar()
    End Sub

    Private Sub ResetProgressBar(Optional min As Integer = 0, Optional max As Integer = 100, Optional current As Integer = 0)
        ProgressBar1.Minimum = min
        ProgressBar1.Maximum = max
        ProgressBar1.Value = current
    End Sub

    '長い処理を同期的に実行するメソッド 
    Private Sub LongTimeAction()
        Thread.Sleep(1000)
        Debug.WriteLine(Now.Ticks)
    End Sub

    '長い処理を非同期的に実行するメソッド 
    Private Function LongTimeActionAsync() As Task
        Return Task.Run(Sub()
                            Thread.Sleep(1000)
                            Debug.WriteLine(Now.Ticks)
                        End Sub)
    End Function

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ResetProgressBar()
        For i = 0 To 100
            LongTimeAction()
            ProgressBar1.Value = i
        Next
        MsgBox("終了")
    End Sub

    Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        ResetProgressBar()
        For i = 0 To 100
            Await LongTimeActionAsync()
            ProgressBar1.Value = i
        Next
        MsgBox("終了")
    End Sub

    Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
        BackgroundWorker1.RunWorkerAsync()
    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        Dim bgw = DirectCast(sender, BackgroundWorker)
        For i = 0 To 100
            LongTimeAction()
            bgw.ReportProgress(i)
        Next
        e.Result = "終了"
    End Sub
    Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
        ProgressBar1.Value = e.ProgressPercentage
    End Sub

    Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
        MsgBox(e.Result)
    End Sub
End Class




> VB側のDataGridViewに検索項目を出力してExcelの各セルに埋め込んで行く感じのソフトです。

Microsoft Excel を直接操作するのではなく、ClosedXML、EPPlus、ReoGrid 等の
Excel データあるいは Excel ファイルを読み書きできるライブラリを使うのはどうでしょう。
これらはインプロセスでの制御となるため、Microsoft Excel を直接制御するよりも高速です。
https://reogrid.net/jp/document/
https://www.atmarkit.co.jp/ait/articles/1810/24/news016.html

VB から Microsoft Excel を制御する場合、アウトプロセスとなるため、
Excel VBA に比べると通信コストが高めです。そのため、Excel のオブジェクトを
操作する必要がある場合は、プロパティやメソッドを呼び出す回数が、
できるだけ少なくなるように設計するべきです。

たとえばループ処理などで、Excel のセル 1 つ 1 つに値を書き込んでいくことと時間がかかりますが、
ループ処理などで二次元配列を作って起き、その配列を複数セルへ一括代入するようにすれば、
通信回数が 1 回にまとまるため、処理速度を向上できます。
投稿者 ロト君  (社会人) 投稿日時 2020/3/12 14:21:40
魔界の仮面弁士 さん。ありがとうございます。

>Microsoft Excel を直接操作するのではなく、ClosedXML、EPPlus、ReoGrid 等のExcel データあるいは Excel ファイルを読み書きできるライブラリを使うのはどうでしょう。

ClosedXMLを使って、Excelを生成しています。



Private Sub LongTimeAction()中に処理を入れる事は分かりましたが、
Excelのセルに入れ込む際に、
 
'シート名を指定してシートを取得
Dim sheet As ClosedXML.Excel.IXLWorksheet = workbook.Worksheet(1)

を、前もって読み込ませて
Private Sub LongTimeAction()中で、

sheet.Cell("A" & i).Value = i + 1

と、使えない感じです。

ローカルで宣言してるためでしょうか?
投稿者 ロト君  (社会人) 投稿日時 2020/3/12 14:38:09
>Private Sub LongTimeAction()中に処理を入れる事は分かりましたが、Excelのセルに入れ込む際に、

問題が解決出来ました!
処理が少なすぎた様でした。

ProgressBarは問題なく動きました。

みなさん。ありがとうございます!!
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2020/3/12 15:06:18
> ローカルで宣言してるためでしょうか?

『ローカル変数』だけを使うのであれば安全です。しかし、
『フィールド変数』をスレッド間で共有することは避けてください。

同一スレッド間であれば、フィールド変数を共有してもまったく問題ありませんが
複数のスレッドから同じフィールド変数を同時に読み書きするのは原則 NG です。

どうしても共有するなら、同時実行制御の仕組みを設けて、書き込み完了まで
他スレッドの読み取りを完全にロックするなど、排他制御をしっかり組み込む必要があります。


処理を分割できる場合は、Task(Of 進捗状況) を戻り値とする Async Function にして
それを繋いで処理するか、もしくは、Progress(Of 進捗状況) 型を利用する方法が使えます。
https://docs.microsoft.com/ja-jp/dotnet/api/system.progress-1
http://outside6.wp.xdomain.jp/2016/07/25/post-91/


あるいは先の例のように、BackgroundWorker などのイベントベースの通知機構を用いるとか、
もしくはワーカースレッドから Invoke メソッドを通じて実行させるという手もあります。
http://blog.livedoor.jp/gab_km/archives/765026.html

もしい¥ BackgroundWorker を使う場合は、過去に何度か注意点を
投稿していたと思うので、過去ログを検索してみてください。