2018年3月2日 星期五

BackgroundWorker 的使用

專案需求:一支常駐程式可以撈資料
想法大概是:一個 Timer 定期檢查、一個 ProgressBar 顯示進度、一個 backgroundworker 避免影響使用者、一個 NotifyIcon 可以出現桌面的右下角
程式寫法大致上是:

新增一個 Form,在上面放一個 ProgressBar
然後拉一個 Timer
一個 BackgroundWorker,並設定 WorkerReportsProgress = true,才可以修改 ProgressBar 的進度 然後在初始化後設定 form 的位置
public Form1()
{
    InitializeComponent();

    // 縮小視窗,以觸發 notifyIcon (不直接觸發是為了保留之後若要點擊縮小的 Icon 時可以恢復視窗)
    this.WindowState = FormWindowState.Minimized;
    this.TopLevel = true;
    this.screenWidth = Screen.PrimaryScreen.Bounds.Width;
    this.screenHeight = Screen.PrimaryScreen.Bounds.Height;
    //this.Location = new Point(screenWidth - Width - 5, screenHeight);

    //設定視窗位置在右下角最高可以顯示的位置, 工具列上方
    this.stopHightLocation = Screen.PrimaryScreen.Bounds.Height - (Screen.PrimaryScreen.Bounds.Height - Screen.PrimaryScreen.WorkingArea.Height + this.Height);
    this.Location = new Point(screenWidth - Width - 5, stopHightLocation);
}
在 BackgroundWorker (Name 設為 bw) 的事件上分別寫上
void bw_DoWork(object sender, DoWorkEventArgs e)
{
    object myParameter = e.Argument; // myParameter 是帶過來要用的參數
    // 開始執行要很久的動作
    ...(略)
    // 結束
}
void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    // 把視窗叫起來讓使用者看到進度條
    if (this.WindowState == FormWindowState.Minimized)
    {
        this.Show();    // 顯示視窗
        this.WindowState = FormWindowState.Normal;
    }
    // 修改進度條的進度
    progressBar1.Value = e.ProgressPercentage;
    // 以文字的方式顯示進度條的百分比
    label2.Text = string.Format("{0:f0}%", (progressBar1.Value / Convert.ToDouble(progressBar1.Maximum)) * 100);
}
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    this.WindowState = FormWindowState.Minimized;   // 隱藏視窗
    timer1.Enabled = true;  // 工作完成了,再次啟用 timer1
}
再來在 form load 事件內啟用 timer1
private void timer1_Tick(object sender, EventArgs e)
{
    // 如果背景程式忙碌中,就停止這次的背景
    if (bw.IsBusy)
        return;

    timer1.Enabled = false;  // 然後將 timer 停用

    // 這邊可以收集要的參數
    // 若收集參數也很耗時,則可以另開一個backgroundworker來做,然後在backgroundworker_completed事件內執行主要的忙碌工作
    // 或是將收集參數的工作也被到bw裡面,但此時可能不單純只顯示progressbar,可能還需要顯示進度的內容說明

    // 然後呼叫backgroundworker 執行 (可以加判斷是否真的需要呼叫背景程式,減少觸發次數)
    if (myParameter != null)
        bw.RunWorkerAsync(myParameter); 
}
後來看到另一篇文章寫到,原來也可以在 BackgroundWorker_Completed 事件中,再次執行耗時的工作即可,減少使用一個 timer 元件,
把timer1_Tick()事件用另一個function 包起來,然後稍微修改一下
void PrepareToRunWorkerAsync()
{
    // 如果背景程式忙碌中,就停止這次的背景
    if (bw.IsBusy)
        return;

    // 這邊可以收集要的參數
    // 若收集參數也很耗時,則可以另開一個backgroundworker來做,然後在backgroundworker_completed事件內執行主要的忙碌工作
    // 或是將收集參數的工作也被到bw裡面,但此時可能不單純只顯示progressbar,可能還需要顯示進度的內容說明

    // 然後呼叫backgroundworker 執行
    bw.RunWorkerAsync(myParameter); 
}
然後在 Form1() 建構式中直接執行上述的 function
public Form1()
{
    InitializeComponent();

    // 縮小視窗,以觸發 notifyIcon (不直接觸發是為了保留之後若要點擊縮小的 Icon 時可以恢復視窗)
    this.WindowState = FormWindowState.Minimized;
    this.TopLevel = true;
    this.screenWidth = Screen.PrimaryScreen.Bounds.Width;
    this.screenHeight = Screen.PrimaryScreen.Bounds.Height;
    //this.Location = new Point(screenWidth - Width - 5, screenHeight);

    //設定視窗位置在右下角最高可以顯示的位置, 工具列上方
    this.stopHightLocation = Screen.PrimaryScreen.Bounds.Height - (Screen.PrimaryScreen.Bounds.Height - Screen.PrimaryScreen.WorkingArea.Height + this.Height);
    this.Location = new Point(screenWidth - Width - 5, stopHightLocation);

    PrepareToRunWorkerAsync();
}
修改一下 BackgroundWorker_Completed 事件
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    this.WindowState = FormWindowState.Minimized;   // 隱藏視窗
    // 若使用者沒有下取消命令,則在Completed 事件下再次執行耗時的工作
    if (e.Cancelled == false)
        PrepareToRunWorkerAsync();

}
當然上述做法需再搭配一個啟動耗時工作的事件,不然一但取消後,就只有重開執行檔一途了
void buttonStart(object sender, EventArgs e)
{
    PrepareToRunWorkerAsync();
}
參考:C# 使用 BackgroundWorker 背景執行