.Net開發筆記(十九) 建立一個可以視覺化設計的物件

周見智發表於2013-12-08

閱讀本篇部落格之前需要了解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 //
View Code

如上所示,我們在使用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     }
View Code

如上程式碼所示,該型別只包含了幾個公共屬性,沒有其他內容。此型別物件就可以通過窗體設計器來設計了,也就是說,從工具箱中向設計器中拖放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 }
View Code

滑鼠點選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 }
View Code

滑鼠點選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 }
View Code

滑鼠點選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 }
View Code

滑鼠點選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);
View Code

沒錯,任何一個例項化出來的物件,都給它傳遞了this.components容器,我們再看MyComponent的構造方法是這樣的:

1  public MyComponent(IContainer container)
2         {
3             container.Add(this);
4             InitializeComponent();
5         }
View Code

也就是說,物件例項化的時候,都將該物件放進了一個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         }
View Code

我們可以發現,在Form1物件Dispose的時候,它將components中的所有個體都Dispose掉了。到此,我們可以總結出來一條:由窗體設計器設計出來的物件由父窗體(父控制元件)的InitializeComponents方法統一初始化,由父窗體(父控制元件)的Dispose()方法統一釋放資源。我們來看一張Form1執行結構圖

圖(更新)1

由此可以看出,由窗體設計器設計出來的物件,它們的生命週期以及存放結構都比較有規律,這個就是窗體設計器設計出來物件的好處。

我們在往窗體設計器中拖放元件時,就是往Form1型別中新增新的成員物件,跟我們定義一個型別,向裡面新增成員變數一個意思,只是前者更直觀,新增的每一個成員物件,在UI設計器中都能看見與它對應的一個物件例項,只要我們改變了這個物件例項的屬性,就能馬上看見設計器中的物件例項效果,緊接著後臺生成程式碼;而後者就沒有這麼直觀了,只能手寫程式碼,而且還看不見效果。

圖(更新)2

如上圖,窗體設計器中某一個物件例項屬性更新之後,馬上就能在設計器中看見效果(因為它是實實在在存在於堆中的物件),接著InitializeComponents中的程式碼就會更新,注意,窗體設計器中的myComponent1物件例項跟.cs程式碼中的myComponent1變數不是一個東西,他們只有一種對映關係。我們最終要的是.cs中的程式碼檔案,而不是我們看見的窗體設計器中的影象,後者只是起到一個視覺化的效果,最終一文不值。這個就像photoshop作圖一樣,最終儲存到硬碟的圖片檔案才是最重要的,作圖過程中作圖區域顯示的東西沒有價值。窗體設計器隱藏得越多,我們知道得越少。

相關文章