閱讀本篇部落格之前需要了解VS窗體設計器的工作原理,詳細可參見本系列部落格(十)、(十一)、(十二)。必須需要知道的一條結論就是:處於窗體設計器(Form Designer)中的任何元件(包含控制元件,下同),都是實際存在的一個例項。也就是說,拖進去的button1,其實就是例項化一個Button控制元件。
通常編碼中,我們在使用一個型別物件時,通過以下方式:
1 Car c = new Car(); //例項化物件 2 c.Type = “標緻308”; //設定屬性 3 c.Color = Color.Black; //設定屬性 4 c. InspectionInformation = new Inspection(“2013-11-12”,”張三”,”RS678T”);//設定屬性 5 c.SomethingHappened+=new SomethingHappenedEventHandler(c_SomethingHappened);//註冊事件 6 //以上為物件的初始化 以下開始使用c物件 7 //…
如上所示,我們在使用Car類時,是通過new的方式來建立一個例項,然後給它初始化一些資訊,這些所有操作都是通過我們手動來編寫程式碼實現的。
我們注意到,在設計UI介面的時候,窗體中的所有控制元件、元件都是可以通過“屬性窗體”來編輯的,也就是說,介面上這些元素的初始化不需要我們手動編寫程式碼,完全可以通過點點滑鼠,按按鍵盤就可以做到。我們可以總結出來設計器可以幫我們做以下工作:
- 例項化物件
沒錯,不用你手動new物件了,設計器幫你來完成。
- 編輯屬性
選定一個元件,在屬性窗體中編輯它的屬性,跟你通過編寫“例項.屬性=屬性值”是一樣的效果。
- 註冊事件
選定一個元件,在屬性窗體的事件選項卡,雙擊事件空白處,自動註冊事件。
窗體設計器不需要你手動編寫一行程式碼,物件的例項化、屬性編輯、事件註冊全部搞定,也就是說,窗體設計器能夠視覺化設計一些物件。至於哪些型別的物件可以通過窗體設計器來進行視覺化設計,請參見本系列(十、十一、十二),我在這裡直接給出結果:窗體設計器能夠視覺化設計實現了IComponent介面型別的物件。也就是說,如果你定義了一個型別A,恰好它實現了IComponent介面(直接或者間接),那麼你就完全可以通過窗體設計器來視覺化設計A型別的物件。
由此可以看出,建立一個可以視覺化設計的物件並不難,只要我們的型別實現了IComponent介面就行(官方稱這種型別為元件)。我們再來看一下,窗體設計器初始化出來的物件,跟我們自己手動編寫程式碼初始化的物件有哪些相同點和不同點:
不同點:
- 前者更直觀簡單,隱藏的東西太多,後者複雜,但是清楚內部過程。
- 前者物件的初始化,在程式一啟動就開始,不能人工控制其時機,具體是在Form1的構造方法中的InitializeComponent()中進行,後者就更靈活,需要的時候編寫程式碼就可以。
- 前者初始化出來的物件幾乎都跟UI介面有關(這個很容易就能想到,窗體設計器肯定設計跟窗體介面有關的東西),而後者沒有這個原則,不管是什麼物件,都是可以的。
相同點:
- 都是初始化一個物件。
- 都有程式碼產生,前者產生的程式碼在InitializeComponent()中,後者為人工編寫。
我們應該清楚,程式最終都是要經過將原始碼編譯成可執行檔案之後才能執行的,所以原始碼是一切根本,沒有原始碼,其他的都是白扯。
綜上所有之述,我們可以手動編寫程式碼來初始化任何物件,我們可以通過窗體設計器來初始化實現了IComponent介面的型別物件。
好了知道怎樣才能建立一個可以視覺化設計的物件之後,我們來建立一個試一下,定義一個型別MyComponent,使其繼承自Component:
1 /// <summary> 2 /// 可被 視覺化設計的類,該類預設只包含屬性 3 /// </summary> 4 public partial class MyComponent : Component 5 { 6 public MyComponent() 7 { 8 InitializeComponent(); 9 } 10 11 public MyComponent(IContainer container) 12 { 13 container.Add(this); 14 InitializeComponent(); 15 } 16 /// <summary> 17 /// 字串屬性 使用預設屬性編輯器 18 /// </summary> 19 public string StringProperty 20 { 21 set; 22 get; 23 } 24 /// <summary> 25 /// 顏色屬性 使用預設屬性編輯器 26 /// </summary> 27 public Color ColorProperty 28 { 29 set; 30 get; 31 } 32 /// <summary> 33 /// 自定義型別屬性 使用下拉選單編輯器 34 /// </summary> 35 [Editor(typeof(MyTypeEditor1),typeof(UITypeEditor)),DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] 36 public MyType MyTypeProperty1 37 { 38 set; 39 get; 40 } 41 /// <summary> 42 /// 自定義型別屬性 使用彈出對話方塊編輯器 43 /// </summary> 44 [Editor(typeof(MyTypeEditor2),typeof(UITypeEditor)),DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] 45 public MyType MyTypeProperty2 46 { 47 set; 48 get; 49 } 50 /// <summary> 51 /// 控制元件屬性 可以在設計器中選擇已經存在的控制元件 52 /// </summary> 53 [Editor(typeof(ControlEditor),typeof(UITypeEditor))] 54 public Control ControlProperty 55 { 56 set; 57 get; 58 } 59 /// <summary> 60 /// ImageList型別屬性 61 /// </summary> 62 [Editor(typeof(ImageListEditor),typeof(UITypeEditor))] 63 public ImageList ImageListProperty 64 { 65 get; 66 set; 67 } 68 }
如上程式碼所示,該型別只包含了幾個公共屬性,沒有其他內容。此型別物件就可以通過窗體設計器來設計了,也就是說,從工具箱中向設計器中拖放MyComponent型別之後,窗體設計器自動會例項化一個MyComponent物件,並且你可以通過屬性窗體來編輯該物件的屬性:
1)StringProperty
String型別屬性,直接可以在屬性窗體中輸入。
2)ColorProperty
Color型別屬性,屬於.NET自帶型別,所以有預設的屬性編輯器,如下圖:
圖1
3)MyTypeProperty1
自定義型別屬性,需要我們自己定義一個屬性編輯器Editor(typeof(MyTypeEditor1),typeof(UITypeEditor))。
4)MyTypeProperty2
自定義型別屬性,需要我們自己定義一個屬性編輯器Editor(typeof(MyTypeEditor2),typeof(UITypeEditor))。
5)ControlProperty
Control型別屬性,我們可以將設計器中已經存在的Control賦值給該屬性,指定了屬性編輯器Editor(typeof(ControlEditor),typeof(UITypeEditor))。
6)ImageListProperty
ImageList屬性,這個就是我們常見的一些控制元件(比如TabControl)含有ImageList屬性,點選右方的小三角形,就可以列出窗體設計器中已經存在的ImageList,供你選擇。指定了屬性編輯器Editor(typeof(ImageListEditor),typeof(UITypeEditor))。
也就是說,當我們在窗體設計器中設計一個物件的時候,如果該物件包含一些特殊(非.NET預設自帶型別)型別屬性時,我們需要為該屬性提供一個“屬性編輯器”。
以下就分別為每個屬性對應的屬性編輯器了(假設諸位看官都知道了UITypeEditor的作用,不知道可以查一下):
1)MyTypeProperty1屬性
1 class MyTypeEditor1 : UITypeEditor 2 { 3 public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) 4 { 5 return UITypeEditorEditStyle.DropDown; //下拉選單 在下拉選單中輸入MyType屬性值 6 } 7 public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) 8 { 9 usrMyTypeEditor usrme = new usrMyTypeEditor(); //下拉選單 10 usrme.EditedValue = value as MyType; //初始化下拉選單框 value為舊值 11 12 IWindowsFormsEditorService ie = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; 13 if (ie != null) 14 { 15 ie.DropDownControl(usrme); //顯示下拉選單 16 return usrme.EditedValue; //返回編輯後的值 17 } 18 else 19 { 20 return value; 21 } 22 } 23 }
滑鼠點選MyTypeProperty1屬性右側的小三角形,出現一個下拉選單框。如下圖:
圖2
2)MyTypeProperty2屬性
1 class MyTypeEditor2 : UITypeEditor 2 { 3 public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) 4 { 5 return UITypeEditorEditStyle.Modal; //模式對話方塊 在彈出對話方塊中輸入MyType屬性值 6 } 7 public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) 8 { 9 using (frmMyTypeEditorForm frmmef = new frmMyTypeEditorForm()) 10 { 11 frmmef.EditedValue = value as MyType; //初始化對話方塊 12 IWindowsFormsEditorService ie = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; 13 if (ie != null) 14 { 15 if (ie.ShowDialog(frmmef) == DialogResult.OK) //顯示模式對話方塊 16 { 17 return frmmef.EditedValue; //返回編輯後的值 18 } 19 else 20 { 21 return value; //返回舊值 22 } 23 24 } 25 else 26 { 27 return value; 28 } 29 } 30 } 31 }
滑鼠點選MyTypeProperty2右側的小三角形,彈出一個對話方塊。
圖3
3)ControlProperty屬性
1 class ControlEditor : UITypeEditor 2 { 3 IWindowsFormsEditorService ie = null; 4 5 public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) 6 { 7 return UITypeEditorEditStyle.DropDown; //下拉選單 在下拉選單中選擇一個(設計器中已經存在)控制元件 8 } 9 public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) 10 { 11 ListBox li = new ListBox(); //下拉選單 12 li.Click += new EventHandler(li_Click); 13 List<Control> liCo=new List<Control>(); //下拉選單每一項對應的控制元件值 14 foreach(Component c in context.Container.Components) //查詢窗體設計器中的每一個元件 15 { 16 if (c is Control && !(c is Form)) //若是控制元件 不是窗體 17 { 18 li.Items.Add((c as Control).Name); //將控制元件名稱寫入listbox 19 if (value as Control == c as Control) 20 { 21 li.SelectedIndex = li.Items.Count - 1; //選中原來值 22 } 23 liCo.Add(c as Control); //對應控制元件值寫入list 24 } 25 } 26 ie = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; 27 if (ie != null) 28 { 29 ie.DropDownControl(li); 30 if (li.SelectedIndex > -1) 31 return liCo[li.SelectedIndex]; 32 else 33 return value; 34 } 35 else 36 { 37 return value; 38 } 39 } 40 41 void li_Click(object sender, EventArgs e) 42 { 43 if (ie != null) 44 { 45 ie.CloseDropDown(); 46 } 47 } 48 }
滑鼠點選ControlProperty右側的小三角形,出現下拉選單框,列表中顯示的都是窗體設計器中已經存在的Control。
圖4
4)ImageListProperty屬性
1 class ImageListEditor : UITypeEditor 2 { 3 IWindowsFormsEditorService ie = null; 4 5 public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) 6 { 7 return UITypeEditorEditStyle.DropDown; //下拉選單 在下拉選單中選擇一個(設計器中已經存在)ImageList元件 8 } 9 public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) 10 { 11 ListBox li = new ListBox(); //下拉選單 12 li.Items.Add("無"); 13 li.Click += new EventHandler(li_Click); 14 List<ImageList> liCo = new List<ImageList>(); //下拉選單每一項對應的ImageList值 15 foreach (Component c in context.Container.Components) //查詢窗體設計器中的每一個元件 16 { 17 if (c is ImageList) //若是ImageList 18 { 19 li.Items.Add((c as ImageList).ToString().Split(' ')[0]); //將ImageList名稱寫入listbox 20 if (value as ImageList == c as ImageList) 21 { 22 li.SelectedIndex = li.Items.Count - 1; //選中原來值 23 } 24 liCo.Add(c as ImageList); //對應ImageList寫入list 25 } 26 } 27 ie = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; 28 if (ie != null) 29 { 30 ie.DropDownControl(li); 31 if (li.SelectedIndex == 0) //無 32 return null; 33 else if (li.SelectedIndex > 0) 34 return liCo[li.SelectedIndex-1]; 35 else 36 return value; 37 } 38 else 39 { 40 return value; 41 } 42 } 43 44 void li_Click(object sender, EventArgs e) 45 { 46 if (ie != null) 47 { 48 ie.CloseDropDown(); 49 } 50 } 51 }
滑鼠點選ImageListProperty右側的小三角形,出現下拉選單,列表中顯示窗體設計器中已經存在的ImageList。
圖5
本篇部落格介紹了怎麼建立一個可以視覺化設計的物件(準確來講,應該是怎樣建立一個可以視覺化設計其物件的型別),首先讓型別(間接或者直接)實現IComponent介面(不要問我為什麼,請參見前面的部落格),然後為型別的某些特殊屬性建立對應的屬性編輯器,就這麼簡單。型別建立者工作量大一點,但是型別使用者更方便一些,Demo中MyComponent型別就是最後的核心成果,供使用者使用,使用者不需要知道其他的類似屬性編輯器之類的東西,在他們看來,這些相當於沒有。
注意使用範圍,並不是任何時候都可以使用,最好參見前面提到的“相同點”和“不同點”那裡。另外,本篇部落格中設計到的知識點很多,像provider.GetService(typeof(IWindowsFormsEditorService))、foreach(Component c in context.Container.Components)這些需要和設計器打交道的地方我幾乎沒有詳細說到過,原因是這些太複雜了,要是細說的話,得說一大簍子,而且不是本篇文章的重點。
原始碼下載地址:http://files.cnblogs.com/xiaozhi_5638/PropertyEditorInDesigner.rar
注意不要試圖執行原始碼,沒有任何效果,只能在設計器中看到效果。希望有幫助。
Update (2013-12-09)
上面結束提到了“支援視覺化設計物件”的使用場合,建議如果該型別跟UI介面有關聯(需要與介面其他元素互動),並且物件的例項化不需要人工控制其時機,那麼可以讓該型別支援視覺化設計(也就是實現IComponent介面)。注意這裡的建議,也就是說不是強制性的,你完全可以定義一個People類,讓其實現IComponent介面,這合法!結果就是,從工具欄中拖放一個People到窗體設計器中,它會自動幫你例項化了一個People物件例項,並且你也能通過屬性窗體編輯它的屬性值(這些後臺都能自動生成對應程式碼)。另外,經發現,具備某一些功能的型別,雖然跟介面無互動,但是它還是支援視覺化設計,比如BackgroundWorker元件,拖一個BackgroundWorker到窗體設計器中去,設計器會自動幫你例項化一個Backgroundworker物件(生成對應程式碼),然後你可以通過屬性窗體編輯它的屬性值(生成對應程式碼),這跟你自己手動new BackgroundWorker沒有區別,你還是可以在其他地方一樣使用該backgroundworke物件,至少你目前看起來沒區別。
為了更好的說明通過窗體設計器設計出來的物件,跟我們手動編寫程式碼搞出來的物件有什麼相同和不同點,我們先來分析一下Form1.Designer.cs中的程式碼:
我們注意到,我們在設計器中的每一步操作,對應生成的程式碼都在InitializeComponent()方法中,它像是word錄製巨集的功能,你在word中的每一個操作,都可以生成對應的VBA程式碼,也就是說,窗體設計器從例項化物件,到編輯屬性,再到註冊事件等等等,都有程式碼幫我們記錄下這些操作。我們再看一下例項化物件的程式碼:
1 this.myComponent1 = new PropertyEditorInDesigner.MyComponent(this.components);
沒錯,任何一個例項化出來的物件,都給它傳遞了this.components容器,我們再看MyComponent的構造方法是這樣的:
1 public MyComponent(IContainer container) 2 { 3 container.Add(this); 4 InitializeComponent(); 5 }
也就是說,物件例項化的時候,都將該物件放進了一個components的容器,這個容器專門用來存放由窗體設計器例項化出來的物件(控制元件除外)。這就像一個大的容器,專門來存放這些小個體。接下來我們再來看一下Form1.Designer.cs中的dispose方法:
1 protected override void Dispose(bool disposing) 2 { 3 if (disposing && (components != null)) 4 { 5 components.Dispose(); 6 } 7 base.Dispose(disposing); 8 }
我們可以發現,在Form1物件Dispose的時候,它將components中的所有個體都Dispose掉了。到此,我們可以總結出來一條:由窗體設計器設計出來的物件由父窗體(父控制元件)的InitializeComponents方法統一初始化,由父窗體(父控制元件)的Dispose()方法統一釋放資源。我們來看一張Form1執行結構圖:
圖(更新)1
由此可以看出,由窗體設計器設計出來的物件,它們的生命週期以及存放結構都比較有規律,這個就是窗體設計器設計出來物件的好處。
我們在往窗體設計器中拖放元件時,就是往Form1型別中新增新的成員物件,跟我們定義一個型別,向裡面新增成員變數一個意思,只是前者更直觀,新增的每一個成員物件,在UI設計器中都能看見與它對應的一個物件例項,只要我們改變了這個物件例項的屬性,就能馬上看見設計器中的物件例項效果,緊接著後臺生成程式碼;而後者就沒有這麼直觀了,只能手寫程式碼,而且還看不見效果。
圖(更新)2
如上圖,窗體設計器中某一個物件例項屬性更新之後,馬上就能在設計器中看見效果(因為它是實實在在存在於堆中的物件),接著InitializeComponents中的程式碼就會更新,注意,窗體設計器中的myComponent1物件例項跟.cs程式碼中的myComponent1變數不是一個東西,他們只有一種對映關係。我們最終要的是.cs中的程式碼檔案,而不是我們看見的窗體設計器中的影象,後者只是起到一個視覺化的效果,最終一文不值。這個就像photoshop作圖一樣,最終儲存到硬碟的圖片檔案才是最重要的,作圖過程中作圖區域顯示的東西沒有價值。窗體設計器隱藏得越多,我們知道得越少。