Swing 提供了多種特殊容器,能夠滿足複雜界面佈局需求。本文將深入講解 JSplitPane(分割面板)、JTabbedPane(標籤面板)、JLayeredPane(分層面板),以及用於實現 MDI(多文檔界面)的 JDesktopPane 與 JInternalFrame。這些容器功能強大,透過完整代碼示例幫助你快速掌握核心用法。
JSplitPane 允許將兩個組件按水平或垂直方向分割,並提供可拖動的分隔條調整比例。常見於「左側導航+右側內容」或「上方工具欄+下方工作區」佈局。
HORIZONTAL_SPLIT(水平)與 VERTICAL_SPLIT(垂直)setContinuousLayout(true))setOneTouchExpandable(true) 顯示箭頭按鈕resetToPreferredSizes() 依組件 preferredSize 自動調整分割比例以下程式展示巢狀分割面板:左側垂直分割顯示封面與描述,右側放置書籍列表;選擇列表項目即時更新左側內容。
public class BookBrowser {
private static class Book {
private final String name;
private final Icon cover;
private final String description;
public Book(String name, Icon cover, String description) {
this.name = name;
this.cover = cover;
this.description = description;
}
public String getName() { return name; }
public Icon getCover() { return cover; }
public String getDescription() { return description; }
@Override public String toString() { return name; }
}
public void init() {
// 準備測試資料
Book[] books = {
new Book("Struts 2 權威指南", new ImageIcon("ico/struts2.jpg"), "全面介紹 Struts 2 核心知識"),
new Book("輕量級 J2EE 企業應用實戰", new ImageIcon("ico/j2ee.jpg"), "Struts、Spring、Hibernate 整合開發"),
new Book("基於 J2EE 的 Ajax 寶典", new ImageIcon("ico/ajax.jpg"), "J2EE 平台 Ajax 開發全方位指南")
};
JFrame frame = new JFrame("JSplitPane 示例:書籍瀏覽器");
JList<Book> bookList = new JList<>(books);
JLabel coverLabel = new JLabel();
JTextArea descArea = new JTextArea();
descArea.setEditable(false);
descArea.setLineWrap(true);
descArea.setWrapStyleWord(true);
// 設定組件優先尺寸
bookList.setPreferredSize(new Dimension(180, 400));
coverLabel.setPreferredSize(new Dimension(300, 200));
descArea.setPreferredSize(new Dimension(300, 200));
// 列表選擇監聽器
bookList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
Book selected = bookList.getSelectedValue();
if (selected != null) {
coverLabel.setIcon(selected.getCover());
descArea.setText(selected.getDescription());
}
}
});
// 左側垂直分割:封面(上)+ 描述(下)
JSplitPane leftPane = new JSplitPane(
JSplitPane.VERTICAL_SPLIT, true, coverLabel, new JScrollPane(descArea));
leftPane.setOneTouchExpandable(true);
leftPane.setDividerLocation(200);
leftPane.resetToPreferredSizes();
// 整體水平分割:左側面板 + 右側列表
JSplitPane mainPane = new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT, leftPane, new JScrollPane(bookList));
mainPane.setOneTouchExpandable(true);
mainPane.setDividerLocation(400);
frame.add(mainPane);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new BookBrowser().init());
}
}
JTabbedPane 是實現多文檔/多功能頁面切換的標準組件,支援標籤位置(上下左右)、佈局策略(換行/滾動)、圖標、工具提示、動態加載內容等豐富功能。
TOP、BOTTOM、LEFT、RIGHTWRAP_TAB_LAYOUT(自動換行)與 SCROLL_TAB_LAYOUT(滾動箭頭)ChangeEvent,僅在標籤首次被選中時建立組件,提升啟動效能setIconAt(index, icon) 可視化標示已讀/未讀狀態示範標籤位置/佈局切換、延遲加載圖片、自訂單選按鈕面板控制屬性。
public class DynamicTabDemo {
private final JFrame frame = new JFrame("JTabbedPane 進階示例");
private final JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.LEFT, JTabbedPane.WRAP_TAB_LAYOUT);
private final Map<String, String> bookCovers = new LinkedHashMap<>();
private final ImageIcon defaultIcon = new ImageIcon("ico/close.gif");
private final ImageIcon openedIcon = new ImageIcon("ico/open.gif");
public void init() {
// 初始化書籍資料
bookCovers.put("ROR 敏捷開發最佳實踐", "ror.jpg");
bookCovers.put("Struts 2 權威指南", "struts2.jpg");
bookCovers.put("基於 J2EE 的 Ajax 寶典", "ajax.jpg");
bookCovers.put("輕量級 J2EE 企業應用實戰", "j2ee.jpg");
bookCovers.put("Spring 2.0 寶典", "spring.jpg");
// 建立標籤(初始組件為 null,延遲加載)
for (String title : bookCovers.keySet()) {
tabbedPane.addTab(title, defaultIcon, null, "點擊查看封面");
}
// 標籤切換監聽器:延遲加載內容
tabbedPane.addChangeListener(e -> {
int idx = tabbedPane.getSelectedIndex();
if (tabbedPane.getComponentAt(idx) == null) {
loadTabContent(idx);
}
});
// 預載第一個標籤
loadTabContent(0);
// 控制面板:切換標籤位置與佈局策略
JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
controlPanel.setBorder(BorderFactory.createTitledBorder("標籤屬性控制"));
String[] placements = {"頂部", "底部", "左側", "右側"};
String[] layouts = {"換行佈局", "滾動佈局"};
controlPanel.add(createRadioGroup("標籤位置", placements, placement -> {
switch (placement) {
case "頂部": tabbedPane.setTabPlacement(JTabbedPane.TOP); break;
case "底部": tabbedPane.setTabPlacement(JTabbedPane.BOTTOM); break;
case "左側": tabbedPane.setTabPlacement(JTabbedPane.LEFT); break;
case "右側": tabbedPane.setTabPlacement(JTabbedPane.RIGHT); break;
}
}));
controlPanel.add(createRadioGroup("佈局策略", layouts, layout -> {
if ("換行佈局".equals(layout)) {
tabbedPane.setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT);
} else {
tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
}
}));
frame.add(tabbedPane, BorderLayout.CENTER);
frame.add(controlPanel, BorderLayout.SOUTH);
tabbedPane.setPreferredSize(new Dimension(600, 400));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private void loadTabContent(int index) {
String title = tabbedPane.getTitleAt(index);
String imgFile = bookCovers.get(title);
ImageIcon cover = new ImageIcon("ico/" + imgFile);
JLabel label = new JLabel(cover);
label.setHorizontalAlignment(SwingConstants.CENTER);
tabbedPane.setComponentAt(index, new JScrollPane(label));
tabbedPane.setIconAt(index, openedIcon);
}
private JPanel createRadioGroup(String title, String[] options, Consumer<String> handler) {
JPanel panel = new JPanel();
panel.setBorder(BorderFactory.createTitledBorder(title));
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
ButtonGroup group = new ButtonGroup();
for (int i = 0; i < options.length; i++) {
JRadioButton rb = new JRadioButton(options[i]);
rb.setActionCommand(options[i]);
rb.addActionListener(e -> handler.accept(((JRadioButton) e.getSource()).getActionCommand()));
rb.setSelected(i == 0);
group.add(rb);
panel.add(rb);
}
return panel;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new DynamicTabDemo().init());
}
}
JLayeredPane 允許組件在 Z 軸方向重疊,透過「層級」控制繪製順序。預設層級常數(由低到高):DEFAULT_LAYER、PALETTE_LAYER、MODAL_LAYER、POPUP_LAYER、DRAG_LAYER。亦可使用整數自定義層級(數值越大越靠上)。
setBounds(x, y, w, h) 指定位置大小add(comp, layer) 或 add(comp, intLayer)public class LayeredPaneDemo {
public void init() {
JFrame frame = new JFrame("JLayeredPane 示例:重疊卡片");
JLayeredPane layeredPane = new JLayeredPane();
layeredPane.setPreferredSize(new Dimension(500, 350));
// 新增三個不同層級的面板
layeredPane.add(createCard(20, 30, "Struts 2 權威指南", "ico/struts2.jpg"), JLayeredPane.MODAL_LAYER);
layeredPane.add(createCard(120, 80, "ROR 敏捷開發", "ico/ror.jpg"), JLayeredPane.DEFAULT_LAYER);
layeredPane.add(createCard(220, 130, "輕量級 J2EE 實戰", "ico/j2ee.jpg"), 150); // 自定義層級
frame.add(layeredPane);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private JPanel createCard(int x, int y, String title, String imgPath) {
JPanel panel = new JPanel();
panel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(), title));
panel.add(new JLabel(new ImageIcon(imgPath)));
panel.setBounds(x, y, 180, 220);
return panel;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new LayeredPaneDemo().init());
}
}
MDI(Multiple Document Interface) 允許在單一主視窗內管理多個子視窗。JDesktopPane 扮演「虛擬桌面」角色,JInternalFrame 則是可獨立最小化、最大化、關閉、調整大小的內部視窗。
JDesktopPane 內建 DesktopManager 處理拖曳、調整大小、圖標化等行為cascadeWindows)、平鋪(tileWindows)、切換下一個視窗(selectNextWindow)LIVE_DRAG_MODE(即時重繪)與 OUTLINE_DRAG_MODE(僅顯示輪廓)包含選單列、工具列、新建/關閉內部視窗、視窗排列、拖曳模式切換。
public class MdiEditor {
private static final int DESKTOP_W = 800;
private static final int DESKTOP_H = 600;
private static final int OFFSET = 30;
private final JFrame frame = new JFrame("MDI 文字編輯器");
private final CustomDesktopPane desktop = new CustomDesktopPane();
private int nextX = 0, nextY = 0;
private final int frameW = DESKTOP_W / 2;
private final int frameH = DESKTOP_H / 2;
public void init() {
// 選單列
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu("檔案(F)");
JMenu windowMenu = new JMenu("視窗(W)");
Action newAction = new AbstractAction("新建文檔", new ImageIcon("ico/new.png")) {
@Override public void actionPerformed(ActionEvent e) { createInternalFrame(); }
};
Action exitAction = new AbstractAction("退出", new ImageIcon("ico/exit.png")) {
@Override public void actionPerformed(ActionEvent e) { System.exit(0); }
};
fileMenu.add(newAction);
fileMenu.addSeparator();
fileMenu.add(exitAction);
windowMenu.add(new JMenuItem(new AbstractAction("下一個視窗") {
@Override public void actionPerformed(ActionEvent e) { desktop.selectNextWindow(); }
}));
windowMenu.add(new JMenuItem(new AbstractAction("級聯排列") {
@Override public void actionPerformed(ActionEvent e) { desktop.cascadeWindows(OFFSET, 0.75); }
}));
windowMenu.add(new JMenuItem(new AbstractAction("平鋪排列") {
@Override public void actionPerformed(ActionEvent e) { desktop.tileWindows(); }
}));
JCheckBoxMenuItem dragModeItem = new JCheckBoxMenuItem("僅顯示拖曳輪廓");
dragModeItem.addActionListener(e -> desktop.setDragMode(
dragModeItem.isSelected() ? JDesktopPane.OUTLINE_DRAG_MODE : JDesktopPane.LIVE_DRAG_MODE));
windowMenu.add(dragModeItem);
menuBar.add(fileMenu);
menuBar.add(windowMenu);
frame.setJMenuBar(menuBar);
// 工具列
JToolBar toolBar = new JToolBar();
toolBar.add(newAction);
toolBar.add(exitAction);
frame.add(toolBar, BorderLayout.NORTH);
// 桌面面板
desktop.setPreferredSize(new Dimension(DESKTOP_W, DESKTOP_H));
frame.add(desktop, BorderLayout.CENTER);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private void createInternalFrame() {
JInternalFrame iframe = new JInternalFrame("未命名文檔 " + (desktop.getAllFrames().length + 1),
true, true, true, true);
iframe.add(new JScrollPane(new JTextArea()));
desktop.add(iframe);
iframe.reshape(nextX, nextY, frameW, frameH);
try { iframe.setSelected(true); } catch (PropertyVetoException ignored) {}
iframe.setVisible(true);
nextX += OFFSET; nextY += OFFSET;
if (nextX + frameW > desktop.getWidth()) nextX = 0;
if (nextY + frameH > desktop.getHeight()) nextY = 0;
}
// 自訂桌面面板:提供級聯、平鋪、切換視窗邏輯
private static class CustomDesktopPane extends JDesktopPane {
public void cascadeWindows(int offset, double scale) {
int w = (int) (getWidth() * scale);
int h = (int) (getHeight() * scale);
int x = 0, y = 0;
for (JInternalFrame f : getAllFrames()) {
try {
f.setMaximum(false); f.setIcon(false);
f.reshape(x, y, w, h);
x += offset; y += offset;
if (x + w > getWidth()) x = 0;
if (y + h > getHeight()) y = 0;
} catch (PropertyVetoException ignored) {}
}
}
public void tileWindows() {
JInternalFrame[] frames = getAllFrames();
int n = frames.length;
if (n == 0) return;
int rows = (int) Math.sqrt(n);
int cols = n / rows;
int extra = n % rows;
int w = getWidth() / cols;
int h = getHeight() / rows;
int x = 0, y = 0;
for (int i = 0; i < n; i++) {
JInternalFrame f = frames[i];
try {
f.setMaximum(false); f.setIcon(false);
f.reshape(x * w, y * h, w, h);
y++;
if (y == rows) {
y = 0; x++;
if (extra == cols - x) { rows++; h = getHeight() / rows; }
}
} catch (PropertyVetoException ignored) {}
}
}
public void selectNextWindow() {
JInternalFrame[] frames = getAllFrames();
for (int i = 0; i < frames.length; i++) {
if (frames[i].isSelected()) {
int next = (i + 1) % frames.length;
while (next != i) {
if (!frames[next].isIcon()) {
try {
frames[next].setSelected(true);
frames[next].toFront();
frames[i].toBack();
return;
} catch (PropertyVetoException ignored) {}
}
next = (next + 1) % frames.length;
}
}
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new MdiEditor().init());
}
}
| 容器 | 適用場景 | 關鍵提示 |
|---|---|---|
| JSplitPane | 可調整比例的雙區域佈局(如導航+內容) | 善用 resetToPreferredSizes() 與 setOneTouchExpandable(true) |
| JTabbedPane | 多功能頁面切換、設定面板、多文檔介面 | 延遲加載標籤內容;動態切換圖標提升 UX |
| JLayeredPane | 組件重疊、浮動元素、自訂彈出層 | 必須使用絕對定位(setBounds);層級數值越大越靠上 |
| JDesktopPane + JInternalFrame | MDI 應用、多視窗編輯器、IDE 風格介面 | 繼承 JDesktopPane 封裝級聯/平鋪/切換邏輯;善用 DesktopManager 自訂拖曳行為 |
這四大特殊容器是構建專業級 Swing 應用的基石。建議先從 JSplitPane 與 JTabbedPane 入手,掌握基本佈局與事件處理後,再挑戰 JLayeredPane 的分層藝術與 MDI 的完整視窗管理。實作過程中請參考官方 API 文檔深入了解各屬性與方法的細節。