Swing 的「可插拔觀感」是其區別於 AWT 的核心特性。通過 UIManager 統一管理 LookAndFeel 實例,實現運行時無縫切換、跨平台一致性、自定義主題。本文將剖析觀感架構、內建觀感對比、動態切換實現、自定義主題開發、生產級主題管理器模式。
| 類/介面 | 角色 | 關鍵方法 |
|---|---|---|
| UIManager | 觀感註冊中心、默認值存儲 | setLookAndFeel(), getInstalledLookAndFeels(), put(), get() |
| LookAndFeel | 抽象基類,定義觀感元數據 | getName(), getID(), initialize(), uninitialize() |
| ComponentUI | 每個組件的 UI 委派實現 | createUI(JComponent), paint(), getPreferredSize() |
| UIDefaults | 鍵值對存儲:顏色、字體、邊框、圖標、UI 類名 | get(key), put(key, value) |
// 獲取所有已安裝觀感
UIManager.LookAndFeelInfo[] infos = UIManager.getInstalledLookAndFeels();
for (UIManager.LookAndFeelInfo info : infos) {
System.out.printf("Name: %s, Class: %s%n", info.getName(), info.getClassName());
}
| 觀感名稱 | 類名 | 特點 | 適用場景 |
|---|---|---|---|
| Metal (默認) | javax.swing.plaf.metal.MetalLookAndFeel | 純 Java 實現、跨平台一致、風格較老舊 | 嵌入式、需統一外觀環境 |
| Nimbus | javax.swing.plaf.nimbus.NimbusLookAndFeel | JDK 6u10+、向量渲染、現代外觀、支援縮放 | 現代桌面應用首選 |
| Windows | com.sun.java.swing.plaf.windows.WindowsLookAndFeel | 原生 Windows 風格、僅 Windows 可用 | Windows 專用發布 |
| Windows Classic | com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel | Win98/2000 風格 | 兼容舊系統 |
| Mac OS X | com.apple.laf.AquaLookAndFeel | 原生 macOS 風格、僅 macOS 可用 | macOS 專用發布 |
| GTK+ | com.sun.java.swing.plaf.gtk.GTKLookAndFeel | Linux GTK+ 2/3 風格 | Linux 桌面整合 |
| Motif | com.sun.java.swing.plaf.motif.MotifLookAndFeel | Unix Motif 風格、極簡 | 極簡環境 |
import javax.swing.*;
import javax.swing.plaf.nimbus.NimbusLookAndFeel;
import java.awt.*;
import java.awt.event.*;
public class LookAndFeelDemo {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
// 1. 設置默認觀感(啟動前調用)
setSystemLookAndFeel();
// 2. 創建主窗口
JFrame frame = new LaFFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
/** 設置系統原生觀感(跨平台自適應) */
private static void setSystemLookAndFeel() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
// 回退到 Metal
try { UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); }
catch (Exception ignored) {}
}
}
}
class LaFFrame extends JFrame {
private final JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
public LaFFrame() {
setTitle("Swing 觀感切換演示");
setSize(500, 350);
setLocationRelativeTo(null);
// 動態生成觀感切換按鈕
for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
addLaFButton(info.getName(), info.getClassName());
}
// 額外添加 Nimbus(若未在列表中)
if (Arrays.stream(UIManager.getInstalledLookAndFeels())
.noneMatch(i -> "Nimbus".equals(i.getName()))) {
addLaFButton("Nimbus", NimbusLookAndFeel.class.getName());
}
add(buttonPanel, BorderLayout.NORTH);
add(createDemoPanel(), BorderLayout.CENTER);
}
private void addLaFButton(String name, String className) {
JButton btn = new JButton(name);
btn.addActionListener(e -> switchLookAndFeel(className));
buttonPanel.add(btn);
}
private void switchLookAndFeel(String className) {
try {
// 1. 設置新觀感
UIManager.setLookAndFeel(className);
// 2. 遞歸更新組件樹(關鍵!)
SwingUtilities.updateComponentTreeUI(this);
// 3. 重新佈局(防止尺寸異常)
pack();
setLocationRelativeTo(null);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this,
"切換失敗: " + ex.getMessage(), "錯誤", JOptionPane.ERROR_MESSAGE);
ex.printStackTrace();
}
}
/** 演示面板:包含常用組件 */
private JPanel createDemoPanel() {
JPanel panel = new JPanel(new GridLayout(0, 1, 5, 5));
panel.setBorder(BorderFactory.createTitledBorder("組件測試區"));
panel.add(new JButton("JButton"));
panel.add(new JCheckBox("JCheckBox"));
panel.add(new JRadioButton("JRadioButton"));
panel.add(new JTextField("JTextField"));
panel.add(new JComboBox<>(new String[]{"Item 1", "Item 2", "Item 3"}));
panel.add(new JSlider(0, 100, 50));
panel.add(new JProgressBar(0, 100));
JTable table = new JTable(new Object[][]{{"A", "1"}, {"B", "2"}}, new Object[]{"Col1", "Col2"});
panel.add(new JScrollPane(table));
return panel;
}
}
public static void updateComponentTreeUI(Component c) {
// 1. 更新該組件的 UI 委派
if (c instanceof JComponent) {
((JComponent) c).updateUI(); // 呼叫 look-and-feel 特定的 updateUI()
}
// 2. 遞歸更新子組件
if (c instanceof Container) {
for (Component child : ((Container) c).getComponents()) {
updateComponentTreeUI(child);
}
}
// 3. 觸發重繪
c.revalidate();
c.repaint();
}
注意:updateUI() 會重新創建 ComponentUI 實例,舊的監聽器、渲染器可能丟失,自定義組件需重寫 updateUI() 保留狀態。
| 問題 | 原因 | 解決 |
|---|---|---|
| 菜單/彈出框未更新 | JPopupMenu/JMenuBar 不在組件樹中 | 手動調用 SwingUtilities.updateComponentTreeUI(menuBar) |
| 自定義渲染器失效 | updateUI 重置了 renderer/editor | 在 updateUI() 中重新設置 |
| 字體/顏色異常 | UIDefaults 被覆蓋 | 切換前備份自定義默認值,切換後恢復 |
| 內存洩漏 | 舊 ComponentUI 未釋放資源 | 實現 LookAndFeel.uninitialize() 清理 |
import javax.swing.*;
import java.util.*;
import java.util.prefs.Preferences;
public final class ThemeManager {
private static final ThemeManager INSTANCE = new ThemeManager();
private final Preferences prefs = Preferences.userNodeForPackage(ThemeManager.class);
private static final String KEY_LAF = "lookAndFeelClassName";
private ThemeManager() {}
public static ThemeManager getInstance() { return INSTANCE; }
/** 獲取當前觀感類名 */
public String getCurrentLookAndFeel() {
return UIManager.getLookAndFeel().getClass().getName();
}
/** 切換並持久化觀感 */
public boolean setLookAndFeel(String className) {
try {
UIManager.setLookAndFeel(className);
prefs.put(KEY_LAF, className);
return true;
} catch (Exception e) {
return false;
}
}
/** 應用持久化觀感(應用啟動時調用) */
public void applySavedTheme() {
String saved = prefs.get(KEY_LAF, null);
if (saved != null) {
setLookAndFeel(saved);
} else {
// 默認系統觀感
try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); }
catch (Exception ignored) {}
}
}
/** 獲取可用觀感列表(含第三方) */
public List getAvailableLookAndFeels() {
List list = new ArrayList<>();
for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
list.add(new LaFInfo(info.getName(), info.getClassName()));
}
// 掃描類路徑發現第三方觀感(如 FlatLaf, Synthetica)
// 略...
return list;
}
public record LaFInfo(String name, String className) {}
/** 批量應用自定義 UIDefaults(字體、顏色統一風格) */
public void applyCustomDefaults(UIDefaults defaults) {
UIDefaults current = UIManager.getLookAndFeelDefaults();
defaults.forEach(current::put);
// 觸發現有窗口更新
for (Window w : Window.getWindows()) {
SwingUtilities.updateComponentTreeUI(w);
}
}
}
| 庫 | 特點 | Maven 坐標 |
|---|---|---|
| FlatLaf | 現代扁平化、支援深色/淺色模式、高 DPI、跨平台一致、活躍維護 | com.formdev:flatlaf:3.4 |
| Synthetica | 商業級、豐富主題、高性能、需授權 | 商業授權 |
| Substance | 動畫豐富、皮膚系統、項目已歸檔 | 不再維護 |
| Darcula (IntelliJ) | IDE 風格深色主題、內建於 IntelliJ Platform | 僅 IDE 內部 |
// 依賴
com.formdev
flatlaf
3.4
// 啟動代碼
import com.formdev.flatlaf.FlatLightLaf;
import com.formdev.flatlaf.FlatDarkLaf;
// 淺色模式
FlatLightLaf.setup();
// 或深色模式
FlatDarkLaf.setup();
// 自動跟隨系統深色模式 (JDK 16+)
FlatLaf.setup(new FlatLaf.Properties() {{
put("theme", "auto");
}});
Swing 觀感機制的核心要點:
LookAndFeel 與 UIDefaultssetLookAndFeel() → updateComponentTreeUI() → pack()/revalidate()getSystemLookAndFeelClassName() 原生融合,getCrossPlatformLookAndFeelClassName() (Metal) 統一外觀Preferences API 保存用戶選擇,啟動時自動應用本文提供的 ThemeManager 封裝了切換、持久化、自定義默認值批量應用、窗口級更新傳播,可直接集成到任何 Swing 應用中實現專業級主題管理。