原文:http://tieba.baidu.com/p/2101752057
Swing 新手最常见的一些问题就是:
为什么我的 setSize / setLocation / setBounds 没起作用?
为什么我的界面布局很难看?
为什么我把JPanel加进JScrollPane里,JScrollPane一直自动适应大小就是不显示滚动条?
…………
希望这篇基础教程能帮你解答类似的疑惑。
有些地方虽然也写进本文,其实我也一知半解,所以需要 @windsun_ul 来把关。
>>> Swing 界面布局的方式 —— LayoutManager接口
在Swing里面,任何一个控件都是一个容器。
你可能听说过Swing里所有的控件都继承自 JComponent 这个类。 如果你去翻API文档,就会发现 JComponent 类继承自 Container 类,Container 就是容器。
容器可以看成一个层面,在它的上面可以添加其他的组件或者容器,称为它的子控件(children),添加到同一个容器内的子控件位于同一层,比容器本身高一层。Swing 的绘图方式是从最底层开始一层一层来绘制的,高层的绘制覆盖低层的绘制。(实际的绘制策略比这稍微更复杂一点,这里方便理解,不作详述)
一个容器,负责完成自己所有子控件的布局排列和绘制。在 Swing 里面,一个通用的接口用来帮助容器完成布局排列,这个接口就是 LayoutManager 接口, 常用的 BorderLayout, FlowLayout, GridLayout 等都是其实现。
在实际应用中,LayoutManager 负责两件事:
1 - 给定当前容器的实际大小,尽最大努力对所有的子控件进行布局排列;
2 - 给定当前容器所有的子控件,以及它们“喜欢的”大小,计算出容器本身“喜欢的”大小(一个容器,假如你没有特别指定 setPreferredSize(),那么它的 getPreferredSize() 返回的就是它的 LayoutManager 帮它算出的大小)。
仔细琢磨一下就会发现,这两件事的思维方向正好相反,一个是“从外向内”,即外部环境已经确定的情况下,运算解决内部细节;另一个是“从内向外”,即内部细节确定的情况下,通知外部环境自己需要的空间大小。
>>> 顶级容器 —— 窗口
那么这两件事究竟哪个“说了算”呢?
这个问题最终被推给最底层的容器,窗口,也就是Window类。如果查一下API文档就会发现,JFrame, JDialog, JWindow 等都是继承自 Window 这个类,而 Window 类又继承自 Container,就是说窗口也是一个容器。
—— 假如这个最底层的容器的大小已经确定了,那所有的问题都能推知答案: 这个最底层容器所用的 LayoutManager会帮助它确定内部细节,即所有子容器的位置和大小,如是这些子容器的大小就被确定了,这些子容器的 LayoutManager 又有了依据可以继续运算………… 最终所有控件的位置和大小都可以确定下来。
就像你猜想的那样,窗口的大小可以用 setSize 来确定。
—— 那么假如我想写一个对内部控件特别尊重的窗口呢? 假如我不愿意用 setSize 来规定窗口大小,而是想问问内部所有的控件“你们觉得窗口多大最合适”呢? 很简单,窗口有一个 pack() 方法,这个方法在 API 里的描述是“Causes this Window to be sized to fit the preferred size and layouts of its subcomponents”,意思是调整窗口大小以适应其内部布局与子控件喜欢的大小。是不是有些眼熟?这不就是前面说的 LayoutManager 负责的第二件事嘛!
窗口在建立的时候就已经带了一些容器,并且有它们的默认布局,这里不作详述,有兴趣可以看司马的帖子《
【科普】JFrame的层次结构究竟是什么样的,什么又是ContentPane?》(http://tieba.baidu.com/p/2004216123)。
常用的一个就是 contentPane ,默认的 getContentPane() 返回类型是 Container,其实它是一个普通的 JPanel,跟一般 JPanel 的区别就在于它被设置布局为 BorderLayout(一般的JPanel默认自带的布局是 FlowLayout)。
要注意的是,如果你直接调用 JFrame 的 add() 方法来添加子控件,其实它内部就是调用 getContentPane().add(),就是说,控件是被添加进了 contentPane 里面。同样的还有 setLayout 方法,也是内部调用了 getContentPane().setLayout()。
你可以调用 setContentPane() 来设置你自己定制的 Container。
LayoutManager 所负责的第一件事,有的人可能有疑问: 在 Container 大小一定的情况下,如果子控件需要的空间超出了范围,放不下了怎么办?
答案是: 这正是LayoutManager需要解决的问题呀。
Container 说: LayoutManager老兄,这里是我现在的大小,这里是所有的子控件,你看着办吧。
LayoutManager 对于所有的子控件有生杀予夺大权,可以设置它们的 setVisible(), setLocation(), setSize(), setBounds() 等,其中 setBounds() 相当于 setLocation() + setSize()。 在LayoutManager的实现中,你有权利决定哪些子控件可见,哪些不可见,还有任何一个子控件的位置、大小。
同理,如果空间太多,你 LayoutManager 也得处理。
每一种 LayoutManager 的实现都规定了它自己的一套布局的规则。
FlowLayout最懒。
对第一件事,FlowLayout说:
子控件都来排队,先来后到,空间一共就这么多,从第一个加进来的子控件开始,每人报出你们的 getPreferredSize(),咱们一个一个,一行一行加进去,左右满了就换行,空间占满了为止。剩下的那些,对不起没位置了你们的脸可能露不出来了等下次容器大小变了再说吧。
至于第二件事,FlowLayout说:
你不是问我吗?那我连换行都懒得换了。——高度就是 getPreferredSize() 最高的那个控件,宽度就是它们都排成一行,getPreferredSize() 的宽度都加起来。
上面是大体的布局原则,除此之外还有些细节,FlowLayout还允许设置:
排列的对齐方式: 左? 右? 中间? setAlignment() 可以选的值有 FlowLayout.CENTER, LEFT, RIGHT, LEADING,TAILING
为啥有一个 LEADING 和 TAILING? 用一下发现 LEADING 不就是 LEFT 嘛!
其实对于 Swing 所有的布局,都有一个“ComponentOrientation”的概念,即某容器“是不是从左往右”,这是为了让容器自己可以方便切换“从左往右”或“从右往左”而不必更换LayoutManager,可以参考 Container 的 getComponentOrientation 方法。 而 LEADING 的意思就很明白了: 如果是从左往右,那我就是左对齐,反之我就是右对齐。
还可以设置控件间的水平间隔 setHgap(int) ,垂直间隔 setVgap(int)。
BorderLayout 比较负责任,它把整个容器的长方形区域划分成了五块,用一些 String 的 key 来记录每一块的位置: CENTER, NORTH, SOUTH, WEST, EAST
对于第一件事,BorderLayout是这么做的:
如果 NORTH 和 SOUTH 位置有控件,把它们拿出来,问它们喜欢的高度是多少,宽度必须是容器宽度,加进去分别放到顶上和底部,然后“可用的空间”减掉这两部分。然后如果 WEST 和 EAST 有控件,问它们喜欢的宽度是多少,高度必须是“当前剩余可用空间”的高度,加进去分别加到左边和右边,然后CENTER填充剩余的部分。
第二件事,根据控件的位置把它们喜欢的宽度或高度相加,算出整个容器喜欢的大小。
另外BorderLayout也支持几个其他的 key 比如 BEFORE_FIRST_LINE 等等,用的比较少可以自己参照API文档。
这里可以回答一个新手问题:
我有一个 JPanel 加进了 JScrollPane 里面,然后 jframe.add(jscrollpane),然后 jframe.pack() ,为什么不出现滚动条呢?
答:
JScrollPane 有它自己的 LayoutManager,这个LayoutManager在第二件事上尊重加进来的控件的 getPreferredSize(),所以当 jframe.pack() 的时候,JScrollPane 说: 我内部那个 JPanel 喜欢的size就是我喜欢的size。于是JPanel正好被完全显示,而 JScrollPane 默认的行为是“如果不需要就不要显示滚动条”。
这里要注意的是 jframe.add(jscrollpane); 相当于 jframe.getContentPane().add(jscrollpane, BorderLayout.CENTER);
当一个容器第一次被显示在屏幕上之前,LayoutManager至少会被调用一次做第一件事(layoutContainer方法)。
当一个容器的大小发生改变的时候,或者新加进了子控件的时候,容器就处于一种 "invalid" 状态,你也可以根据需要调用 container.invalidate() 方法,来让容器达到 "invalid" 状态。在容器被绘制以前,如果Swing发现容器处于 "invalid" 状态,那么其布局器的 layoutContainer 方法就会被调用,来重新布局。 "invalid"状态的作用就是告诉Swing的绘制机制: 这个容器内部的组件布局有问题,需要重新计算。
对于 JComponent 的子类型组件,一般内部布局或者子控件的大小有变化的时候,调用这两个方法让其重新计算布局并且重绘:
c.revalidate();
c.repaint();
JComponent 中定义的 revalidate 方法做了两件事: 调用父类型 Container 中的 invalidate 方法; 然后把自己加进 RepaintManager 的 "invalid" 控件列表内。
你也许被推荐过使用空布局,即 setLayout(null),然后手动设置每一个子控件的位置和大小(setBounds())。
空布局实际上就是每次手动去完成 LayoutManager 所做的工作。它的好处是简单直观,易被新手掌握,所以像司马这样的懒人就会推荐给新手使用
![](https://i-blog.csdnimg.cn/blog_migrate/f637f34ea17e2b5cf81b403a575a42ac.gif)
但是对于愿意在Swing方面进阶的同学,不要用空布局,学会 LayoutManager 的使用,甚至自己实现 LayoutManager, 是 Swing 入门的标志之一。
一个真正的Swing程序员极少使用空布局,因为: Java 是跨平台的。
Java是跨平台的,Swing程序应该支持多种皮肤,所以同样一个控件,它的大小并不是固定的,在不同的平台上有不同的大小。要恰如其分把所有的控件布局排列起来,然后窗口显示为一个合适的大小,最佳的实践就是使用LayoutManager。
在绝大部分 Swing 程序里,不要假定控件的大小是固定不变的。
一个好的 Swing 程序员,甚至在用了LayoutManager的前提下,尽量少去干涉控件“喜欢的”大小,少去调用控件的 setPreferredSize() 方法。特别是控件中有文字显示的时候,JLabel, JButton, JTextField, JTextArea 等等。因为,——不要假定文字的大小是固定不变的。尽量让控件自己,让控件所用的皮肤的 UI 类去根据绘图的需求计算它“喜欢”的大小。
Swing 自带的 LayoutManager,比如 BorderLayout, FlowLayout,特别是 GridBagLayout,组合嵌套使用,基本可以满足绝大部分的布局需求。如果嫌 GridBagLayout 不好用,可以试一下第三方的 MiGLayout。
如果这些都不能满足你的需求,那就自己去给LayoutManager接口写一个实现吧!
比如作者自己写的第一个LayoutManager是遇到了这样一个需求:
做一个类似 MS Word 的工具栏,工具栏固定高度,宽度随外部环境改变,当宽度足够的时候,显示大图标按钮,——当宽度不足的时候,显示中等size的小图表+文字列表,——再不足的时候,显示小图标按钮。——Swing里能实现吗?当然可以,写出你自己的LayoutManager,然后实现有三种显示方式的按钮就行啦。
LayoutManager接口一共就五个方法,除了前面提到的两件事用了三个方法(计算容器需要的大小包括了“喜欢的”大小和“最少需要的”大小),另外两个方法是注册和反注册子组件的位置,取决于具体的实现你可能不需要这两个方法,比如FlowLayout就不需要这两个方法,——留空就行啦。
有兴趣的同学还可以去看看 LayoutManager2 接口,这个我从来没用过,就不多说了。
当你写出了自己的 LayoutManager 实现,恭喜你对 Swing 的理解又加深了一步,回头看以前困惑的地方是不是有很多都迎刃而解了呢?