.Net開發筆記(九)自定義窗體設計器


其實本文標題說得有點大,一個窗體設計器包含的功能實在是太多而且非常復雜,網上有很多地方也講到這方面的內容,不過基本上都是E文,http://www.codeproject.com/Articles/24385/Have-a-Great-DesignTime-Experience-with-a-Powerful該處作者實現了一個簡單的Form Designer,而且附有源碼,不過也只是點到為止,功能不多,還有一個老外的開源項目http://www.icsharpcode.net/OpenSource/SD/,基本上涵蓋了界面設計、生成代碼、編譯、調試等功能,能和VS IDE有一拼,源碼壓縮包有幾十M,復雜度可想而知,我大概看了一下這兩個的源碼結構,基本上都是利用微軟FCL中提供的接口、類來實現的,它們主要存在System.ComponentModel,Design命名空間中,比如DesignSurface、IDesignerHost、ISelectionService等,諸位也知道,微軟的東西用起來很方便,理解起來嘛,那是相當的困難,因為你根本不知道為啥要那么搞,所以本篇文章我打算試着從底層來說明“窗體可視化設計”到底是個什么東西,諸位看官如果之前了解也不妨看看,不了解的話也沒事,基本上涉及到的都是平時用到的技術,不涉及到System.ComponentModel,Design中的任何東西。文章最后附帶Demo源碼下載地址。

我不知道窗體設計器對諸位來說是個什么樣的概念,從開始學習可視化編程時,我們就開始接觸了它,就我來說,用過VC6.0、VB以及現在的VS,基本上都是一樣,在界面上拖拖控件,寫寫事件處理程序,編譯后就能運行一個簡單功能的Windows桌面應用程序了,但是,有以下疑問(Winform為例):

  1. 在新建一個Windows窗體時,設計器中默認出現的窗體是個什么東西?是真實實例化的一個Form(或其派生類)對象,還是其他替代品?
  2. 從工具箱中向窗體上拖一個Button控件,是一個什么樣的過程?
  3. 類似1,拖進的Button控件是個什么東西?是真是實例化的一個Button類對象,還是其他替代品?
  4. 在設計器中選中一個控件(或者窗體),周圍出現的方框是什么東西?鼠標又是怎么改變控件的位置和大小?
  5. 怎樣選中多個控件,讓其按照某一規則自動對齊?怎樣實現設計器中的“剪切”、“粘貼”等菜單?

暫時先列這么幾個問題,其實還有很多,只是其他的本文沒有給出解釋,比如雙擊Button按鈕時,怎么自動生成事件代碼?最重要的是,怎樣根據設計的界面生成代碼?這些問題其實利用.net中提供的類都能實現,只是不太容易去探究它底層的具體實現細節。下面我就來原汁原味地解釋一下以上提出的疑問,Demo中我用自己的方法從底層開始實現了跟常用設計器效果基本一樣的功能,當然我不保證.net類庫中的類底層就是像我解釋的這樣去實現的,因為我也沒看該部分類的源碼,呵呵。

以下就是解釋:

  1. 設計器中的默認窗體是真實實例化的一個Form(或其派生類)對象,也就是說,它跟桌面左下角“開始”按鈕一樣,是一個真實存在的控件(窗體即控件,控件即窗體,詳見我前面的博客),至於為什么,這個很好解釋,因為如果不是真實存在的控件,是不可能通過屬性窗口(一般為PropertyGrid控件)來動態顯示和修改它的屬性。另外,諸位可以用Spy++查看設計器中的窗體和控件,你會發現能看到他們的句柄(Handle)。
  2. 當用戶(Programmer)從工具箱向窗體中拖一個Button控件時,對於設計器來說,做的事情很簡單:實例化一個控件,將其加到設計器中的默認窗體中,設置屬性窗口中的PropertyGrid.SelectedObject = 實例化的控件。
  3. 跟1相同。其實這時候出現了一個問題,既然1中3中說到,設計器中的窗體和控件都是像桌面左下角“開始”按鈕一樣真實存在的,那么它應該能接受用戶輸入吧?!比如鼠標點擊、鍵盤輸入等,可是如我們所見,所有的設計器中的窗體和控件幾乎都不能按照正常方式去響應用戶輸入,這是為什么?其實,我們的眼睛騙了我們,因為設計器中不只是窗體和控件,在它們上面還有一個透明的遮罩層,看一幅圖:

圖1

