Q: 顯示一個WinForms閃屏(Splash Screen)
我的應用程式需要一定的時間來啟動。我想在應用程式繼續載入時顯示一個閃屏(就像Visual Studio .net和Office應用程式那樣)。工具箱中沒有這樣的控制項。我該如何?呢?
A:
本專欄所附帶的代碼中包含了一個 SplashScreen類:
public class SplashScreen{ public SplashScreen(Bitmap splash); public void Close();} |
SplashScreen的構造器可以將顯示的位元影像作為參數。Close方法用來關閉閃屏。通常情況下,我們在處理表單(form)的Load事件的方法中運用SplashScreen(在圖1中可以看到形成的閃屏):
private void OnLoad(object sender,EventArgs e){ Bitmap splashImage; splashImage = new Bitmap("Splash.bmp"); SplashScreen splashScreen; splashScreen = new SplashScreen(splashImage); //Do some lengthy operations, then: splashScreen.Close(); Activate();} |
在關閉閃屏後,你必須啟用表單,將它放到最顯著的位置。
你可以將任何位元影像作為一個閃屏。你也可以通過構建一個新的位元影像對象從BMP或JPG檔案建立位元影像:
Bitmap splashImage;splashImage = new Bitmap("Splash.bmp"); |
或者你也可以用從表單資源載入的一個圖片:
using System.Resources;ResourceManager resources;resources = new ResourceManager(typeof(MyForm));Bitmap splashImage;SplashImage = (Bitmap)(resources.GetObject( "SplashImage")) |
要實現一個閃屏不只是我們所看到的這些內容。它可以依賴於一些很好的WinForms功能,而且它也涉及一些應用在其它WinForms環境中的有趣的設計問題。閃屏實際上是一個叫做SplashForm的WinForms表單。你可以通過WinForms的可視設計視窗( Visual Designer)充分利用所需要的變化,將一個預設的表單轉換成一個閃屏——這就證明了WinForms不僅簡單易用,而且還有很多功能。在這個例子中,我們添加了一個單獨的控制項——一個叫做m_SplashPictureBox的簡單的圖片框。
在編譯的時候,我們並不知道閃屏圖片的大小,因為它是一個runtime參數,但是圖片框需要根據圖片來調整大小。你可以通過將m_SplashPictureBox的SizeMode屬性設定為AutoSize很容易地實現這一點。接下來,你必須將圖片框定位到表單的左上方。你可以通過將m_SplashPictureBox的Dock屬性設定為Fill來實現它。這就會將圖片框固定在左上方了。在運行時,它會向右下角擴充來填充表單,因為大小模式被設定成了AutoSize。最後,將m_SplashPictureBox的Cursor屬性設定為AppStarting(帶有一個指標的沙漏),這樣的話,如果使用者將滑鼠移動到閃屏上,他或她就會知道應用程式正在啟動。
閃屏表單不應該顯示任何控制框按鈕(關閉、最小化和最大化),它也不會有一個標題列。我們可以通過可視設計視窗將SplashForm的ControlBox屬性設定為False;這樣就取消了控制框(control box)。可以在設計視窗中清除Text屬性來刪除標題列。
下面我們來看閃屏的邊界。它應該是一條單獨的線——不是預設的可調整的邊界樣式——所以我們應該將表單的FormBorderStyle屬性設定為FixedSingle。將TopMost屬性設定為True,使閃屏總是在z-order(Windows在案頭顯示視窗的順序)的頂部。閃屏應該總是在螢幕的中心。幸運的是,我們可以將StartPosition屬性設定為CenterScreen來實現這一點,WinForms會自動考慮視窗的大小,並將它置中。圖2顯示了SplashForm和m_SplashPictureBox的Properties視窗,總結了你需要設定的屬性和新的值。
接下來,我們需要寫一些代碼來調整閃屏的大小。SplashForm的構造器可以將閃動的圖片作為參數,並將它賦值給圖片框的圖片:
internal class SplashForm : Form{ PictureBox m_SplashPictureBox; public SplashForm(Bitmap splashImage) { InitializeComponent(); m_SplashPictureBox.Image = splashImage; ClientSize = m_SplashPictureBox.Size; } //Rest of the implementation } |
注意,你必須將SplashForm的用戶端大小設定為圖片框的大小,它會根據圖片的大小自動調節自己的大小。結果SplashForm就可以在圖片框中精確地顯示圖片了,因為圖片框是被放在表單的左上方的。
你不能在用來載入應用程式的同一個線程上顯示SplashForm,因為那個線程在忙於載入應用程式而不會考慮顯示或重繪閃屏。作為替代,我們應該讓SplashScreen建立一個背景工作執行緒(worker thread)來顯示SplashForm(見列表1)。背景工作執行緒調用Show方法,該方法會建立SplashForm對象並調用它的ShowDialog方法:
void Show(){ m_SplashForm = new SplashForm(m_SplashImage); m_SplashForm.ShowDialog();} |
ShowDialog顯示表單並開始將Windows訊息填充到裡面。閃屏是在它自己的線程上啟動並執行,因此該線程可以進行訊息處理——不是指忙於載入應用程式的那個主應用程式線程。
接下來的任務是為主應用程式找到一個方法來關閉閃屏。最容易的方法就是用訊號通知背景工作執行緒關閉表單——除非該線程的方法(Show)正忙於在表單的訊息迴圈中(ShowDialog方法)填充訊息,而不能查看標記或事件。解決的方法很簡單,就是用Windows Timers。運用設計視窗在表單上添加一個Timer控制項,將它的Interval屬性設定為適當的值,如500毫秒。Timer類實際上是基於VM_TIMER訊息的,所以timer的Tick事件是Windows訊息驅動的。背景工作執行緒將那個訊息提供給閃屏,在那裡它會查看是否需要關閉閃屏,因為主應用程式已經完成了載入。SplashForm類提供了Boolean屬性HideSplash,SplashScreen的Close方法將它設定為:
public void Close(){ m_SplashForm.HideSplash = true; m_WorkerThread.Join();} |
HideSplash可以訪問SplashForm的m_HideSplash Boolean成員變數。m_HideSplash可以由多個線程訪問,所以HideSplash需要通過鎖定SplashForm以一種安全執行緒的方法來訪問m_HideSplash:
public bool HideSplash{ get { lock(this){ return m_HideSplash; } } set { lock(this){ m_HideSplash = value; } }} |
SplashForm在OnTick方法中處理timer的Tick事件:
private void OnTick(object sender,EventArgs e){ if(HideSplash == true) { m_Timer.Enabled = false; Close(); }} |
如果HideSplash屬性設定為true(因為調用了SplashScreen的Close方法),OnTick就會使timer無效並關閉SplashForm。它的運作過程是這樣的:主表單開始載入,並在另外的一個線程上顯示閃屏。然後,主表單繼續啟動應用程式。閃屏定期查看(運用timer)是否應該關閉。當主表單完成載入時會調用SplashScreen的Close方法。Close方法將HideSplash設定為true,並在背景工作執行緒上調用Join,等閃屏關閉。這會阻礙主表單的顯示,所以只要顯示閃屏,主表單就不會顯示。下一次timer響了時,它就會查看HideSplash的值。它會取消timer並關閉SplashForm,因為HideSplash被設定為true。這會返回ShowDialog方法(該方法在SplashScreen的Show方法中被調用),然後返回Show。一旦返回Show,線程就終止了,因為Show是背景工作執行緒的線程方法。這時候,會返回SplashScreen的Close方法中的Join。Close方法被返回到主表單,現在就可以顯示主表單了。
Q:允許可序列化的(Serializable)類型包含不可序列化的(Nonserializable)成員
我有一個可序列化的類,它包含一個資料庫連接,作為一個成員變數。當我試著去序列化這個類時,出現了一個異常,因為串連是不可序列化的。如果我將串連標識為不可序列化,那麼我就可以序列化類別了——但在還原序列化(deserialization)後,我就不能用這個對象了,因為串連成員是無效的。我該怎麼處理呢?
A:
當你用Serializable屬性來標識一個類進行序列化時,.NET認為所有的成員變數也都是可序列化的,如果它發現一個不可序列化的成員,它在序列化時就會拋出一個SerializationException類型的異常。然而,類可能會包含一個不能被序列化的成員。該類型沒有Serializable屬性,不能讓所包含的類型被序列化。通常情況下,這個不可序列化成員是一個參考型別,需要一些特殊的初始化設定。要解決這個問題,我們需要將這樣的一個成員標識為不可序列化,並在還原序列化中採用一個自訂的步驟來初始化它。
你必須用NonSerialized欄位屬性來標識成員,讓一個可序列化的類型包含一個不可序列化的類型,作為一個成員變數:
public class MyOtherClass{..}[Serializable]public class MyClass{ [NonSerialized] MyOtherClass m_Obj; /* Methods and properties */} |
當.NET序列化一個成員變數時,它會首先查看它是否有NonSerialized屬性:如果有,.NET就會忽略該變數,跳過它。然而,當.NET還原序列化對象時,它就會初始化那個類型的不可序列化的成員變數,將它設定為預設值(對所有參考型別來說,預設值為零)。然後,就由你來提供代碼將變數初始化到正確的值。最後,對象必須知道它是在什麼時候被還原序列化的。你必須實現IDeserializationCallback介面,該介面是在System.Runtime.Serialization命名空間中定義的:
public interface IDeserializationCallback{ void OnDeserialization(object sender);} |
在.NET完成對對象的還原序列化處理後,就會調用IDeserializationCallback的OnDeserialization()方法,讓它執行所需要的自訂的初始化步驟。你可以忽略發送的參數,因為.NET總是將它設定為零。下面的代碼說明了如何通過實現IDeserializationCallback來執行自訂的序列化:
using System.Runtime.Serialization;[Serializable]public class MyClass : IDeserializationCallback{ [NonSerialized] IDbConnection m_Connection; public void OnDeserialization(object sender) { Debug.Assert(m_Connection == null); m_Connection = new SqlConnection(); m_Connection.ConnectionString = "data source= ... "; m_Connection.Open(); } /* Other members */} |
在上面的代碼中,MyClass類有一個作為成員變數的資料庫連接。連線物件(SqlConnection)不是一個可序列化的類型,所以你需要用NonSerialized屬性來標識它。MyClass在它的OnDeseralization()實現中建立了一個新的連線物件,因為串連成員在還原序列化後被設定為預設值(零)。然後,通過提供一個連接字串,MyClass初始化了一個連線物件並開啟它。
關於作者:
Juval Lowy是位經驗豐富的軟體架構師,並且是IDesign的負責人。這是一家專門從事.NET設計和.NET移植的諮詢和培訓公司。作為Microsoft在矽谷的地區主管,Juval負責協助將.NET運用到企業中。最近,他寫了一本名為Programming .NET Components (O'Reilly & Associates)的書。你可以通過www.idesign.net與他聯絡。