2015-04-08 32 views
1

我正在使用WebClient對象的DownloadData從兩個網站下載favicon。WebClient DownloadData方法凍結表格

我收到一個字節數組的響應,一切運作順利,除了一兩件事:DownloadData方法被執行時,它會凍結我的形式,直到該方法返回

現在,我以一個BackgroundWorker對象來完成這項工作解決了這個,但我很好奇我怎麼會實現使用System.Threading.Thread同樣的事情。

我試着創建另一個線程,下載favicons,然後循環我的mainThread,直到線程完成處理,然後使用Abort()方法中止線程,但到目前爲止我的表單在執行期間被凍結其他線程。

這是我用來創建其他線程的代碼:

bool downloadFavIcon_Completed = false; 
    private void downloadFavIcon() 
    { 
     downloadFavIcon_Completed = false; 
     Byte[] dl; 
     System.IO.MemoryStream dlMem; 
     Bitmap favCollection = new Bitmap(96, 64); 
     Graphics g = Graphics.FromImage(favCollection); 
     Bitmap dlImg; 
     String[] addr = new String[24]; 
     addr[0] = @"http://google.com/favicon.ico"; 
     addr[1] = @"http://microsoft.com/favicon.ico"; 
     addr[2] = @"http://freesfx.com/favicon.ico"; 
     addr[3] = @"http://yahoo.com/favicon.ico"; 
     addr[4] = @"http://downloadha.com/favicon.ico"; 
     addr[5] = @"http://hp.com/favicon.ico"; 
     addr[6] = @"http://bing.com/favicon.ico"; 
     addr[7] = @"http://webassign.com/favicon.ico"; 
     addr[8] = @"http://youtube.com/favicon.ico"; 
     addr[9] = @"https://twitter.com/favicon.ico"; 
     addr[10] = @"http://cc.com/favicon.ico"; 
     addr[11] = @"http://stackoverflow.com/favicon.ico"; 
     addr[12] = @"http://vb6.us/favicon.ico"; 
     addr[13] = @"http://facebook.com/favicon.ico"; 
     addr[14] = @"http://flickr.com/favicon.ico"; 
     addr[15] = @"http://linkedin.com/favicon.ico"; 
     addr[16] = @"http://blogger.com/favicon.ico"; 
     addr[17] = @"http://blogfa.com/favicon.ico"; 
     addr[18] = @"http://metal-archives.com/favicon.ico"; 
     addr[19] = @"http://wordpress.com/favicon.ico"; 
     addr[20] = @"http://metallica.com/favicon.ico"; 
     addr[21] = @"http://wikipedia.org/favicon.ico"; 
     addr[22] = @"http://visualstudio.com/favicon.ico"; 
     addr[23] = @"http://evernote.com/favicon.ico"; 
     for (int i = 0; i < addr.Length; i++) 
     { 
      using (System.Net.WebClient client = new System.Net.WebClient()) 
      { 
       try 
       { 
        dl = client.DownloadData(addr[i]); 
        dlMem = new System.IO.MemoryStream(dl); 
        dlImg = new Bitmap(dlMem); 
       } 
       catch (Exception) 
       { 
        dlImg = new Bitmap(Properties.Resources.defaultFavIcon); 
       } 
      } 
      g.DrawImage(dlImg, (i % 6) * 16, (i/6) * 16, 16, 16); 
     } 
     passAddDisplay.Image = favCollection; 
     downloadFavIcon_Completed = true; 
    } 

    private void button2_Click(object sender, EventArgs e) 
    { 
     Thread downloader = new Thread(new ThreadStart(downloadFavIcon)); 
     downloader.Start(); 
     while (!downloader.IsAlive) ; 
     while (!downloadFavIcon_Completed) ; 
     downloader.Abort(); 
    } 

注:passAddDisplay已經放在我的窗體上的圖片框。

如何改進我的應用程序以避免在執行WebClient.DownloadData期間被凍結? (我不想使用Application.DoEvents())

+0

應該存在運行時錯誤,因爲您正在從後臺線程'downloader'訪問UI元素,這是不允許的。你如何處理這個問題?你是否通過設置'this.CheckForIllegalCrossThreadCalls = false'來壓制它? – kennyzx

+0

什麼UI元素?你在談論passAddDisplay嗎? –

+0

是的,我的意思是Picturebox。 – kennyzx

回答

0

歡迎堆棧溢出...

通過檢查你的循環很明顯,可以有效地防止循環語句中的UI線程。

while (!downloader.IsAlive) ; 
while (!downloadFavIcon_Completed) ; 

