作者:taowen

首先要说明的我也只是一个普通的程序爱好者,并不是专业的GUI设计师,所以关于GUI的排版怎样美观并没有什么很多的研究。我只是想从GUI库提供的排版方法入手,给出一些自己的看法和想法。

GUI的平台

平台我用过的有两个,一个是Windows,一个是X-Window。他们都提供了原生的(Native)的图形和窗口方面的API。虽然WindowsGUI部分和X-Window的结构上有很大的差别,但是从界面排版这个部分来说,基本上都没有什么过多的花俏,都是以象素为中心的。

Windows是比较特殊的,它提供的不仅仅是一个平台,能够开窗口,能够进行绘画,还提供了一套控件(Control)。在这些Control的放置的API中,我们看到也都是以象素为单位来确定在主窗口中的位置的。

GUI的库

GUI这种东西中,库比平台重要。库提供了真正具体的内容。而且做到平台无关的库是在太多了。而且开发者真正需要关心和打交道的就是库,很少有人会去直接和底层平台交互的(虽然在平台发展初期的确如此,比如Windows刚出来的那个时候)。

具体的库有:Gtk+2.0, WxWindows, Swing, WindowsForms

在这个列表之外还有很多优秀的库,只是我不大熟悉他们,或者暂时不大愿意去查找这个方面的资料,所以就不列入本文讨论了。

GUI排版的主体

这个主体就是窗体上的控件,虽然不同的平台或者库会有不同的名字来描述它,比如Control, Component, Widget等,但是在用户的角度来讲,他们都有很大的相似性,比如按钮,文本框什么的。

GUI排版的目的

目的是显而易见的,就是让控件在窗口上以合适的位置出现。这种位置和样子是美工头脑中的概念,要具体描述可能就是要以什么什么窗口大小,里面的内容又是什么什么样子。这样的精确定义,我不喜欢,我想你也应该知道我是什么意思(比如窗体缩放和不同显示分辨率)。

本文的目的

研究各个平台提供的把控件摆放到你想要的排版格式之中去的方法。并且最终给出自己的一些看法。

 

控件的大小

先从控件的大小说起。控件的大小有实际尺寸,有最适尺寸,有最小尺寸,有最大尺寸。在有的库中有所有这些概念,有的库中只能自己知道有这些东西,但是库并不显式的支持。最适尺寸就是控件在计算完了内容控件的大小之后,自己应该是多大的那个尺寸,是一个自然尺寸。而实际尺寸和最小最大都是手工指定的尺寸。如果一个窗口的所有控件大小都不手工指定尺寸,那么很有可能尺寸的决定完全就是基础内部的文字的字体的大小了。

控件的大小属性是排版的基础。排版无非就是算出一个位置和大小来。在这个过程中,可能排版者(抽象的程序概念,相当于Swing中的LayoutManager)需要从控件那儿知道需要多大,或者最小多大的概念,也可能是会在排版过程中因为版式需要改变控件的实际大小(只可能改变实际大小,最适尺寸是基于内部组成大小计算的而最大最小是我们手工指定的)。

 

按象素排版

这个是最简单和直观的。在创建窗体的时候给出的是象素的大小,指定控件位置的时候也是如此。

比如这个图片就是从《Programming Windows》这本书中取出来的。而创建上图中的一个控件的代码是这样的:

for (i = 0 ; i < NUM ; i++)

               hwndButton[i] = CreateWindow ( TEXT("button"),

                                   button[i].szText,

                                   WS_CHILD | WS_VISIBLE | button[i].iStyle,

                                   cxChar, cyChar * (1 + 2 * i),

                                   20 * cxChar, 7 * cyChar / 4,

                                   hwnd, (HMENU) i,

                                   ((LPCREATESTRUCT) lParam)->hInstance, NULL) ;

其中那个cxCharcyChar这些就是绝对的象素值。

 

利用容器排版

这里的容器指的是Gtk+中的Box的概念,以及Swing中的部分Flow Layout以及Box LayoutContainer。他们的原理就是控件放在容器之中,指定的容器有自己的排版行为,比如先添加的在左边,然后一次往右排。

比如上图就是来自Gtk+2.0tutorial中的一副图片。产生一行控件的代码是这样的:

    box = gtk_hbox_new (homogeneous, spacing);

   

    button = gtk_button_new_with_label ("gtk_box_pack");

    gtk_box_pack_start (GTK_BOX (box), button, expand, fill, padding);

    gtk_widget_show (button);

核心内容就是窗口直接包含的是一个boxbox可以是横向的也可以是纵向的。控件可以插入到box之中,可以选择从前插入和还是从后插入,box自己的属性以及插入的时候都可以指定诸如空白之类。而box可以包含在另外的box之中,这个就是能够利用box排出复杂的版面的关键。


代码如:

listPane.setLayout(new BoxLayout(listPane, BoxLayout.PAGE_AXIS));
JLabel label = new JLabel(labelText);
listPane.add(label);
listPane.add(Box.createRigidArea(new Dimension(0,5)));

 

利用网格排版

