WinForm二三事(四)界面布局(上)
2010-01-20 09:29 横刀天笑 阅读(59334) 评论(42) 编辑 收藏 举报记得刚从Web转向WinForm开发时有一段狂喜,没有Session丢失、没有浏览器不兼容,不用围着HTML、CSS、Javascript、C#打转,在Visual Studio里控件摆成什么样子最后就是什么样子(虽然根据最终用户的操作系统和分辨率设置有稍微的不同),这种感觉好久没有过了,但随着开发的界面越来越复杂却有些无措。
接下来我会分两篇来介绍WinForm界面布局中需要注意的一些事情,上篇介绍的是一些简单但也常见的,而下篇会接触更为复杂但灵活的内容。
Dock & Anchor
Dock和Anchor是水火不容的,同时给控件设置Dock和Anchor属性时后设置的会覆盖前面的设置。
Dock
我初接触WinForm的时候发现控件的Width和Height只能是int,不能设置为百分比,那个时候就想子控件如何跟着父控件而变化呢?父控件变大就变大,而且还填满那个区域。甚至以为这个需要通过代码来解决,如是在SizeChanged事件里……
后来才发现我要的就是Dock给的,Dock是停靠的意思。Dock属性的类型是DockStyle枚举:
1: public enum DockStyle
2: {
3: None = 0,
4: Top = 1,
5: Bottom = 2,
6: Left = 3,
7: Right = 4,
8: Fill = 5
9: }
默认是None,当为Left的时,就表示子控件停靠在父控件的左边区域,并把左区域填充满:
上图中的Panel总是会停靠在Form的左边区域,不管如何调整Form的高度,它总是能把左边区域填满。Dock麻烦的地方在于多个控件碰到一起时,比如有两个Panel都设置为Left该怎么办?我们会发现向父控件的Controls集合中添加子控件,越晚添加具有更高的“优先级”(不知道有没有这个说法,这是我杜撰的)。这里的优先级指的是,子控件“优先级”越高,越靠近父控件边缘,其他子控件就得避让:
this.Controls.Add(this.panel1);
this.Controls.Add(this.panel2);
Panel2后添加进去,所以它具有更高的优先级,更靠近Form的边缘。这个规则不仅仅对于Dock都设为Left的有效,对Dock属性不同,但是碰到一起的子控件也适用:
1: this.panel1.Dock = DockStyle.Left;
2: this.panel2.Dock = DockStyle.Left;
3: this.panel3.Dock = DockStyle.Top;
4:
5: this.Controls.Add(this.panel1);
6: this.Controls.Add(this.panel2);
7: this.Controls.Add(this.panel3);
panel3的Dock设置为Top,最后添加到Controls集合中,所以具有最高的优先级,其他两个panel都得避让:
当然,这里的“优先级”比较只在同一个“层次”起作用,将子控件与父控件同一层次的控件相比较是没有意义的。
Anchor
Dock一般是划分区域的,将一个窗体或大控件划分几个大区域以便布局,就像Web中的div一样。但有的时候我们却想子控件在父控件里的相对位置不要随着父控件的变大缩小而变化,或者控件边缘距离父控件边缘的相对距离不要发生变化,但又不是停靠在父控件的边缘,这就是Anchor该出场的时候了。
Anchor属性的类型是AnchorStyles位标记(如果不理解位标记就将其理解为可以使用位运算符进行操作,从而可以设置多个值的枚举吧,深入理解请参见MSDN或《CLR via C#》中对位标记的描述):
1: [Flags]
2: public enum AnchorStyles
3: {
4: None = 0,
5: Top = 1,
6: Bottom = 2,
7: Left = 3,
8: Right = 4
9: }
Anchor的默认值是Anchor.Left | Anchor.Top,也就是子控件与父控件的左边缘和上边缘的相对位置不会变化,这也保证了在窗体最大化后子控件的位置不会发生变化:
窗体默认显示时->
窗体变大后->
还是悬停在左上角不会发生变化。那如果设置为Left和Right呢?当Anchor设置为Left|Right的时候,为了确保父控件(在这里就是Form)变大时,控件的边缘与父控件距离不变,子控件会自动的扩大:
总之,记着Anchor的中文意思:锚。当给控件设置Anchor的时候,就相当于用一个铁钉将控件的边缘给钉住。
Padding & Margin
Padding和Margin没有什么好说的,和CSS的盒模型描述的一模一样,Padding指的是控件内部空间,Margin指的是控件之外的:
padding和margin都可以指定四个值。
AutoSize
有的时候我们需要控件随着里面的内容的增长而增长,比如在做多语言的程序时,各国的语言描述同一个意思的时候长度会不同,这个时候就需要AutoSize为true了,这样当文字过长不会被截断。关于AutoSize更详细的内容请参见MSDN。
如何面对复杂界面?出了问题咋办?
设计时
当界面变得越来越复杂的时候,我们很期望了解控件之间的层次关系,这个按钮是放在哪个Panel上?这个Panel的区域又是咋样的。我刚接触WinForm的时候,我非常期望WinForm上也有类似于IE Developer Toolbar的工具,点击HTML,可以可视化的在界面上显示区域,在界面上选中某区域,也可以定位到HTML元素。实际上在设计WinForm界面时也是可以的。这就是Visual Studio的Document Outline窗口(View->Other Windows->Document Outline):
不过如果你只想顺着button1->panel4->panel3->……这条线导航控件树,有更方便的方法:ESC键。选中一个控件,然后敲ESC键就会顺着这个控件层次不断的上溯。
选中一个控件,然后点击右键,还会出现 Select …的菜单,可以选中该控件的某个父控件:
真是够方便的~~~
运行时
不过有个问题是,上面的方法都是设计时的,有的时候我们的程序中如果动态的修改了某些涉及布局的属性最后发现界面乱套了,这可咋整。运行时的问题当然要运行时解决,给某个父控件附加Layout事件,当修改了涉及Layout的属性时会触发这个事件(也有特例,下一节介绍)。这个事件会有一个LayoutEventArgs参数,该参数有AffectedProperty属性,该属性指示的就是影响布局的罪魁祸首,你就找到病症所在了。
SuspendLayout & ResumeLayout
我想大家对这两个方法肯定不默认,几乎在WinForm里的InitializeComponent方法里,在方法开始处有会调用SuspendLayout方法,然后在方法快结束处会调用ResumeLayout方法。有些的读者也许还尝试过删除这两个方法,发现程序表现行为和以前也一样。
了解这两个方法对WinForm程序的性能还是挺有帮助的,在上一节提到修改涉及Layout的属性时会触发Layout事件,但是有特例,特例就是调用了SuspendLayout方法,关于修改哪些属性会触发Layout事件请查阅MSDN。在代码中如果修改Size、Dock等属性或向父控件添加子控件时,会执行布局逻辑,有的时候甚至会重绘。当我们要修改一堆的这样的属性时,比如前面提到的InitializeComponent方法,我们当然不想修改一下就执行一次布局逻辑,那太慢了。这个时候在修改之前你就可以调用SuspendLayout方法挂起布局逻辑,等所有属性都设置好后再调用ResumeLayout属性,特别是在界面很复杂的时候性能有很大的提升。
Visual Studio默认将设置这些属性的语句全部放在InitializeComponent方法里,然后用SuspendLayout和ResumeLayout括住,所以我们一般不要自作主张的将这些属性移出到外面设置,不过有的时候我们想在代码里动态生成一些界面,比如添加一些子控件什么的,我们最好也像VS干的那样调用这两个方法。
要注意的是,并不是调用了Form的SuspendLayout和ResumeLayout方法就一了百了了。如果你是向一个Panel添加子控件,你还得调用Panel的这两个方法。
总结
本文介绍了WinForm界面布局的初步知识,还介绍了通过临时挂起布局逻辑来优化程序性能。这些都很基础也很简单,应付简单的布局是够了,不过对于更灵活更复杂的布局就要涉及布局引擎的内容了,这个我会在下一节里介绍。
本系列其他文章
WinForm二三事(三)Control.Invoke&Control.BeginInvoke
WinForm二三事(四)界面布局(上)
WinForm二三事(四)界面布局(下)
WinForm二三事(五)实作
WinForm二三事(六)数据绑定
WinForm二三事(七)GDI+
WinForm二三事(八)开源项目
WinForm二三事(九)常用第三方控件库
WinForm二三事(十)漫谈