是您的UI鎖定的原因。理想情況下,這對於後臺工作人員來說是一份工作,因爲它被設計爲在後臺運行,並提供事件以便返回到UI線程。這可以使用Thread編寫,但是應該使用後臺工作器。

現在,如果你真的想用Thread對象來寫這個,那麼我建議你爲你的下載器創建一個類並創建事件。因此,我們可以寫簡單的事件,如

  1. Completed事件
  2. 取消事件
  3. UI進度事件

我不推薦這種方法(讀進一步下跌)

簡單首先創建一個新類Downloader,這裏是一個示例(最小值)

public class Downloader 
{ 

    /// <summary> 
    /// Delegate Event Handler for the downloading progress 
    /// </summary> 
    /// <param name="sender"></param> 
    /// <param name="e"></param> 
    public delegate void DownloaderProgressEventHandler(Downloader sender, DownloaderProgressEventArgs e); 

    /// <summary> 
    /// Delegate Event Handler for the completed event 
    /// </summary> 
    /// <param name="sender"></param> 
    /// <param name="e"></param> 
    public delegate void DownloaderCompletedEventHandler(Downloader sender, DownloaderCompletedEventArgs e); 

    /// <summary> 
    /// The completed event 
    /// </summary> 
    public event DownloaderCompletedEventHandler Completed; 

    /// <summary> 
    /// The cancelled event 
    /// </summary> 
    public event EventHandler Cancelled; 

    /// <summary> 
    /// the progress event 
    /// </summary> 
    public event DownloaderProgressEventHandler Progress; 

    /// <summary> 
    /// the running thread 
    /// </summary> 
    Thread thread; 

    /// <summary> 
    /// the aborting flag 
    /// </summary> 
    bool aborting = false; 

    //the addresses 
    String[] addr = new String[] { 
     "http://google.com/favicon.ico", 
     "http://microsoft.com/favicon.ico", 
     "http://freesfx.com/favicon.ico", 
     "http://yahoo.com/favicon.ico", 
     "http://downloadha.com/favicon.ico", 
     "http://hp.com/favicon.ico", 
     "http://bing.com/favicon.ico", 
     "http://webassign.com/favicon.ico", 
     "http://youtube.com/favicon.ico", 
     "https://twitter.com/favicon.ico", 
     "http://cc.com/favicon.ico", 
     "http://stackoverflow.com/favicon.ico", 
     "http://vb6.us/favicon.ico", 
     "http://facebook.com/favicon.ico", 
     "http://flickr.com/favicon.ico", 
     "http://linkedin.com/favicon.ico", 
     "http://blogger.com/favicon.ico", 
     "http://blogfa.com/favicon.ico", 
     "http://metal-archives.com/favicon.ico", 
     "http://wordpress.com/favicon.ico", 
     "http://metallica.com/favicon.ico", 
     "http://wikipedia.org/favicon.ico", 
     "http://visualstudio.com/favicon.ico", 
     "http://evernote.com/favicon.ico" 
    }; 


    /// <summary> 
    /// Starts the downloader 
    /// </summary> 
    public void Start() 
    { 
     if (this.aborting) 
      return; 
     if (this.thread != null) 
      throw new Exception("Already downloading...."); 
     this.aborting = false; 

     this.thread = new Thread(new ThreadStart(runDownloader)); 
     this.thread.Start(); 

    } 

    /// <summary> 
    /// Starts the downloader 
    /// </summary> 
    /// <param name="addresses"></param> 
    public void Start(string[] addresses) 
    { 
     if (this.aborting) 
      return; 

     if (this.thread != null) 
      throw new Exception("Already downloading...."); 

     this.addr = addresses; 
     this.Start(); 
    } 

    /// <summary> 
    /// Aborts the downloader 
    /// </summary> 
    public void Abort() 
    { 
     if (this.aborting) 
      return; 
     this.aborting = true; 
     this.thread.Join(); 
     this.thread = null; 
     this.aborting = false; 

     if (this.Cancelled != null) 
      this.Cancelled(this, EventArgs.Empty); 
    } 

    /// <summary> 
    /// runs the downloader 
    /// </summary> 
    void runDownloader() 
    { 
     Bitmap favCollection = new Bitmap(96, 64); 
     Graphics g = Graphics.FromImage(favCollection); 

     for (var i = 0; i < this.addr.Length; i++) 
     { 
      if (aborting) 
       break; 

      using (System.Net.WebClient client = new System.Net.WebClient()) 
      { 
       try 
       { 
        byte[] dl = client.DownloadData(addr[i]); 
        using (var stream = new MemoryStream(dl)) 
        { 
         using (var dlImg = new Bitmap(stream)) 
         { 
          g.DrawImage(dlImg, (i % 6) * 16, (i/6) * 16, 16, 16); 
         } 
        } 
       } 
       catch (Exception) 
       { 
        using (var dlImg = new Bitmap(Properties.Resources.defaultFacIcon)) 
        { 
         g.DrawImage(dlImg, (i % 6) * 16, (i/6) * 16, 16, 16); 
        } 
       } 
      } 
      if (aborting) 
       break; 

      if (this.Progress != null) 
       this.Progress(this, new DownloaderProgressEventArgs 
       { 
        Completed = i + 1, 
        Total = this.addr.Length 
       }); 
     } 

     if (!aborting && this.Completed != null) 
     { 
      this.Completed(this, new DownloaderCompletedEventArgs 
      { 
       Bitmap = favCollection 
      }); 
     } 
     this.thread = null; 
    } 

    /// <summary> 
    /// Downloader progress event args 
    /// </summary> 
    public class DownloaderProgressEventArgs : EventArgs 
    { 
     /// <summary> 
     /// Gets or sets the completed images 
     /// </summary> 
     public int Completed { get; set; } 

     /// <summary> 
     /// Gets or sets the total images 
     /// </summary> 
     public int Total { get; set; } 
    } 

    /// <summary> 
    /// Downloader completed event args 
    /// </summary> 
    public class DownloaderCompletedEventArgs : EventArgs 
    { 
     /// <summary> 
     /// Gets or sets the bitmap 
     /// </summary> 
     public Bitmap Bitmap { get; set; } 
    } 

} 

現在這是代碼的分配,但讓我們快速看看它。首先我們爲我們的Completed和Progress事件定義了2個代表。這些代表接受下載器實例作爲底部列出的發件人和特殊事件參數類。隨後是我們的3個事件(如上所列),這些事件將用於向下載器發出信號變化。

接下來我們定義了我們的字段。

Thread thread;這是對調用'Start()`方法時將創建的線程的引用。

bool aborting = false;這是一個簡單的標誌,用來向線程表示我們應該中止的信號。現在我決定使用一個標誌,讓線程正常完成,而不是調用Thread.Abort()方法。這確保了所有的清理工作都可以正常進行。

string[] addres =....我們的初始地址。

到目前爲止,這一切都很簡單。接下來是我們的Start()方法。我提供了兩種不同的方法。其中一種方法接受一個新的地址下載(不重要)string[]

您將在此方法

/// <summary> 
/// Starts the downloader 
/// </summary> 
public void Start() 
{ 
    if (this.aborting) 
     return; 
    if (this.thread != null) 
     throw new Exception("Already downloading...."); 
    this.aborting = false; 

    this.thread = new Thread(new ThreadStart(runDownloader)); 
    this.thread.Start(); 
} 

我們做的第一件事是檢查是否中止標誌設置通知。如果它是忽略開始呼叫(你可以拋出異常)。接下來我們檢查線程是否不爲空。如果線程不爲空​​,那麼我們的下載程序正在運行。最後,我們簡單地將我們的中止標誌重置爲false並開始我們的新Thread

向下移動到Abort()方法。該方法將首先檢查是否設置了aborting標誌。如果是這樣,那麼什麼都不做。接下來,我們將aborting標誌設置爲true。下一步,我會提醒你這個阻止你的調用線程調用方法Thread.Join()這將加入到我們的調用線程的線程。基本上等待線程退出。

最後,我們只需將線程實例設置爲空,將aborting標誌重置爲false並觸發Cancelled事件(如果訂閱)。

接下來是下載的主要方法。首先你會注意到我移動了你的變量,並使用using語句來處理一次性對象。 (這是另一個話題)。

runDownloader()方法的一大特點是它會定期檢查'中止'標誌。如果此標誌設置爲true,則downloader停在此處。現在請注意,您可能會遇到在WebClient下載圖像時調用中止的情況。理想情況下,您會讓WebClient完成請求,正確處置然後退出循環。

每次下載圖像後,都會觸發進度事件(如果訂閱)。最後,當迭代完成並下載所有圖像時,「已完成」事件將與編譯後的位圖圖像一起觸發。

停頓了BREATHE

現在,這是所有偉大的..但你如何使用它。簡單地說,我使用按鈕,進度條和圖片框創建了一個表單。該按鈕將用於啓動和停止下載器,進度條處理進度事件和完成圖像的圖片框。

這裏是我已經評論過它的一部分的示例程序。

public partial class Form1 : Form 
{ 
    public Form1() 
    { 
     InitializeComponent(); 

     this.progressBar1.Visible = false; 
     this.progressBar1.Enabled = false; 
    } 

    Downloader downloader; 

    /// <summary> 
    /// starts \ stop button pressed 
    /// </summary> 
    /// <param name="sender"></param> 
    /// <param name="e"></param> 
    private void button1_Click(object sender, EventArgs e) 
    { 
     //if downloader is not null then abort it 
     if (downloader != null) 
     { 
      downloader.Abort(); 
      return; 
     } 

     //setup and start the downloader 
     this.progressBar1.Value = 0; 
     this.progressBar1.Minimum = 0; 
     this.progressBar1.Enabled = true; 
     this.progressBar1.Visible = true; 
     this.downloader = new Downloader(); 
     this.downloader.Progress += downloader_Progress; 
     this.downloader.Completed += downloader_Completed; 
     this.downloader.Cancelled += downloader_Cancelled; 
     this.downloader.Start(); 
    } 

    /// <summary> 
    /// downloader cancelled event handler 
    /// </summary> 
    /// <param name="sender"></param> 
    /// <param name="e"></param> 
    void downloader_Cancelled(object sender, EventArgs e) 
    { 
     this.unhookDownloader(); 

     if (this.InvokeRequired) 
      this.Invoke((MethodInvoker)delegate 
      { 
       this.progressBar1.Enabled = false; 
       this.progressBar1.Visible = false; 
       MessageBox.Show(this, "Cancelled"); 
      }); 
     else 
     { 
      this.progressBar1.Enabled = false; 
      this.progressBar1.Visible = false; 
      MessageBox.Show(this, "Cancelled"); 
     } 

    } 

    /// <summary> 
    /// downloader completed event handler 
    /// </summary> 
    /// <param name="sender"></param> 
    /// <param name="e"></param> 
    void downloader_Completed(Downloader sender, Downloader.DownloaderCompletedEventArgs e) 
    { 
     this.unhookDownloader(); 
     if (this.InvokeRequired) 
      this.Invoke((MethodInvoker)delegate 
      { 
       this.progressBar1.Enabled = false; 
       this.progressBar1.Visible = false; 
       this.pictureBox1.Image = e.Bitmap; 
       MessageBox.Show(this, "Completed"); 
      }); 
     else 
     { 
      this.progressBar1.Enabled = false; 
      this.progressBar1.Visible = false; 
      this.pictureBox1.Image = e.Bitmap; 
      MessageBox.Show(this, "Completed"); 
     } 
    } 

    /// <summary> 
    /// downloader progress event handler 
    /// </summary> 
    /// <param name="sender"></param> 
    /// <param name="e"></param> 
    void downloader_Progress(Downloader sender, Downloader.DownloaderProgressEventArgs e) 
    { 
     if (this.progressBar1.InvokeRequired) 
      this.progressBar1.Invoke((MethodInvoker)delegate 
      { 
       this.progressBar1.Value = e.Completed; 
       this.progressBar1.Maximum = e.Total; 
      }); 
     else 
     { 
      this.progressBar1.Value = e.Completed; 
      this.progressBar1.Maximum = e.Total; 
     } 

    } 

    /// <summary> 
    /// unhooks the events handlers and sets the downloader to null 
    /// </summary> 
    void unhookDownloader() 
    { 
     this.downloader.Progress -= downloader_Progress; 
     this.downloader.Completed -= downloader_Completed; 
     this.downloader.Cancelled -= downloader_Cancelled; 
     this.downloader = null; 
    } 
} 

這是一個簡單的實現,你怎麼可以使用Thread對象做你的工作。在我看來,這是太多的工作。現在讓我們在後臺工作人員中完成。

我強烈推薦這種方法

爲什麼你可能會說什麼?那麼後臺工作者向我們提供的測試支持方法(加上更多),我們試圖實現。你會注意到下載圖像和檢測取消的實際工作是相同的。但是您會注意到,在發佈已完成和進度事件時,我並不擔心(儘可能多)有關跨線程問題。