网格这个概念在不同库中是不一样的,有的叫Table,有的叫Form,有的叫Grid。其实就是打格子,然后把控件放入格子之中。有的规定格子等大,比如SwingGridLayout,有的让你指定每个行列的属性,让属性动态的决定格子的形状(比如跟着控件的最适大小走)。这个是我认为的最好的排版方法,虽然有一个很小的缺点是当控件的边缘可能不能对齐的时候很不好处理,但是利用grouppanel的办法也是很好解决的。


代码如:

table = gtk_table_new (2, 2, TRUE);


代码如:

pane.setLayout(new GridLayout(0,2));
 
pane.add(new JButton("Button 1"));
pane.add(new JButton("Button 2"));
pane.add(new JButton("Button 3"));
pane.add(new JButton("Long-Named Button 4"));
pane.add(new JButton("5"));

 

按相对关系排版

这里有两个代表,一个适Swing中的SpringLayout以及WindowsFormsAnchor&Dock的排版方式。基本的概念就是让控件与控件的关系位置来确定其最终的位置。


这个是Swing中的BorderLayout

pane.add(button, BorderLayout.CENTER);

把按钮放在中心

这个是SpringLayout,代码如:

layout.putConstraint(SpringLayout.NORTH, label,
                     5,
                     SpringLayout.NORTH, contentPane);

可以看到指定的是控件与控件的相对位置。

这个是WinForm产生的窗口,左边的OutLookBar就是用Dock附着在窗口左边的。

 

我的看法

个人觉得功能最强大的是SwingLayoutManager的概念,提供了无限的排版可能。而且控件本身就提供了各种尺寸属性的支持。而最实用的应该是WindowsForm中提供的Dock的排版方式。而最务实的排版者的态度是对不同场合需要选择最简单最方便最能够应对变化的排版方式。下面,就从排版的最终目的,我来分析一下我自己的观点:

 

无论排版方式提供得多么花俏,功能有多么灵活和强大,GUI排版的目的其实是非常简单的。就是给窗口内部的控件一个位置,一个大小。最终达到的效果是让用户能够感受到和GUI美工脑海中想象的相同的的视觉效果。这里就有几个元素,一个是控件在窗口中的位置,控件的大小,控件之间相对的关系(比如之间的空白,对齐等)。其中每一点都非常重要,比如控件如果在窗口中的位置会在某些情况下窗口内,某些情况下被显示在窗口边缘之外从而看不见那就非常难看了。而一个大得吓人的按钮同样也是非常糟糕的设计,两个控件在一种状态下是齐平的,而在另外一种状态下就不平了,这些都是非常糟糕的设计结果。各种排版手段为的都是达到这些目的,而起点也都是相同的,有窗口,有控件,把控件显示在窗口上。

 

GUI排版难以设计的最大问题是在于变化。GUI排版和报纸漫画排版不同之处就在于,报纸这些东西的最终输出大小是固定的,已知的。而GUI要面对的是一个变化的环境,而且这种变化并不是说我给PC做一个界面,而掌机做另外一个界面就能够解决问题的。变化因素有这么几点:窗口大小(用户能够缩放窗口),显示分辨率,窗口中的内容(文字的多少和长度),底层平台(比如WindowsX-Window)。无论哪一点都是让人头疼的因素。

 

目的之一——让控件显示在窗口的一侧。

这个目的一般是要做一个工具栏,或者状态栏,或者是左边的选择栏之类的东西。在Delphi中的Alignment.net中的DockSwing中的BorderLayout都达到了同样的目的。这种需求也是被满足得最好的。

 

目的之二——让控件与控件对齐

这个问题是看似简单,实则复杂的问题。为什么这么说呢,还是因为前面讲的种种可变因素。在最简单的固定情况下,我们可以选择使用直接指定象素的办法,让控件与控件拥有相同的xy坐标,从而一致。虽然直接指定象素的结果可能让缩放变得困难了,但是对于大多数需要对齐控件的场合来说,是非常合适的,比如按钮和文本框,它们一旦缩放会非常难看。

其他的办法有利用一个Panel把需要对齐的控件放在一起,然后把Panel放到窗口之上。还有BoxLayout什么的,很多排版方式都隐含有把控件放在同一个水平线或者垂直线上的含义。但是他们有一个共同的确定就是在指定了横向的时候,就把控件绑死了,很难把另外一个方向也用同样的方法对齐,比如一个控件就不能利用grouppanel的办法同时进行两个方向的对齐。

最佳的办法是网格排版。其实按象素排版也是按网格排版,只是它的单位是显示支持的最小的单位,是细得不能再细的网格。所以按象素排版劣于按网格排版之处也就在于它的网格的大小是不能改变的。Swing中的FormLayoutGtk+中的TableLayout都是比较不错的选择。你能够通过指定控件位于同样的xy网格位置来得到对齐了的控件。而且缩放和空白等其他问题,都能通过网格大小可调这个特性在上一个层次中得到解决,只是有的很差(比如GridLayout的固定网格大小),而有的很好(比如FormLayout的全面支持网格单个属性)。

 