如你們所見,整個設計器可以分為三層,底層的灰色部分可以看作整個設計器的容器,中間層的藍色部分就是設計器中的默認窗體,所有拖進設計器中的控件都是放在藍色部分中,在藍色部分上面,有一層透明的遮罩層(用橘黃色方框表示),大小跟底層的灰色部分一致,在用戶(Programer)看來,最上面的遮罩層是“不存在”的,但是事實上,所有的鼠標鍵盤操作都被遮罩層攔截,由它進行處理,因此,中間層的窗體和控件不會接受任何用戶輸入。注:中間層的藍色部分和最上面的橘黃色部分都屬於底層灰色部分,如果把它們都看做控件,那么藍色部分和橘黃色是平行關系,都屬於灰色部分的子控件,只是Z軸順序不同(Demo中的源碼有所體現)。

     4.既然最上面有一個透明的遮罩層,那么當選中一個控件時,周圍出現的選中方框可以把它畫在這個遮罩層上,任何時候遮罩層中的方框跟中間層的選中控件相對應,保持一致,也就是說,無論方框大小還是位置,都跟中間層的選中控件一樣,讓人看起來,它兩重合  ( 雖然不在同一個層面)。鼠標操作的是頂層的方框,中間層的控件跟着移動、變大、變小。

     5.在設計器中,如果選中多個控件,就會出現多個選中方框,道理一樣,所有的選中方框都在最頂層,每一個方框都與中間層的某一控件相對應,操作跟4中一樣。復制時,可以將選中控件的完整名稱、屬性、以及屬性值收集起來存放在ClipBoard中,粘貼時,根據 完整名稱新建控件,將屬性值賦予新建控件,將其加到中間層。

以上就是從底層開始解釋一個窗體設計器的簡單原理,在圖1中,底層灰色部分DesignerControl是一個UserControl,HostFrame是一個控件容器(Form),Overlayer也是一個透明的UserControl,Recter為虛線效果的選中框,這些名稱分別與Demo中的源碼對應,諸位可以對照參考。

另外,我說幾個關鍵點:

  • 透明控件的實現
View Code
 1         protected override CreateParams CreateParams
 2         {
 3             get
 4             {
 5                 CreateParams para = base.CreateParams;
 6                 para.ExStyle |= 0x00000020; //WS_EX_TRANSPARENT 透明支持
 7                 return para;
 8             }
 9         }
10         protected override void OnPaintBackground(PaintEventArgs e) //不畫背景
11         {
12             //base.OnPaintBackground(e);
13         }
  • 當移動或者改變頂層的方框時,不要讓中間層的控件刷新的方法,就是過濾掉中間層控件的WM_PAINT消息,用到了System.Windows.Forms.IMessageFilter接口,該接口中有一PreFilterMessage方法,專門用來過濾需要忽略的Windows消息,具體實現如下:
View Code
 1 class MessageFilter : IMessageFilter
 2     {
 3         HostFrame _thehost; //中間層控件容器
 4         DesignerControl _theDesignerBoard; //設計面板
 5         public MessageFilter(HostFrame hostFrame, DesignerControl designer)
 6         {
 7             _thehost = hostFrame;
 8             _theDesignerBoard = designer;
 9         }
10         #region IMessageFilter 成員
11         public bool PreFilterMessage(ref Message m) //過濾所有控件的WM_PAINT消息
12         {
13             Control ctrl = (Control)Control.FromHandle(m.HWnd);
14             if (_thehost != null && _theDesignerBoard != null && _thehost.Controls.Contains(ctrl) && m.Msg == 0x000F) // 0x000F == WM_PAINT
15             {
16                 _theDesignerBoard.Refresh();
17                 return true;
18             }
19             return false;
20         }
21         #endregion
22     }

使用時,用Application.AddMessageFilter()方法就可以,MSDN上對該方法有一個解釋,大概意義是說此操作只對當前UI線程有效,也就是說如果一個應用程序有兩個或者多個UI線程,只能過濾當前UI線程中的某一消息。當然這個不是重點,我只是簡單的提一下,有關Windows消息以及WInform中的消息流動問題,請參考我之前的博客。

  • 選中方框就是一個普通的類,代碼如下:
View Code
  1 /// <summary>
  2     /// 選中控件時,周圍出現的方框
  3     /// </summary>
  4     class Recter
  5     {
  6         Rectangle _rect = new Rectangle();
  7         bool _isform = false;  //是否為窗體周圍的方框(如果是,則位置不可改變,且只有從下、右、右下三個方向改變大小)
  8 
  9         public Rectangle Rect
 10         {
 11             get
 12             {
 13                 return _rect;
 14             }
 15             set
 16             {
 17                 _rect = value;
 18             }
 19         }
 20         public bool IsForm
 21         {
 22             set
 23             {
 24                 _isform = value;
 25             }
 26         }
 27         public Recter()
 28         {
 29 
 30         }
 31         /// <summary>
 32         /// 繪制方框
 33         /// </summary>
 34         /// <param name="g"></param>
 35         public void Draw(Graphics g)
 36         {
 37             Rectangle rect = _rect;
 38             using (Pen p = new Pen(Brushes.Black,1))
 39             {
 40                 p.DashStyle = DashStyle.Dot;
 41                 rect.Inflate(new Size(+1, +1));
 42                 g.DrawRectangle(p, rect); //方框
 43 
 44                 p.DashStyle = DashStyle.Solid;
 45                 
 46                 //8個方塊
 47                 g.FillRectangle(Brushes.White, new Rectangle(rect.Left - 6, rect.Top - 6, 6, 6));
 48                 g.FillRectangle(Brushes.White, new Rectangle(rect.Left + rect.Width / 2 - 3, rect.Top - 6, 6, 6));
 49                 g.FillRectangle(Brushes.White, new Rectangle(rect.Left + rect.Width, rect.Top - 6, 6, 6));
 50                 g.FillRectangle(Brushes.White, new Rectangle(rect.Left - 6, rect.Top + rect.Height / 2 - 3, 6, 6));
 51                 g.FillRectangle(Brushes.White, new Rectangle(rect.Left - 6, rect.Top + rect.Height, 6, 6));
 52                 g.FillRectangle(Brushes.White, new Rectangle(rect.Left + rect.Width, rect.Top + rect.Height / 2 - 3, 6, 6));
 53                 g.FillRectangle(Brushes.White, new Rectangle(rect.Left + rect.Width / 2 - 3, rect.Top + rect.Height, 6, 6));
 54                 g.FillRectangle(Brushes.White, new Rectangle(rect.Left + rect.Width, rect.Top + rect.Height, 6, 6));
 55 
 56                 g.DrawRectangle(p, new Rectangle(rect.Left - 6, rect.Top - 6, 6, 6));
 57                 g.DrawRectangle(p, new Rectangle(rect.Left + rect.Width / 2 - 3, rect.Top - 6, 6, 6));
 58                 g.DrawRectangle(p, new Rectangle(rect.Left + rect.Width, rect.Top - 6, 6, 6));
 59                 g.DrawRectangle(p, new Rectangle(rect.Left - 6, rect.Top + rect.Height / 2 - 3, 6, 6));
 60                 g.DrawRectangle(p, new Rectangle(rect.Left - 6, rect.Top + rect.Height, 6, 6));
 61                 g.DrawRectangle(p, new Rectangle(rect.Left + rect.Width, rect.Top + rect.Height / 2 - 3, 6, 6));
 62                 g.DrawRectangle(p, new Rectangle(rect.Left + rect.Width / 2 - 3, rect.Top + rect.Height, 6, 6));
 63                 g.DrawRectangle(p, new Rectangle(rect.Left + rect.Width, rect.Top + rect.Height, 6, 6));
 64             }
 65             
 66         }
 67         /// <summary>
 68         /// 判斷鼠標操作類型
 69         /// </summary>
 70         /// <param name="p"></param>
 71         /// <returns></returns>
 72         public DragType GetMouseDragType(Point p)
 73         {
 74             Rectangle _rect = this._rect;
 75             _rect.Inflate(new Size(3, 3));
 76             if (new Rectangle(_rect.Left - 2, _rect.Top - 2, 4, 4).Contains(p) && !_isform)
 77             {
 78                 return DragType.LeftTop;
 79             }
 80             if (new Rectangle(_rect.Left + 2, _rect.Top - 2, _rect.Width - 4, 4).Contains(p) && !_isform)
 81             {
 82                 return DragType.Top;
 83             }
 84             if (new Rectangle(_rect.Left - 2, _rect.Top + 2, 4, _rect.Height - 4).Contains(p) && !_isform)
 85             {
 86                 return DragType.Left;
 87             }
 88             if (new Rectangle(_rect.Left - 2, _rect.Top + _rect.Height - 2, 4, 4).Contains(p) && !_isform)
 89             {
 90                 return DragType.LeftBottom;
 91             }
 92             if (new Rectangle(_rect.Left + 2, _rect.Top + _rect.Height - 2, _rect.Width - 4, 4).Contains(p))
 93             {
 94                 return DragType.Bottom;
 95             }
 96             if (new Rectangle(_rect.Left + _rect.Width - 2, _rect.Top + _rect.Height - 2, 4, 4).Contains(p))
 97             {
 98                 return DragType.RightBottom;
 99             }
100             if (new Rectangle(_rect.Left + _rect.Width - 2, _rect.Top + 2, 4, _rect.Height - 4).Contains(p))
101             {
102                 return DragType.Right;
103             }
104             if (new Rectangle(_rect.Left + _rect.Width - 2, _rect.Top - 2, 4, 4).Contains(p) && !_isform)
105             {
106                 return DragType.RightTop;
107             }
108             if (new Rectangle(_rect.Left + 2, _rect.Top + 2, _rect.Width - 4, _rect.Height - 4).Contains(p) && !_isform)
109             {
110                 return DragType.Center;
111             }
112             return DragType.None;
113         }
114         
115     }

其余的都很簡單,用到的都是簡單技術,詳細請參考源碼。XP .net3.5測試通過,源碼下載地址:http://download.csdn.net/detail/xiaozhi_5638/5185939。源碼中沒有剪切、復制、自動對齊等功能。

上一張Demo效果圖:

圖2

希望有幫助,O(∩_∩)O~。感覺本文對自己有用的朋友,給個“推薦”或者建議啥的都是可以滴,非常感謝~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM