.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~。感觉本文对自己有用的朋友,给个“推荐”或者建议啥的都是可以滴,非常感谢~

 

posted @ 2013-03-27 10:29  周见智  阅读(8117)  评论(20编辑  收藏  举报