目的之三——控件与控件之间留出空白

这又是另外一个大难题,而且经常是和对齐在一起的。有好多种解决方案,最现而易见的还是按象素排版的指定不同象素值,在中间计算出差值就是空白了。

和上面说的同样的道理,网格排版也是类似于象素排版的,只要让控件和控件之间在网格中的索引值不同就可以留出空格了。而且这个空白的大小完全是可变可调的(忘记Swing中的GridLayout吧,那就是垃圾)。

还有办法就是用容器排版或者按Dock排版的时候,用一个空白的控件做空白。在SwingBoxLayout中有一个Filler的概念就是这样的。而在.net中当把控件Dock到窗口的一侧可能需要留一点空白的边的时候,就需要用一个空的panelDock到那一层,占去一点空白的位置。这个办法在有些时候也是不错的选择。

控件的Border在很多库中是不大使用的东西,在Swing中确经常用来留出控件之间的空白。我觉得这是一个非常不好的习惯。因为我们思考排版的时候,总是把控件的内容边缘作为思考的边缘,如果控件本身充当了排版的职责,则在GUI排版的混水中又加了一个混乱角色的概念。既然能够用Border之外的办法解决,为什么要麻烦它呢?

 

目的之四——让控件与控件保持相同大小

这个相对容易,无论在什么库中都能够比较容易的作出这个效果,只是容易还是复杂了。这个保持大小的意思其实是说保持和一组尺寸中最大的那个一致。

SwingGridLayout把所有加入的控件都拉伸为同样的长和宽。在FormLayout中的也有设置某几个列或者行的大小一致的功能。可以看到,网格排版中要做到这一点相对容易。而其他排版方式似乎就比较困难,你需要在界面重排的时候手工的把他们的大小设为一样的。或者干脆用象素排版,在设计的时候固定下来。

 

目的之五——应对缩放

对于缩放比较极端的态度是.net 中的ReSize控件的办法,让所有成员控件的大小和字体大小与窗体大小同比例缩放,我以前在用Delphi写程序的时候一度也这么使用过。其实很多控件是不适合缩放的,应该说是大多数控件都不能让你随便的缩放。代表性的有按钮,和文本框。而比较适合缩放的是一些起到组织控件角色的大的控件,比如一个ListBox就非常适合随着父窗口的缩放而缩放。

对于缩放有两种处理方式,除了前面的那种同比例缩放之外,还有一个就是BorderLayout或者Alignment的办法,让停靠在窗口侧面的控件把自己延伸方向的空间吃掉,然后把所有的空间分配给中间填充的那个控件。两种方法都是很有用的办法。

关键就是合理的把小控件用大的控件组合起来,比如group到一个panel之中,然后把小控件的位置定在一个合适的位置,比如中心。然后让外面的世界去变吧,大的框架性的控件才是缩放真正需要关心的对象。比较失败的一个例子是Fox Toolkit这个C++ GUI库的计算器这个sample,它把所有的按钮和主窗口一起缩放,结果是非常难看的缩放后的景象。如果你的程序就像计算器一样,根本没有缩放的余地,simply disable it

 

目的之六——让GUI设计器来排版

在这点上马上就能看出网格排版的好处,其对于GUI设计器是非常natrual的一种排版方式。而其他的排版方式也或多或少的不受GUI设计器的欢迎。从这里也可以看出为什么JAVA的可视化设计工具特别少了,而JBuilder又是多么的笨拙。

 

综合起来

GUI的问题出在了最终的综合,种种的排版方式加上各种小技巧,足够排出任意样子的东西来。但是问题出在了在一层中只能使用一个排版方式之上。这里的一层只的是一个抽象的概念,如果你的程序比较简单那么整个主窗口就是一层,但是你的程序可能复杂到了需要自己把一些东西放入到一个panel之中,然后把panel放到主窗口之中的地步,那么这就出了主窗口这个层之下的另外一个层面。这种一层一个排版方式的限制最具体的表现就是一个SwingContainer就只能有一个Layout。所以除了学习有多少Layout方法手段之外就是给你主窗口分出合适的层,然后用合适的Layout把层和控件组织起来达到理想的排版效果。就我的经验来说,最有用的就两个:

FormLayout + Docking

微软的Dock&Anchor几乎就完美了,只要把基于象素编程基于网格,再给这个容器的网格属性指定非常灵活好用的属性定制功能。

最有潜力的GUI系统是Swing,它提供的LayoutManager无比强大,而且FormLayout的成功也说明了这一点。如果再加上一个很好的GUI设计器将是Sun征服桌面的一个很好的开始。

 

遗憾:

没能包括所有库和平台,QT啊,SWT啊这些。而去对于各个库,很多不是天天在手上用就不大熟悉。对于GUI设计器知识的欠缺也是一个遗憾。对于这么多排版手段的讨论也是非常不够深度的,希望有朝一日能够再来整理一下文章的脉络以及添加更多内容。

posted on 2004-07-01 00:00  taowen  阅读(3028)  评论(0编辑  收藏  举报