這是一個程式"中鳥"開發Windows Form要面對的問題...

Windows Form裡的Threading有些討厭的限制。菜鳥還處於天真無邪的Single Thread打天下階段,渾然不知它的險惡;而老鳥早就吃過苦頭,熟知要如何對付它,所以也不畏懼它的刁難。而剛開始學會在Windows Form中展現Multi-threading威力的中鳥,一起步多半要先闖過這一關。

用一個最簡單的例子來說明好: 我寫了一個Windows Form,放了一個label1一個button1, 在button1_Click事件另外建立一個Thread呼叫chgLabel(text)修改label1的Text屬性:

        private void button1_Click(object sender, EventArgs e)
        {
            Thread trd = new Thread(new ThreadStart(jobBad));
            trd.Start();
        }
 
        private void jobBad() 
        {
            chgLabel("Wrong way.");
        }
 
        private void chgLabel(string text)
        {
            label1.Text = text;
        }

程式碼很簡單,看起來應該沒什麼問題吧?

如果你這麼想,表示你在Windows Form的世界還涉世未深...

這段程式觸犯了Windows Form裡的一條鐵律: 在非UI Thread裡更動了UI的內容!

從沒用C++, Windows SDK角度探索Windows Application Model的我,並不知道它的細節原由,但歷經多次慘痛的經驗教訓,我學到一點: 如果你另外開了一條Thread,程式碼如會變更到Windows Form上的Control內容,請記得繞道而行。在上述的例子中,我們很明確地修改了Label的內容,但有時程式碼會在你不知情的情況下更動了UI元素。最典型的例子是DataGrid Bind到一個DataTable,而我們在另一條Thread中表面上只更改了DataTable的內容,並沒有修改UI上的Control;但由於Data Binding的關係,一更動DataTable就會間接觸發了修改DataGrid顯示的事件,進而違反了在非UI Thread修改UI內容的規則。

記得在.NET 1.1時代,違反這條規則並非絕對會出錯,而是偶爾冒出Null Reference Exception讓你丈二金剛摸不著腦袋,完全不知自己犯了什麼天條遭此報應。.NET 2.0比較仁道一點,會很明確地彈出以下的訊息:
Invalid Operation Exception:
Cross-thread operation not valid: Control 'label1' accessed from a thread other than the thread it was created on.

這張罰單把違規事由寫得清清楚楚,想當初由Null Reference去追出問題出在UI Thread限制就讓我吃盡苦頭,由.NET 2.0入門的朋友算是幸福多了。

Good! 知道問題後,要怎麼修改? 官方的標準答案是宣告一個deleate, 再用InvokeBeginInvoke來觸發它,這樣可以強迫Windows切回UI Thread來執行程式

        private void button1_Click(object sender, EventArgs e)
        {
            Thread trd = new Thread(new ThreadStart(jobGood));
            trd.Start();
        }
 
        private void chgLabel(string text)
        {
            label1.Text = text;
        }
 
        delegate void ChgLabelHandler(string text);
        private void jobGood()
        {
            this.Invoke(new ChgLabelHandler(chgLabel), "Correct way.");
        }

還有一些進階的課題,例如: 某些Code可能被UI Thread呼叫,也可能被非UI Thread執行,所以可以用InvokeRequired來偵測,決定要直接變更UI內容或是使用Invoke,這裡就先不要搞得太複雜,以免嚇到大家。先記住一點,當你遇到"Cross-thread operation not valid",要知道發生了什麼事以及該怎麼辦,就夠了。


Comments

# by kennyshu

我在菜鳥的時候就遇到了這個問題(現在還是菜) >"< 之前用.Net 1.1的時候(也就是VS2003)直接改UI上的control都沒有問題,升級到2.0後就出現一堆問題了,當時真的覺得2.0比1.1還不好用… 研究了好久並且請教一些先進之後,目前我是用BeginInvoke + MethodInvoker來解決這個問題。

# by Robert

除了比較複雜的狀況, 我都用BackgroupWorker這個Component. 感覺上不用傷腦筋, Code也比較好看...ㄟ...不過可能也是我比較懶惰吧...哈

# by Jeffrey