private void button2_Click(object sender, EventArgs e) 
{ 
    if (this.worker != null && this.worker.IsBusy) 
    { 
     this.worker.CancelAsync(); 
     return; 
    } 

    string[] addr = new string[] {".... our full addresss lists" }; 

    this.progressBar1.Maximum = addr.Length; 
    this.progressBar1.Value = 0; 
    this.progressBar1.Visible = true; 
    this.progressBar1.Enabled = true; 
    this.worker = new BackgroundWorker(); 
    this.worker.WorkerSupportsCancellation = true; 
    this.worker.WorkerReportsProgress = true; 
    this.worker.ProgressChanged += (s, args) => 
    { 
     this.progressBar1.Value = args.ProgressPercentage; 
    }; 

    this.worker.RunWorkerCompleted += (s, args) => 
    { 
     this.progressBar1.Visible = false; 
     this.progressBar1.Enabled = false; 

     if (args.Cancelled) 
     { 
      MessageBox.Show(this, "Cancelled"); 
      worker.Dispose(); 
      worker = null; 
      return; 
     } 

     var img = args.Result as Bitmap; 
     if (img == null) 
     { 
      worker.Dispose(); 
      worker = null; 
      return; 
     } 
     this.pictureBox1.Image = img; 

     MessageBox.Show(this, "Completed"); 
     worker.Dispose(); 
     worker = null; 
    }; 

    this.worker.DoWork += (s, args) => 
    { 
     Bitmap favCollection = new Bitmap(96, 64); 
     Graphics g = Graphics.FromImage(favCollection); 

     for (var i = 0; i < addr.Length; i++) 
     { 
      if (worker.CancellationPending) 
       break; 

      using (System.Net.WebClient client = new System.Net.WebClient()) 
      { 
       try 
       { 
        byte[] dl = client.DownloadData(addr[i]); 
        using (var stream = new MemoryStream(dl)) 
        { 
         using (var dlImg = new Bitmap(stream)) 
         { 
          g.DrawImage(dlImg, (i % 6) * 16, (i/6) * 16, 16, 16); 
         } 
        } 
       } 
       catch (Exception) 
       { 
        using (var dlImg = new Bitmap(Properties.Resources.defaultFacIcon)) 
        { 
         g.DrawImage(dlImg, (i % 6) * 16, (i/6) * 16, 16, 16); 
        } 
       } 
      } 
      if (worker.CancellationPending) 
       break; 

      this.worker.ReportProgress(i); 
     } 

     if (worker.CancellationPending) 
     { 
      g.Dispose(); 
      favCollection.Dispose(); 
      args.Cancel = true; 
      return; 
     } 
     args.Cancel = false; 
     args.Result = favCollection; 
    }; 
    worker.RunWorkerAsync(); 
} 

我希望這有助於理解一些可能的方法來實現您想實現的目標。

-Nico

+0

感謝您花時間詳細回答我的問題,我很感激。這真讓我感到困惑。 正如我所提到的,我已經通過使用BackgroundWorker解決了它,但我必須說,你比我的完整得多。 我知道現在該怎麼做! 再次感謝您。 :) –

1

是的,它是一種同步方法,它使線程在不處理消息的情況下等待,直到它返回;你應該嘗試它的異步版本,而不是。

+0

感謝您的回答@nathan –

0

我會考慮使用Microsoft的Reactive Framework。它會自動處理後臺線程,並將非常有效地清理所有一次性引用。 NuGet「Rx-Main」&「Rx-WinForms」/「Rx-WPF」。

首先,先從你的地址的數組:

var addr = new [] 
{ 
    "http://google.com/favicon.ico", 
    // DELETED FOR BREVITY 
    "http://evernote.com/favicon.ico", 
}; 

現在,定義查詢以異步方式去讓你的圖片:

var query = 
    from a in addr.ToObservable().Select((url, i) => new { url, i }) 
    from dl in Observable 
     .Using(
      () => new System.Net.WebClient(), 
      wc => Observable.FromAsync(() => wc.DownloadDataTaskAsync(a.url))) 
    from bitmap in Observable 
     .Using(
      () => new System.IO.MemoryStream(dl), 
      ms => Observable.Start(() => new Bitmap(ms))) 
     .Catch(ex => Observable.Return(new Bitmap(Properties.Resources.defaultFavIcon))) 
    select new { x = (a.i % 6) * 16, y = (a.i/6) * 16, bitmap }; 

最後,等待所有圖像的進來,然後在UI線程上創建合成圖像,並將其分配給控制器passAddDisplay

query 
    .ToArray() 
    .ObserveOn(passAddDisplay) 
    .Subscribe(images => 
    { 
     var favCollection = new Bitmap(96, 64); 
     using(var g = Graphics.FromImage(favCollection)) 
     { 
      foreach (var image in images) 
      { 
       g.DrawImage(image.bitmap, image.x, image.y, 16, 16); 
       image.bitmap.Dispose(); 
      } 
     } 
     passAddDisplay.Image = favCollection; 
    }); 

我測試了查詢,它工作正常。