摘要
1.是什么導致我們需要提供一個自定義的消息框?
2.說說我的大致思路
3.你的亮點在哪里?
4.難道就是這些嗎?
下載本文講述的項目源碼包
是什么導致我們需要提供一個自定義的消息框?
最初產生這樣一個需求是源於項目經理的近乎白痴般的要求,有一天,他告訴我說那個彈出來的消息框太小了。我告訴他,這是系統自帶的東西,大小隨着給定文本內容的長度自動變化,他說不行,給弄大點,而且字體也太小,換個字體吧!我的天,難道架構師都是從來不真實寫代碼的嗎?
接 到這個要求,想想也許不大難吧。先看看系統給了我什么接口:MessageBox位於System.Windows.Forms命名空間內,是一個被密封了的靜態類,能夠使用的公共方法也就是MessageBox.Show,倒是有很多重載版本,也有一個選項參數,但該參數卻沒提供我真正感興趣的東西。
碰 到問題我還是老規矩,首先請教Google,然后直接上codeproject或sourceforge查找,一般的問題都可以解決。一看,原來大家都曾 有過這樣的需求啊;再來看別人的解決方法。大部分人使用Hook,也就是系統消息攔截,獲取消息框的句柄,然后使用winApi篡改該窗體的某些特征;這樣的示例非常多,而且有着非常有特色的特征:可以在上面添加個comboBox,告訴它Don't show this in the next time;可以賦予按鈕自己想要的文本值;也能修改一些文本的字體和字號屬性;
看起來這就是我要的解決方案,因為它幾乎能做一切;作為 windows上的新手,我於是迫切需要去弄弄Hook,這東西也不難;可問題在於,我發現無論怎么去嘗試修改那些作者使用Hook做的示例,最后的結果仍然是:該消息框上所有使用文本的地方最終都是同一個變化!也就是說你如果把Message Text部分的字體弄成Vadana,按鈕上的文本的字體也會變為Vadana,這居然是一體的!
很好,我正不想弄Hook,這東西我就沒弄成功過,別人例子都行,我就是不行!
也罷,逼急了,我自己來搞一個。
說說我的大致思路
.NET下畫個窗體不難,事實上拖下鼠標就行了;問題在於一樣東西總是牽扯到其它的很多東西。
ok,看看我自己的MessageBox究竟需要什么,其實需要的就是盡可能模仿系統自帶的消息框,最好就是完全取代之。
必須能夠自定義按鈕的數目,給出不同的提示圖標(錯誤,詢問,信息等),最好還能發出相應的系統聲音,消息框消失后獲取按鈕的某種枚舉值,必須能夠為其指定一個父窗體(作為此上的模式窗體),必須能夠安全的在非UI線程下使用。大概就是這些吧。
自定義按鈕的數目
按鈕的數目會影響它們在窗體上的位置布局,於是需要知道單個按鈕的寬度,相鄰2個按鈕之間的位置差。
在源代碼中我提供了這樣一個枚舉:















因為我發現大多數情況下,這樣的按鈕組合足夠應付需要了。
我發現我無法獲取系統默認的按鈕的大小,於是我作了個假設:



以上同時包含了按鈕之間的間距值。
自定義錯誤提示圖標
目的是一眼看出來發生了什么:錯誤?提示性信息?詢問選擇?
為此我拖了一個imageList組件,嵌入了一組圖標,並定義了如下枚舉:


























需要注意的是:這些枚舉值與我的
imageList中的image的順序是一一對應的,如果你要添加你自己的圖標,你就應該保持這種對應關系。
彈出窗體的時候發出聲音
這個功能完全是模仿來的,看代碼吧





















