第15章:主题与样式

第15章:主题与样式


作者:步子哥 (steper@foxmail.com)


15.1 SWT 主题的概念

Dark Mode 的支持

// 检测系统是否是 Dark Mode
boolean isDarkMode = false;
if (Platform.getWS() == Platform.WS_WIN32) {
    // Windows 10+ 支持 Dark Mode
    // 需要使用 JNA 检测系统主题
    // 简化版本:假设用户选择
} else if (Platform.getWS() == Platform.WS_COCOA) {
    // Mac 自动支持 Dark Mode
    isDarkMode = true;
} else if (Platform.getWS() == Platform.WS_GTK) {
    // Linux 需要检测 GTK 主题
    isDarkMode = true;
}

if (isDarkMode) {
    // 使用 Dark Mode 颜色
    shell.setBackground(display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
} else {
    // 使用 Light Mode 颜色
    shell.setBackground(display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
}

费曼解释:Dark Mode 的含义

Dark Mode = 夜间模式
- 白天(Light Mode):亮色背景,黑色文字
- 晚上(Dark Mode):深色背景,浅色文字
- 保护眼睛,省电

类比:
Dark Mode = 日光灯
- 白天(Light Mode):开大灯(亮色背景)
- 晚上(Dark Mode):开小灯(深色背景)
- 保护眼睛,省电

自定义颜色方案

// 自定义颜色方案
class ColorScheme {
    public Color background;
    public Color foreground;
    public Color selectionBackground;
    public Color selectionForeground;
    
    public ColorScheme(Display display, String name) {
        if (name.equals("light")) {
            background = display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
            foreground = display.getSystemColor(SWT.COLOR_WIDGET_FOREGROUND);
            selectionBackground = display.getSystemColor(SWT.COLOR_LIST_SELECTION);
            selectionForeground = display.getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT);
        } else if (name.equals("dark")) {
            background = new Color(display, 30, 30, 30);
            foreground = new Color(display, 220, 220, 220);
            selectionBackground = new Color(display, 60, 120, 180);
            selectionForeground = new Color(display, 255, 255, 255);
        } else if (name.equals("blue")) {
            background = new Color(display, 240, 248, 255);
            foreground = new Color(display, 0, 0, 0);
            selectionBackground = new Color(display, 0, 120, 215);
            selectionForeground = new Color(display, 255, 255, 255);
        }
    }
    
    public void dispose() {
        if (!background.isDisposed()) background.dispose();
        if (!foreground.isDisposed()) foreground.dispose();
        if (!selectionBackground.isDisposed()) selectionBackground.dispose();
        if (!selectionForeground.isDisposed()) selectionForeground.dispose();
    }
}

// 使用颜色方案
ColorScheme lightScheme = new ColorScheme(display, "light");
shell.setBackground(lightScheme.background);
shell.setForeground(lightScheme.foreground);

费曼解释:自定义颜色方案的作用

自定义颜色方案 = 装修风格
- 北欧风(Light Mode):亮色,简洁
- 现代风(Dark Mode):深色,时尚
- 海洋风(Blue):蓝色,清新

类比:
自定义颜色方案 = 房间装修
- 北欧风:白色墙,浅色家具
- 现代风:黑色墙,深色家具
- 海洋风:蓝色墙,海浪家具

代码示例:主题切换

public class ThemeSwitcherExample {
    private static ColorScheme currentScheme;
    
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("主题切换");
        shell.setLayout(new GridLayout(1, false));
        
        // 初始主题:Light
        currentScheme = new ColorScheme(display, "light");
        applyTheme(shell, currentScheme);
        
        // 主题选择
        Composite themePanel = new Composite(shell, SWT.NONE);
        themePanel.setLayout(new FillLayout(SWT.HORIZONTAL));
        
        Button lightButton = new Button(themePanel, SWT.PUSH);
        lightButton.setText("Light");
        lightButton.addListener(SWT.Selection, event -> {
            if (currentScheme != null) currentScheme.dispose();
            currentScheme = new ColorScheme(display, "light");
            applyTheme(shell, currentScheme);
        });
        
        Button darkButton = new Button(themePanel, SWT.PUSH);
        darkButton.setText("Dark");
        darkButton.addListener(SWT.Selection, event -> {
            if (currentScheme != null) currentScheme.dispose();
            currentScheme = new ColorScheme(display, "dark");
            applyTheme(shell, currentScheme);
        });
        
        Button blueButton = new Button(themePanel, SWT.PUSH);
        blueButton.setText("Blue");
        blueButton.addListener(SWT.Selection, event -> {
            if (currentScheme != null) currentScheme.dispose();
            currentScheme = new ColorScheme(display, "blue");
            applyTheme(shell, currentScheme);
        });
        
        // 文本框
        Text text = new Text(shell, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
        GridData textData = new GridData();
        textData.horizontalAlignment = GridData.FILL;
        textData.verticalAlignment = GridData.FILL;
        textData.grabExcessHorizontalSpace = true;
        textData.grabExcessVerticalSpace = true;
        textData.heightHint = 150;
        text.setLayoutData(textData);
        text.setText("这是一个主题切换示例。\n\n点击上方的按钮切换不同的主题:\n- Light:浅色主题\n- Dark:深色主题\n- Blue:蓝色主题");
        
        shell.setBounds(100, 100, 500, 300);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        if (currentScheme != null) currentScheme.dispose();
        display.dispose();
    }
    
    private static void applyTheme(Shell shell, ColorScheme scheme) {
        shell.setBackground(scheme.background);
        shell.setForeground(scheme.foreground);
        
        // 应用到所有子控件
        applyTheme(shell, scheme);
    }
    
    private static void applyTheme(Composite composite, ColorScheme scheme) {
        Control[] children = composite.getChildren();
        for (Control child : children) {
            child.setBackground(scheme.background);
            child.setForeground(scheme.foreground);
            
            if (child instanceof Composite) {
                applyTheme((Composite) child, scheme);
            }
        }
    }
    
    static class ColorScheme {
        public Color background;
        public Color foreground;
        
        public ColorScheme(Display display, String name) {
            if (name.equals("light")) {
                background = display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
                foreground = display.getSystemColor(SWT.COLOR_WIDGET_FOREGROUND);
            } else if (name.equals("dark")) {
                background = new Color(display, 30, 30, 30);
                foreground = new Color(display, 220, 220, 220);
            } else if (name.equals("blue")) {
                background = new Color(display, 240, 248, 255);
                foreground = new Color(display, 0, 0, 0);
            }
        }
        
        public void dispose() {
            if (background != null && !background.isDisposed()) {
                background.dispose();
            }
            if (foreground != null && !foreground.isDisposed()) {
                foreground.dispose();
            }
        }
    }
}

15.2 CSS 在 SWT 中的应用

使用 CSS 定义样式

注意:SWT 本身不直接支持 CSS,但可以使用第三方库或自己实现

// 简化的 CSS 解析器
class CSSParser {
    public static Map<String, String> parse(String css) {
        Map<String, String> styles = new HashMap<>();
        
        String[] rules = css.split(";");
        for (String rule : rules) {
            String[] parts = rule.split(":");
            if (parts.length == 2) {
                String property = parts[0].trim();
                String value = parts[1].trim();
                styles.put(property, value);
            }
        }
        
        return styles;
    }
    
    public static Color parseColor(Display display, String color) {
        if (color.startsWith("#")) {
            int r = Integer.parseInt(color.substring(1, 3), 16);
            int g = Integer.parseInt(color.substring(3, 5), 16);
            int b = Integer.parseInt(color.substring(5, 7), 16);
            return new Color(display, r, g, b);
        } else if (color.equals("red")) {
            return display.getSystemColor(SWT.COLOR_RED);
        } else if (color.equals("blue")) {
            return display.getSystemColor(SWT.COLOR_BLUE);
        } else if (color.equals("green")) {
            return display.getSystemColor(SWT.COLOR_GREEN);
        } else if (color.equals("white")) {
            return display.getSystemColor(SWT.COLOR_WHITE);
        } else if (color.equals("black")) {
            return display.getSystemColor(SWT.COLOR_BLACK);
        }
        return null;
    }
}

// 使用 CSS 样式
String css = "background-color: #f0f0f0; color: #333333; font-size: 12px;";
Map<String, String> styles = CSSParser.parse(css);

Color backgroundColor = CSSParser.parseColor(display, styles.get("background-color"));
Color foregroundColor = CSSParser.parseColor(display, styles.get("color"));

费曼解释:CSS 解析的原理

CSS 解析 = 翻译说明书
- CSS 说明书:background-color: #f0f0f0;
- 翻译器(CSSParser):解析 CSS
- 转换成 Java 对象:Color(240, 240, 240)

类比:
CSS 解析 = 翻译菜谱
- 菜谱:盐:5克
- 翻译器(CSSParser):解析菜谱
- 转换成 Java 对象:Salt(5)

动态切换主题

// 定义 CSS 主题
Map<String, String> lightTheme = new HashMap<>();
lightTheme.put("background-color", "#ffffff");
lightTheme.put("color", "#000000");
lightTheme.put("font-size", "12px");

Map<String, String> darkTheme = new HashMap<>();
darkTheme.put("background-color", "#1e1e1e");
darkTheme.put("color", "#ffffff");
darkTheme.put("font-size", "12px");

Map<String, String> currentTheme = lightTheme;

// 切换主题
Button darkModeButton = new Button(shell, SWT.PUSH);
darkModeButton.setText("Dark Mode");
darkModeButton.addListener(SWT.Selection, event -> {
    currentTheme = darkTheme;
    applyTheme(shell, currentTheme);
});

费曼解释:动态切换主题的原理

动态切换主题 = 换装修
- 原装修(Light Theme):白墙,黑字
- 新装修(Dark Theme):黑墙,白字
- 换装修 = 切换主题

类比:
动态切换主题 = 换衣服
- 原衣服(Light Theme):白 T 恤
- 新衣服(Dark Theme):黑 T 恤
- 换衣服 = 切换主题

代码示例:CSS 样式(简化版)

public class CSSThemeExample {
    private static Map<String, String> currentTheme;
    
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("CSS 样式示例");
        shell.setLayout(new GridLayout(1, false));
        
        // 初始主题:Light
        currentTheme = createLightTheme();
        applyTheme(shell, currentTheme);
        
        // 主题按钮
        Composite themePanel = new Composite(shell, SWT.NONE);
        themePanel.setLayout(new FillLayout(SWT.HORIZONTAL));
        
        Button lightButton = new Button(themePanel, SWT.PUSH);
        lightButton.setText("Light");
        lightButton.addListener(SWT.Selection, event -> {
            currentTheme = createLightTheme();
            applyTheme(shell, currentTheme);
        });
        
        Button darkButton = new Button(themePanel, SWT.PUSH);
        darkButton.setText("Dark");
        darkButton.addListener(SWT.Selection, event -> {
            currentTheme = createDarkTheme();
            applyTheme(shell, currentTheme);
        });
        
        Button blueButton = new Button(themePanel, SWT.PUSH);
        blueButton.setText("Blue");
        blueButton.addListener(SWT.Selection, event -> {
            currentTheme = createBlueTheme();
            applyTheme(shell, currentTheme);
        });
        
        // 文本框
        Text text = new Text(shell, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
        GridData textData = new GridData();
        textData.horizontalAlignment = GridData.FILL;
        textData.verticalAlignment = GridData.FILL;
        textData.grabExcessHorizontalSpace = true;
        textData.grabExcessVerticalSpace = true;
        textData.heightHint = 150;
        text.setLayoutData(textData);
        text.setText("这是一个 CSS 样式示例。\n\n点击上方的按钮切换不同的主题:\n- Light:浅色主题\n- Dark:深色主题\n- Blue:蓝色主题\n\n注意:这是简化版的 CSS 解析器,实际应用中可以使用第三方库。");
        
        shell.setBounds(100, 100, 500, 300);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
    
    private static Map<String, String> createLightTheme() {
        Map<String, String> theme = new HashMap<>();
        theme.put("background-color", "#ffffff");
        theme.put("color", "#000000");
        theme.put("font-size", "12");
        return theme;
    }
    
    private static Map<String, String> createDarkTheme() {
        Map<String, String> theme = new HashMap<>();
        theme.put("background-color", "#1e1e1e");
        theme.put("color", "#ffffff");
        theme.put("font-size", "12");
        return theme;
    }
    
    private static Map<String, String> createBlueTheme() {
        Map<String, String> theme = new HashMap<>();
        theme.put("background-color", "#e3f2fd");
        theme.put("color", "#000000");
        theme.put("font-size", "12");
        return theme;
    }
    
    private static void applyTheme(Shell shell, Map<String, String> theme) {
        Display display = shell.getDisplay();
        
        Color backgroundColor = parseColor(display, theme.get("background-color"));
        Color foregroundColor = parseColor(display, theme.get("color"));
        int fontSize = Integer.parseInt(theme.get("font-size"));
        
        shell.setBackground(backgroundColor);
        shell.setForeground(foregroundColor);
        shell.setFont(new Font(display, "Arial", fontSize, SWT.NORMAL));
        
        // 应用到所有子控件
        applyTheme(shell, theme);
    }
    
    private static void applyTheme(Composite composite, Map<String, String> theme) {
        Display display = composite.getDisplay();
        
        Color backgroundColor = parseColor(display, theme.get("background-color"));
        Color foregroundColor = parseColor(display, theme.get("color"));
        int fontSize = Integer.parseInt(theme.get("font-size"));
        
        Control[] children = composite.getChildren();
        for (Control child : children) {
            child.setBackground(backgroundColor);
            child.setForeground(foregroundColor);
            child.setFont(new Font(display, "Arial", fontSize, SWT.NORMAL));
            
            if (child instanceof Composite) {
                applyTheme((Composite) child, theme);
            }
        }
    }
    
    private static Color parseColor(Display display, String color) {
        if (color == null) return null;
        
        if (color.startsWith("#")) {
            int r = Integer.parseInt(color.substring(1, 3), 16);
            int g = Integer.parseInt(color.substring(3, 5), 16);
            int b = Integer.parseInt(color.substring(5, 7), 16);
            return new Color(display, r, g, b);
        } else if (color.equals("red")) {
            return display.getSystemColor(SWT.COLOR_RED);
        } else if (color.equals("blue")) {
            return display.getSystemColor(SWT.COLOR_BLUE);
        } else if (color.equals("green")) {
            return display.getSystemColor(SWT.COLOR_GREEN);
        } else if (color.equals("white")) {
            return display.getSystemColor(SWT.COLOR_WHITE);
        } else if (color.equals("black")) {
            return display.getSystemColor(SWT.COLOR_BLACK);
        }
        return null;
    }
}

15.3 图标主题

SVG 图标的支持

注意:SWT 本身不直接支持 SVG,但可以使用第三方库如 Apache Batik

// 使用 Batik 库加载 SVG
import org.apache.batik.dom.svg.SVGDOMRootElement;
import org.apache.batik.svggen.SVGGraphics2D;
import org.w3c.dom.svg.SVGDocument;

public class SVGIconLoader {
    public static Image loadSVGImage(Display display, String svgPath) {
        try {
            // 加载 SVG 文档
            String parser = XMLResourceDescriptor.getXMLParserClassName();
            SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);
            SVGDocument doc = f.createSVGDocument(new File(svgPath).toURI());
            
            // 渲染 SVG 到 Image
            SVGGraphics2D g2d = new SVGGraphics2D(true);
            g2d.setSVGCanvasSize(doc.getRootElement().getBBox());
            
            // 创建 Image
            Image image = new Image(display, g2d.getSVGCanvasSize().width, 
                    g2d.getSVGCanvasSize().height);
            
            GC gc = new GC(image);
            g2d.setGC(gc);
            g2d.renderTree(doc);
            g2d.dispose();
            gc.dispose();
            
            return image;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

// 使用 SVG 图标
Image svgIcon = SVGIconLoader.loadSVGImage(display, "icon.svg");
shell.setImage(svgIcon);

费曼解释:SVG 图标的原理

SVG 图标 = 矢量图形
- SVG 用数学公式描述图形(矢量)
- 放大不失真(无限分辨率)
- PNG 用像素描述图形(位图)
- 放大会失真(固定分辨率)

类比:
SVG 图标 = 数学函数
- y = x^2(函数)
- 无论放大多少倍,曲线都平滑

PNG 图标 = 像素网格
- 100x100 的网格(像素)
- 放大到 200x200,网格变模糊

多尺寸适配

// 加载不同尺寸的图标
Image icon16 = new Image(display, "icon16.png");
Image icon32 = new Image(display, "icon32.png");
Image icon48 = new Image(display, "icon48.png");
Image icon64 = new Image(display, "icon64.png");
Image icon128 = new Image(display, "icon128.png");

// 根据屏幕 DPI 选择图标
int dpi = getDPI();
if (dpi <= 96) {
    shell.setImage(icon16);
} else if (dpi <= 120) {
    shell.setImage(icon32);
} else if (dpi <= 144) {
    shell.setImage(icon48);
} else if (dpi <= 192) {
    shell.setImage(icon64);
} else {
    shell.setImage(icon128);
}

// 释放图标
icon16.dispose();
icon32.dispose();
icon48.dispose();
icon64.dispose();
icon128.dispose();

费曼解释:多尺寸适配的原理

多尺寸适配 = 尺码齐全的服装
- 1.6 米:S 号
- 1.7 米:M 号
- 1.8 米:L 号
- 1.9 米:XL 号
- 根据身高选择尺码

类比:
多尺寸适配 = 鞋码齐全的鞋店
- 38 码:适合 24 厘米脚
- 40 码:适合 25 厘米脚
- 42 码:适合 26 厘米脚
- 根据脚长选择鞋码

代码示例:图标主题

public class IconThemeExample {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("图标主题");
        shell.setLayout(new GridLayout(1, false));
        
        // 图标主题选择
        Composite iconPanel = new Composite(shell, SWT.NONE);
        iconPanel.setLayout(new FillLayout(SWT.HORIZONTAL));
        
        Button colorButton = new Button(iconPanel, SWT.PUSH);
        colorButton.setText("彩色");
        colorButton.addListener(SWT.Selection, event -> {
            // 注意:实际应用中应该从文件加载图标
            // shell.setImage(colorIcon);
            System.out.println("切换到彩色图标");
        });
        
        Button grayButton = new Button(iconPanel, SWT.PUSH);
        grayButton.setText("灰色");
        grayButton.addListener(SWT.Selection, event -> {
            // 注意:实际应用中应该从文件加载图标
            // shell.setImage(grayIcon);
            System.out.println("切换到灰色图标");
        });
        
        Button highDPIButton = new Button(iconPanel, SWT.PUSH);
        highDPIButton.setText("High DPI");
        highDPIButton.addListener(SWT.Selection, event -> {
            // 注意:实际应用中应该根据 DPI 选择不同尺寸的图标
            // int dpi = getDPI();
            // if (dpi >= 144) {
            //     shell.setImage(icon48);
            // } else {
            //     shell.setImage(icon16);
            // }
            System.out.println("切换到 High DPI 图标");
        });
        
        // 文本框:显示信息
        Text text = new Text(shell, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
        GridData textData = new GridData();
        textData.horizontalAlignment = GridData.FILL;
        textData.verticalAlignment = GridData.FILL;
        textData.grabExcessHorizontalSpace = true;
        textData.grabExcessVerticalSpace = true;
        textData.heightHint = 150;
        text.setLayoutData(textData);
        text.setText("这是一个图标主题示例。\n\n点击上方的按钮切换不同的图标主题:\n- 彩色:彩色图标\n- 灰色:灰色图标\n- High DPI:根据 DPI 选择不同尺寸的图标\n\n注意:这是简化版,实际应用中应该从文件加载不同尺寸的图标。");
        
        shell.setBounds(100, 100, 500, 300);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
    
    // 简化的 DPI 检测
    private static int getDPI() {
        // 注意:实际应用中应该使用 JNA 检测系统 DPI
        return 96;  // 默认 96 DPI
    }
}

15.4 本章小结

主题与样式总结

概念作用类比
Dark Mode深色主题夜间模式
ColorScheme颜色方案装修风格
CSS样式表说明书
SVG 图标矢量图形数学函数
多尺寸适配DPI 适配鞋码齐全

费曼测试:你能解释清楚吗?

  1. Dark Mode 的作用?

- 深色背景,浅色文字,保护眼睛

  1. 自定义颜色方案的原理?

- 定义背景色、前景色、选中色等,形成整体风格

  1. CSS 解析的作用?

- 将 CSS 样式转换成 Java 对象

  1. SVG 图标的优势?

- 矢量图形,放大不失真

下一章预告

现在你已经掌握了主题与样式,
可以支持 Dark Mode,
提升用户体验。

下一章,我们将学习 SWT 应用的打包与部署,
让应用可以分发到用户手中,
真正投入使用。

练习题:

  1. 创建一个应用,支持 Light、Dark、Blue 三种主题切换。
  2. 创建一个简化的 CSS 解析器,解析 background-color、color 属性。
  3. 创建一个应用,支持根据 DPI 选择不同尺寸的图标(模拟)。

(提示:使用 ColorScheme 类管理颜色,使用 Map 存储主题)

← 返回目录