To Robert, 沒錯,.NET 2.0的BackgroundWorker又是一個"傑克,這真的是太神奇了"級的新發明,事實上我也正在寫一篇相關的文章。 現在的程式開發人員需要懂的東西愈來愈少,卻可以做出同樣複雜的東西,真是幸福,但相對也少了深入了解背後原理的機會,某種角度來說也是種不幸吧?? (謎之聲: 只有你這種愛鑽牛角尖的賤骨頭才會這樣想吧? 哈!)

# by Ken Wu

錯別字deleate -> delegate 推薦幾篇2002~2004年相關的官方文章: http://msdn2.microsoft.com/en-us/library/ms998490.aspx http://msdn2.microsoft.com/en-us/library/ms996402.aspx http://msdn2.microsoft.com/en-us/library/ms996483.aspx

# by SGY

Test . Runtime 可執行Change UI !! (真的改變了) Debug Mode: 會Error!!

# by SGY

When App 在 TaskTray this.Visual =false When App 不在 TaskTray this.Visual =false Use thread Update UI時要小心 private void button1_Click(object sender, EventArgs e) { Thread trd = new Thread(new ThreadStart(jobGood)); trd.Start(); } private void chgLabel(string text) { label1.Text = text; } delegate void ChgLabelHandler(string text); private void jobGood() { this.Invoke(new ChgLabelHandler(chgLabel), "Correct way."); } 改良一下 private void jobGood() { if (this.Visible) { this.Invoke(new ChgLabelHandler(chgLabel), "Correct way."); } else { chgLabel("Correct way."); } } 這樣就完美了 ^^ By:SGY

# by Jeffrey

to SGY, 謝謝你的補充,我有另一篇較完整的探討文章,也歡迎你參考指教。 http://blog.darkthread.net/blogs/darkthreadtw/archive/2008/03/26/better-winform-ui.aspx

# by Samuel

請問在 .net C++ 要如何做出此功能

# by Pao

謝謝! 寫的簡潔有利 對我這菜鳥幫助頗大 感恩

# by vix

請問前輩 我最近就遇到這個問題 我在另一個thread裡調用一個函式 裡面用了 AnimateWindow(this.Handle, 1000, AW_SLIDE + AW_VER_NEGATIVE); 錯誤就出現在這 當我在建構式加上Form.CheckForIllegalCrossThreadCalls = false; 沒出現問題 但是視窗在下降時 AnimateWindow(this.Handle,1000,AW_SLIDE + AW_VER_POSITIVE + AW_HIDE); rect會畫錯而產生畫面好像抖動感 不知前輩能給些意見嗎? 感激不盡

# by vix

忘了說小的用的是System.timers 謝謝

# by Jeffrey

to vix, 不太確定你AnimateWindow的寫法,但依我的直覺,應該是AnimateWindow的執行效率還不夠好,所以重新產生畫面時產生抖動感,不過還能有沒有改良空間就很難說了。

# by vix

感謝前輩指點,問題已經解決 但是我目前遇到另一個問題 因為看了一些網路上的文提到 使用Form.CheckForIllegalCrossThreadCalls = false; 是不安全的(其實我不清楚不安全的地方是什麼) 所以改以另一個thread去執行delegate的方式 delegate裡跑的是使用invoke()去跑包含了animatewindow的函式 但是這樣當執行到animatewindow函式時 一樣佔用了UI Thread 所有form的操作還是要等到animatewindow執行完才能再操作 不知道是我觀念錯誤寫錯了 還是invoke把animatewindow拉回UI Thread執行不可避免的會佔掉 還是有什麼辨法能讓animatewindow在另一個thread執行而沒有警告發生呢? 謝謝前輩

# by Jeffrey

to vix, 依我淺薄的WinForm開發經驗(我WebForm玩的比較多),跨Thread去動UI的東西會產生"在某些你不知道的特定條件下不定期出錯"的問題,這種Bug查起來叫人很想死。所以,其實我很贊成設定CheckForIllegalCrossThreadCalls=true,可以減少誤入險境的機會。 Invoke時,就是將工作交給UI Thread去處理,UI Thread只有一條,所以排隊執行是免不了的,依你說的情況,我想得到的策略是將耗時的運算交給其他Thread去處理,只留最後真正要更動UI元素的一小部分再用Invoke,儘可能減少佔用UI Thread的時間。

Post a comment