獲取窗體關閉時的DialogResult值
這個值很重要,它將反饋給調用者,以輔助其作出某種相應的決策。在這里我直接使用了系統框架類中的DialogResult枚舉,在窗體中寫死了每個按鈕的DialogResult值。
為之提供父窗體參數
這個本身很簡單,任何窗體的ShowDialog都有一個owner參數,我也提供就行了。可是你將會發現,如果你身處該父窗體所在的UI線程中,這樣調用沒有任何的不適,你甚至不需要為之提供owner參數,缺省的是將當前窗體作為父窗體。一切都運行良好;但是在非UI線程中,你就不能這么做了。看下文。
非UI線程中使用它
假定我們已經提供了ShowDialog方法,事實上我提供了該方法的很多重載版本,其易用性比系統自帶的還好。在非UI線程中使用它時,你有2個選擇:為之提供owner參數或者不提供;如果不提供,它就無法和你的UI主窗體聯系起來,它不會擋在任何窗體的前面,成為其之上的模式窗體,這讓人感覺很不爽,我發現尤其要解決這一點;那就提供這個參數就可以了嘛,把主窗體的Handle給它不就完了嘛,哈,這樣一來,你的程序只能偶爾的工作。大家應該都知道,這就是跨線程訪問控件的問題,原來問題在於,如果需要在A窗體上顯示一個模式窗體b,這個任務必須委托給A窗體UI線程執行才行!請注意這句話,我花了很長時間才明確這一點!
ok,你明白了,你需要一個委托來做這件事情,很好,你在A窗體的非UI線程的代碼中定義一個委托對象,使用某個方法(b窗體的ShowDialog方法)將其實例化,然后通過FormA.Invoke()來委托給A的UI線程來執行。
沒錯,這就是標准的解決方案,大家一直都是這樣做的,.NET的MSDN就告訴並建議我們這樣做。可是我們需要的消息框是一個獨立的組件,你難道要求每個使用該組件的程序都去提供一個委托來在非UI線程中顯示這樣一個消息框嗎?很麻煩不是嗎?我就希望在任何線程中,我給你一個Handle,你給我擋在這個Handle的窗體前面顯示就行了。於是這個問題成為這個小項目的難點。
這是本文最重要的一個部分。我花了很長時間領悟到,我希望你只需要不到1分鍾就能理解並記住。
上面說了,.NET的這種跨線程調用機制要求我們必須使用委托來實現:如果需要在A窗體上顯示一個模式窗體b,那么請將該任務委托給A的UI線程來執行!
委托對象放在調用者方會帶來額外的編碼,於是我把它放進了我自己的組件里,也就是這個自定義的消息框中!














這樣看起來,你的消息框確實比系統的要強,可以在非UI線程下方便的使用;但也就是一個很基本的消息框,那些Hook示例所展示的似乎比你的還要吸引人。
沒錯,以上Hook示例中展示了很多很有用的技術:添加comboBox,讓窗體在一段時間沒有被點擊后自動消失(同時返回某個默認的DialogResult值),可以替換按鈕上的文本字體。
可是這一切對於一個完全由你自己開發的窗體程序而言,是個難題么?太簡單了,當然我把一些東西做死了,其實如果你想獲得高度自定義的功能,你為自己的Show方法提供一個MsgBoxOptions參數就可以了,源代碼在你自己手里,沒有什么是不能做的!
當然這取決於你的需要了,就我而言,這已經非常足夠了,我一直在用我自己編寫的消息框,太方便了!贊一個自己先 :-)
后記
最初我使用的是Singleton模式,可是在很多地方可能先后都要求彈出一個窗體,而先前的這個窗體尚未消失,這就帶來了一個問題,.NET拋出了這樣一個異常:已經顯示的模式窗體不能夠再次被顯示(大意如此)。經過自己觀察,我發現系統自帶的消息框類似於每次需要時創建,然后丟棄之,也就是說每個使用的地方都是一個新的消息框。於是就演化成了現在的這樣一個版本。
謝謝各位大蝦和菜鳥的閱讀,祝您使用愉快!
(全文完,頁面上會提供源碼包下載)