如果你使用過netBeans的GUI構件器Matisse,在感受到其拖拽帶來的方便的同時,也會發現自動生成的代碼“慘不忍睹”。例如Matisse默認的布局為GroupLayout,隨便拖拽3個組件生成的代碼如下
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {
jButton1 = new javax.swing.JButton();
jButton2 = new javax.swing.JButton();
jButton3 = new javax.swing.JButton();
jButton1.setText("jButton1");
jButton2.setText("jButton2");
jButton3.setText("jButton3");
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGap(60, 60, 60)
.addComponent(jButton1))
.addGroup(layout.createSequentialGroup()
.addGap(160, 160, 160)
.addComponent(jButton2)))
.addContainerGap(165, Short.MAX_VALUE))
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addContainerGap(191, Short.MAX_VALUE)
.addComponent(jButton3)
.addGap(134, 134, 134))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGap(45, 45, 45)
.addComponent(jButton1)
.addGap(18, 18, 18)
.addComponent(jButton2)
.addGap(46, 46, 46)
.addComponent(jButton3)
.addContainerGap(122, Short.MAX_VALUE))
);
}// </editor-fold>
無論Matisse發展得如何強大,但是其本質只是用來生成Java代碼而已,當你修改這代碼后,再逆向恢復成UI設
計器時Matisse卻出于自己的一套安全考慮不允許你這樣做,所以最終不得迫使開發人員放棄拖曳方式設計UI,
而統統采用面向代碼的方式。就我看來拖拽作業方式能解決80%的簡單布局,剩下的20%的專業細化GUI設計器是
做不到的。《netBeans6.0咸魚翻身與Swing稱霸桌面應用》一文有人在評論中說到“VB,Delphi,C++Bulider的
開發人員早已習慣了用拖曳的方式來畫頁面,界面都是保存成一個資配置源文件”,那么Java為什么不能模仿
這一點呢?下面就詳細介紹利用外部配置文件實現組件的布局。
第三部分:利用外部配置文件實現組件的布局
在學習本章之前,需要對前文介紹的2種自定義布局有所了解,分別是、《自定義布局管理器-FormLayout》、《自定義布局管理器-CenterLayout》本文介紹的配置文件均是以這兩種布局為基礎的。
開門見山,程序運行結果如下圖。
有4個組件,JButton、JScrollPane(內嵌JTree)、自定義組件ImageButton、一個JTextField。布局原則是JButton左邊界距離容器左邊界5像素、右邊界距離容器左邊界130像素、所以長度為130-5=125固定不變,JButton上邊界距離容器上邊界10像素,下邊界距離容器下邊界35像素,所以高度為35-10=25固定不變;JScrollPane位于容器的中央,其中左右兩邊距離容器兩邊均是20像素,所以JScrollPane的寬度隨著容器寬度的變化而變化,JScrollPane上下兩邊距離容器中心高度均是50像素,所以整體高度是100像素;ImageButton的上邊界距離容器的底部50像素,左邊界距離容器右邊界100像素,由于ImageButton會根據背景尺寸產生PreferredSize,所以右邊界、下邊界不用設置。剩下的JTextField不是通過配置產生,具體見后面介紹。
下面來看看xml配置,如下。
<ui-container>
(1) <layout-manager class="org.swingframework.layout.FormLayout" />
<components>
(2) <component id="1101" class="javax.swing.JButton">
(3) <form-data>
<left percentage="0.0" offset="5" />
<right percentage="0.0" offset="130" />
<top percentage="0.0" offset="10" />
<bottom percentage="0.0" offset="35" />
</form-data>
</component>
<component id="1102" class="javax.swing.JScrollPane">
<form-data>
<left percentage="0.0" offset="20" />
<top percentage="0.5" offset="-50" />
<right percentage="1.0" offset="-20" />
<bottom percentage="0.5" offset="50" />
</form-data>
</component>
<component id="1103" class="org.swingframework.component.ImageButton">
<form-data>
<left percentage="1.0" offset="-100" />
<top percentage="1.0" offset="-50" />
</form-data>
</component>
</components>
</ui-container>
(1)指定容器的布局類,目前僅支持FormLayout、CenterLayout兩種。
(2)定義一個組件,指定唯一的id和完整類名。
(3)為組件指定布局約束。
由于xml文檔結構是固定的,因此xml解析采用XPath。XPath已在JDK1.5中被集成,而且相比DOM更加簡單,關于XPath的更多內容參考其他資料。
定義布局注入LayoutInjection類,目的就是解釋一個給定的xml文件,然后去給一個容器生成內部組件并布局這些組件。
public class LayoutInjection {
private Container injectTarget;
private InputStream layoutSource;
private LayoutManager layoutManager;
private Map<String, ComponentEntry> entryMap;
public LayoutInjection(Container injectTarget, InputStream layoutSource) {
this.injectTarget = injectTarget;
this.layoutSource = layoutSource;
}
......
private class ComponentEntry {
private Component component;
private FormData formData;
ComponentEntry(Component component, FormData formData) {
this.component = component;
this.formData = formData;
}
public Component getComponent() {
return component;
}
public FormData getFormData() {
return formData;
}
}
}
單獨定義一個ComponentEntry內部類是為了將組件-布局約束關聯。定義一個injectLayout方法來完成布局,在injectLayout方法內部,首先讀取外部配置文件并將組件與布局約束保存到entryMap,然后為容器設置根據配置得到的布局管理器,下一步插入自定義屬性,包括非配置產生的組件,與組件修飾、組件事件監聽器等。最后一步是遍歷entryMap根據每個組件與其布局約束完成組件創建與布局。
public void injectLayout(){
if (!load()) {
Logger.getLogger(LayoutInjection.class.getName()).log(Level.WARNING, "load components failed");
return;
}
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
injectTarget.setLayout(layoutManager);
/* Notifies all listeners that have registered interest for notification. */
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying those that are ICustomer type.
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == ICustomer.class) {
try {
((ICustomer) listeners[i + 1]).customProperties(LayoutInjection.this);
} catch (Throwable ex) {
Logger.getLogger(LayoutInjection.class.getName()).log(Level.WARNING, ex.getLocalizedMessage(), ex);
}
}
}
synchronized (injectTarget.getTreeLock()) {
Set<String> idSet = entryMap.keySet();
for (String id : idSet) {
Component component = getComponentById(id);
FormData formData = getFormDataById(id);
try {
injectTarget.add((Component) component, formData);
} catch (Throwable ex) {
Logger.getLogger(LayoutInjection.class.getName()).log(Level.WARNING, ex.getLocalizedMessage(), ex);
}
}
injectTarget.doLayout();
}
}
});
}
customProperties方法是空實現,需要自己去實現邏輯。
protected void customProperties() {
}
injectLayout其他部分的實現參見完整源碼。
最后給一個Test測試這個類。
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.io.FileInputStream;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.WindowConstants;
import org.swingframework.component.ImageButton;
import org.swingframework.layout.FormAttachment;
import org.swingframework.layout.FormData;
import org.swingframework.layout.LayoutInjection;
public class Test {
public static void main(String[] args) throws Exception {
JFrame frm = new JFrame();
frm.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
final JPanel panel = new JPanel();
panel.setPreferredSize(new Dimension(350, 250));
frm.getContentPane().add(panel, BorderLayout.CENTER);
FileInputStream input = new FileInputStream("C:/layout.xml");
LayoutInjection injection = new LayoutInjection(panel, input);
injection.addCustomer(new ICustomer() {
public void customProperties(LayoutInjection source) {
JButton button = (JButton) source.getComponentById("1101");
button.setText("this is a JButton");
JScrollPane jsp = (JScrollPane) source.getComponentById("1102");
jsp.setBorder(BorderFactory.createLineBorder(new Color(128, 128, 128), 2));
jsp.getViewport().add(new JTree());
ImageButton imageButton = (ImageButton) source.getComponentById("1103");
imageButton.setBackgroundImage(new ImageIcon("button_up.png"));
imageButton.setRolloverBackgroundImage(new ImageIcon("button_over.png"));
imageButton.setPressedBackgroundImage(new ImageIcon("button_down.png"));
// add extend component
JTextField jtf = new JTextField();
jtf.setBorder(BorderFactory.createLineBorder(new Color(128, 128, 128), 2));
FormData jtfFormData = new FormData();
jtfFormData.top = new FormAttachment(0.8f, 0);
jtfFormData.left = new FormAttachment(0.2f, 0);
jtfFormData.right = new FormAttachment(0.2f, 100);
jtfFormData.bottom = new FormAttachment(0.8f, 25);
panel.add(jtf, jtfFormData);
}
});
injection.injectLayout();
input.close();
frm.pack();
frm.setVisible(true);
}
}
加紅的需要解釋以下,xml數據源是java.io.InputStream的實例,當然不局限與文件讀取;一般地,都要實現customProperties完成自定義邏輯,本例是修飾了組件外觀,另外以非配置方式加載了JTextField組件,以非配置方式添加組件是很必要的,因為實際應用的時候,你添加的更多的是已有的組件實例而不是xml文件中寫死的。
至此,這一節內容介紹完畢。下一節我想做個有關布局管理器的總結,并穿插我在開發當中的一些感悟。
源代碼這里下載