第2章:环境搭建

第2章:环境搭建


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


2.1 最简单的 Hello SWT

Maven 依赖配置

第一步:创建 Maven 项目

mkdir hello-swt
cd hello-swt
mvn archetype:generate -DgroupId=com.example -DartifactId=hello-swt -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
cd hello-swt

第二步:修改 pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>hello-swt</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- SWT 核心库 -->
        <dependency>
            <groupId>org.eclipse.platform</groupId>
            <artifactId>org.eclipse.swt</artifactId>
            <version>3.124.200</version>
        </dependency>
    </dependencies>

    <!-- 根据你的操作系统选择对应的原生库 -->
    <profiles>
        <!-- Windows -->
        <profile>
            <id>windows</id>
            <activation>
                <os>
                    <family>windows</family>
                </os>
            </activation>
            <dependencies>
                <dependency>
                    <groupId>org.eclipse.platform</groupId>
                    <artifactId>org.eclipse.swt.win32.win32.x86_64</artifactId>
                    <version>3.124.200</version>
                </dependency>
            </dependencies>
        </profile>

        <!-- Mac -->
        <profile>
            <id>mac</id>
            <activation>
                <os>
                    <family>mac</family>
                </os>
            </activation>
            <dependencies>
                <dependency>
                    <groupId>org.eclipse.platform</groupId>
                    <artifactId>org.eclipse.swt.cocoa.macosx.x86_64</artifactId>
                    <version>3.124.200</version>
                </dependency>
            </dependencies>
        </profile>

        <!-- Linux -->
        <profile>
            <id>linux</id>
            <activation>
                <os>
                    <family>linux</family>
                </os>
            </activation>
            <dependencies>
                <dependency>
                    <groupId>org.eclipse.platform</groupId>
                    <artifactId>org.eclipse.swt.gtk.linux.x86_64</artifactId>
                    <version>3.124.200</version>
                </dependency>
            </dependencies>
        </profile>
    </profiles>
</project>

费曼解释:为什么需要平台特定的依赖?

SWT 就像一套语言包:
- org.eclipse.swt 是"语法书"(通用 API)
- org.eclipse.swt.win32.win32.x86_64 是"Windows 词典"
- org.eclipse.swt.cocoa.macosx.x86_64 是"Mac 词典"
- org.eclipse.swt.gtk.linux.x86_64 是"Linux 词典"

你有了语法书,还需要对应平台的词典,
才能说出当地语言(调用本地 API)。

下载依赖:

mvn clean install

Gradle 依赖配置

第一步:创建 Gradle 项目

mkdir hello-swt
cd hello-swt
gradle init --type java-application

第二步:修改 build.gradle

plugins {
    id 'java'
    id 'application'
}

java {
    sourceCompatibility = 11
    targetCompatibility = 11
}

repositories {
    mavenCentral()
}

dependencies {
    // SWT 核心库
    implementation 'org.eclipse.platform:org.eclipse.swt:3.124.200'

    // 根据操作系统选择原生库
    if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
        implementation 'org.eclipse.platform:org.eclipse.swt.win32.win32.x86_64:3.124.200'
    } else if (org.gradle.internal.os.OperatingSystem.current().isMacOsX()) {
        implementation 'org.eclipse.platform:org.eclipse.swt.cocoa.macosx.x86_64:3.124.200'
    } else {
        implementation 'org.eclipse.platform:org.eclipse.swt.gtk.linux.x86_64:3.124.200'
    }
}

application {
    mainClass = 'com.example.App'
}

下载依赖:

gradle build

手动添加 jar 包

适用于:不想用 Maven/Gradle,或者网络受限的情况

步骤:

  1. 下载 SWT jar 包

- 访问:https://download.eclipse.org/eclipse/downloads/ - 选择最新版本 - 下载 SWT Binary - 解压后得到: `` swt.jar // 核心库 swt-win32.dll // Windows 原生库 swt-cocoa.dylib // Mac 原生库 libswt-gtk-3.so // Linux 原生库 ``

  1. 创建项目结构

`` hello-swt/ ├── src/ │ └── main/ │ └── java/ │ └── com/ │ └── example/ │ └── App.java └── lib/ ├── swt.jar ├── swt-win32.dll ├── swt-cocoa.dylib └── libswt-gtk-3.so ``

  1. 设置 CLASSPATH

```bash # Windows set CLASSPATH=lib\swt.jar;.

# Mac/Linux export CLASSPATH=lib/swt.jar:. ```

  1. 设置 java.library.path

```bash # Windows java -Djava.library.path=lib App

# Mac/Linux java -Djava.library.path=lib App ```

费曼解释:什么是 java.library.path?

java.library.path 就像"搜索路径":
- JVM 运行 Java 代码(.class 文件)
- SWT 需要加载原生库(.dll, .so, .dylib)
- JVM 在 java.library.path 指定的目录中查找原生库

类比:
- CLASSPATH:告诉 JVM 在哪里找 .class 文件
- java.library.path:告诉 JVM 在哪里找 .dll 文件

2.2 第一个 SWT 程序

Hello World 程序

package com.example;

import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;

public class HelloWorld {
    public static void main(String[] args) {
        // 1. 创建 Display(连接操作系统的桥梁)
        Display display = new Display();

        // 2. 创建 Shell(窗口)
        Shell shell = new Shell(display);
        shell.setText("Hello SWT");  // 设置窗口标题
        shell.setSize(300, 200);     // 设置窗口大小

        // 3. 创建 Label(标签)
        Label label = new Label(shell, SWT.NONE);
        label.setText("Hello, SWT!");
        label.setBounds(100, 80, 100, 30);  // 设置位置和大小

        // 4. 打开窗口
        shell.open();

        // 5. 事件循环(让程序持续运行)
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }

        // 6. 释放资源
        display.dispose();
    }
}

运行程序:

# Maven
mvn exec:java -Dexec.mainClass="com.example.HelloWorld"

# Gradle
gradle run

# 手动编译
javac -cp lib/swt.jar src/main/java/com/example/HelloWorld.java
java -cp lib/swt.jar;. -Djava.library.path=lib com.example.HelloWorld

代码详细解释

1. 创建 Display

Display display = new Display();

费曼解释:Display 是什么?

Display 就像"电话机":
- 你的 Java 代码在一个房间(Java 虚拟机)
- 操作系统在另一个房间(操作系统)
- Display 是连接两个房间的电话线

没有 Display:
- 你的代码无法联系操作系统
- 就像没有电话,无法联系外界

有了 Display:
- 你可以"拨号"(调用 Display 的方法)
- 操作系统会"接听"(处理你的请求)
- 例如:创建窗口、显示控件、处理事件

类比:

Display 的作用 = 浏览器的网络连接
- 没有 Display = 离线模式,什么都做不了
- 有了 Display = 在线模式,可以访问操作系统资源

Display 的生命周期:

创建 Display → 连接操作系统
使用 Display → 创建窗口、控件
销毁 Display → 断开连接

2. 创建 Shell

Shell shell = new Shell(display);
shell.setText("Hello SWT");  // 设置窗口标题
shell.setSize(300, 200);     // 设置窗口大小(宽x高)

费曼解释:Shell 是什么?

Shell 就像"相框":
- 你可以在相框里放照片(控件)
- 相框本身可以摆放在不同位置
- 相框可以关闭、最小化、最大化

类比:
- Display 是桌子
- Shell 是桌子上的相框
- Label 是相框里的照片

没有 Shell:
- 控件(照片)无处摆放
- Display(桌子)是空的

有了 Shell:
- 控件(照片)有家了
- 用户看到的是一个窗口

Shell 的属性:

  • setText(): 设置窗口标题
  • setSize(): 设置窗口大小(宽 × 高)
  • setLocation(): 设置窗口位置(x, y)
  • setBounds(): 同时设置位置和大小

3. 创建 Label

Label label = new Label(shell, SWT.NONE);
label.setText("Hello, SWT!");
label.setBounds(100, 80, 100, 30);

费曼解释:Label 是什么?

Label 就像"便利贴":
- 你可以写字在上面
- 粘在相框(Shell)的某个位置
- 用户可以看到,但不能修改

类比:
- Label = 便利贴(只读文本)
- Button = 开关(可以点击)
- Text = 笔记本(可以写)

Label 的构造函数:

Label(Composite parent, int style)
  • parent: 父容器(这里是 Shell)
  • style: 样式(SWT.NONE 表示默认样式)

为什么需要指定父容器?

UI 是一个树形结构:
Display
  └── Shell(窗口)
      └── Label(标签)
          └── Button(按钮)
              └── ...

每个控件都需要一个"家"(父容器):
- Label 的家是 Shell
- Button 的家是 Label(但通常 Button 的家也是 Shell)
- 控件的"家"决定了它显示在哪里

4. 打开窗口

shell.open();

费曼解释:open() 做了什么?

open() 就像"掀开红布":
- 创建 Shell 时,窗口还在"后台"(看不见)
- 调用 open() 后,窗口"前台"显示(可见)

类比:
- 在商店里,衣服穿在模特身上(shell 创建)
- 但模特被布盖着(不可见)
- 掀开布(shell.open()),顾客才能看到衣服

5. 事件循环

while (!shell.isDisposed()) {
    if (!display.readAndDispatch()) {
        display.sleep();
    }
}

费曼解释:事件循环是什么?

事件循环就像"门卫":
- 站在门口(Display)守着
- 有人来了(事件发生),门卫去处理
- 没人来了,门卫休息一下

事件的例子:
- 用户点击按钮
- 用户关闭窗口
- 用户移动鼠标
- 用户输入文字

readAndDispatch() 的作用:
- 检查有没有事件
- 有事件:处理事件(dispatch)
- 没事件:返回 false

sleep() 的作用:
- 没事件时,让 CPU 休息一下
- 避免空转浪费资源

类比:

事件循环 = 餐厅服务员
while (餐厅没关门) {
    if (有顾客点单) {
        去点单(readAndDispatch)
    } else {
        在旁边休息(sleep)
    }
}

为什么需要事件循环?

没有事件循环:
- 程序创建窗口后就退出了
- 窗口闪一下就消失了

有了事件循环:
- 程序持续运行
- 窗口一直显示
- 可以响应用户操作

<code>shell.isDisposed()</code> 的含义:

Disposed = 已销毁
- 用户点击窗口的关闭按钮
- 窗口被标记为"已销毁"
- isDisposed() 返回 true
- 事件循环退出
- 程序结束

6. 释放资源

display.dispose();

费曼解释:dispose() 是什么?

dispose() 就像"打扫卫生":
- 关门前,把所有东西清理干净
- 释放占用的内存
- 释放占用的系统资源

类比:
- 你开了一间店(Display 创建)
- 营业了一天(事件循环)
- 关门前要打扫卫生(dispose)
- 否则垃圾会越堆越多

为什么必须释放资源?

SWT 资源的特殊性:
- SWT 控件映射到操作系统的原生对象
- 这些对象由操作系统管理
- Java 的 GC(垃圾回收)不会自动回收它们

如果不 dispose:
- 操作系统资源泄漏
- 内存占用增加
- 最终系统变慢或崩溃

类比:

Java 对象 = 你在笔记本上写的字
- GC 像橡皮擦,擦掉没用的字

SWT 资源 = 你借了图书馆的书
- GC 不会帮你还书
- 你必须自己还(dispose)
- 否则书越来越多,图书馆崩溃

完整的注释版本

package com.example;

import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;

public class HelloWorld {
    public static void main(String[] args) {
        // ===== 第一阶段:准备环境 =====
        
        // 创建 Display(连接操作系统的桥梁)
        // 每个 SWT 程序必须有且只有一个 Display
        Display display = new Display();

        // 创建 Shell(窗口的容器)
        // Shell 需要 Display 作为参数,因为窗口需要与操作系统通信
        Shell shell = new Shell(display);
        shell.setText("Hello SWT");      // 设置窗口标题(显示在标题栏)
        shell.setSize(300, 200);         // 设置窗口大小(宽度 × 高度)

        // ===== 第二阶段:添加控件 =====
        
        // 创建 Label(标签)
        // 第一个参数:父容器(这里把 Label 放在 Shell 里面)
        // 第二个参数:样式(SWT.NONE 表示默认样式)
        Label label = new Label(shell, SWT.NONE);
        label.setText("Hello, SWT!");   // 设置标签文本
        label.setBounds(100, 80, 100, 30); // 设置位置和大小(x, y, 宽度, 高度)

        // ===== 第三阶段:显示窗口 =====
        
        // 打开窗口(从不可见变为可见)
        shell.open();

        // ===== 第四阶段:事件循环 =====
        
        // 事件循环:持续运行,处理事件
        while (!shell.isDisposed()) {    // 只要窗口没有被关闭
            if (!display.readAndDispatch()) {  // 如果没有事件需要处理
                display.sleep();                // 就让 CPU 休息一下
            }
        }

        // ===== 第五阶段:清理资源 =====
        
        // 释放 Display 及其管理的所有资源
        // 这会自动释放所有控件(Label、Shell 等)
        display.dispose();
    }
}

2.3 常见问题解决

问题 1:找不到原生库(.dll, .so, .jnilib)

错误信息:

java.lang.UnsatisfiedLinkError: Could not load SWT library. Reasons:
no swt-win32-**** in java.library.path
no swt-win32 in java.library.path
Can't load library: ...\swt-win32-****.dll

原因分析:

SWT 原生库就像"翻译词典":
- Java 代码需要"翻译"成操作系统能理解的指令
- 原生库就是"翻译词典"
- 找不到词典 = 无法翻译 = 程序崩溃

解决方案(按优先级):

方案 1:确保依赖正确(Maven/Gradle)

<!-- 检查 pom.xml 是否包含了对应平台的依赖 -->
<dependency>
    <groupId>org.eclipse.platform</groupId>
    <artifactId>org.eclipse.swt.win32.win32.x86_64</artifactId>
    <version>3.124.200</version>
</dependency>

方案 2:手动设置 java.library.path

# 编译
javac -cp lib/swt.jar src/main/java/com/example/HelloWorld.java

# 运行(指定原生库位置)
java -cp lib/swt.jar;. -Djava.library.path=lib com.example.HelloWorld

方案 3:提取原生库到运行目录

// 在代码中提取原生库(适合打包应用)
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class NativeLibraryExtractor {
    public static void extractNativeLibrary(String resourceName, String targetPath) throws Exception {
        try (InputStream in = NativeLibraryExtractor.class.getResourceAsStream(resourceName);
             FileOutputStream out = new FileOutputStream(targetPath)) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
        }
    }
}

// 使用示例
public static void main(String[] args) throws Exception {
    // 提取原生库
    String osName = System.getProperty("os.name").toLowerCase();
    String nativeLib;
    
    if (osName.contains("win")) {
        nativeLib = "/lib/swt-win32.dll";
    } else if (osName.contains("mac")) {
        nativeLib = "/lib/swt-cocoa.dylib";
    } else {
        nativeLib = "/lib/libswt-gtk-3.so";
    }
    
    String targetDir = System.getProperty("java.io.tmpdir");
    String targetPath = targetDir + nativeLib.substring(nativeLib.lastIndexOf('/'));
    
    // 确保目录存在
    Files.createDirectories(Path.of(targetDir));
    
    // 提取
    extractNativeLibrary(nativeLib, targetPath);
    
    // 设置库路径
    System.setProperty("java.library.path", targetDir);
    
    // 继续运行 SWT 程序...
}

问题 2:平台相关的坑

Windows 平台

问题:高分屏(HiDPI)模糊

原因:
- Windows 的 DPI 缩放导致 SWT 控件模糊

解决方案:
// 在 main 方法最开始设置
System.setProperty("swt.autoScale", "true");

问题:菜单栏显示在窗口顶部(Mac 风格)

Windows 默认菜单栏在窗口顶部,这是正常的。
如果想让菜单栏在窗口顶部(而不是屏幕顶部),默认就是这样的。

Mac 平台

问题:程序没有 Dock 图标

原因:
- SWT 程序默认没有 Dock 图标
- 用户看不到程序在运行

解决方案:
// 设置应用名称(会在 Dock 显示)
System.setProperty("apple.laf.useScreenMenuBar", "true");
System.setProperty("com.apple.mrj.application.apple.menu.about.name", "我的应用");

问题:菜单栏显示在窗口顶部,但应该在屏幕顶部

原因:
- Mac 的菜单栏应该在屏幕顶部,不是窗口顶部

解决方案:
// 设置菜单栏在屏幕顶部
System.setProperty("apple.laf.useScreenMenuBar", "true");

问题:无法打开多个窗口

原因:
- Mac 应用默认是单例模式

解决方案:
// 在 Info.plist 中添加(打包应用时)
<key>LSMultipleInstancesProhibited</key>
<false/>

Linux 平台

问题:找不到 GTK 库

错误信息:
java.lang.UnsatisfiedLinkError: Could not load SWT library. ... libgtk-3.so: cannot open shared object file

原因:
- Linux 需要 GTK 库,但系统没有安装

解决方案(Ubuntu/Debian):
sudo apt-get install libgtk-3-dev

解决方案(Fedora/RHEL):
sudo dnf install gtk3-devel

问题:主题不统一

原因:
- Linux 有多个桌面环境(GNOME、KDE、XFCE)
- 每个桌面环境的主题不同

解决方案:
- SWT 会自动使用系统默认主题
- 如果需要统一主题,使用 CSS(需要额外库)

问题 3:调试技巧

启用 SWT 调试模式

// 在 main 方法最开始设置
System.setProperty("org.eclipse.swt.debug", "true");

// 启用特定调试选项
System.setProperty("org.eclipse.swt.debug.widgets", "true");      // 打印控件创建信息
System.setProperty("org.eclipse.swt.debug.events", "true");       // 打印事件信息
System.setProperty("org.eclipse.swt.debug.graphics", "true");     // 打印图形调试信息

使用 Display.getAppName()

Display display = new Display();
System.out.println("应用名称: " + display.getAppName());
System.out.println("应用版本: " + display.getAppVersion());
System.out.println("SWT 版本: " + SWT.getVersion());

检查控件树

public static void printControlTree(Control control, int indent) {
    String indentation = "  ".repeat(indent);
    System.out.println(indentation + control.getClass().getSimpleName());
    
    if (control instanceof Composite) {
        Composite composite = (Composite) control;
        Control[] children = composite.getChildren();
        for (Control child : children) {
            printControlTree(child, indent + 1);
        }
    }
}

// 使用示例
printControlTree(shell, 0);

监听所有事件

public class EventTracer {
    public static void addAllListeners(Control control) {
        // Selection 事件
        control.addListener(SWT.Selection, event -> {
            System.out.println("Selection: " + control);
        });
        
        // Modify 事件
        control.addListener(SWT.Modify, event -> {
            System.out.println("Modify: " + control);
        });
        
        // Focus 事件
        control.addListener(SWT.FocusIn, event -> {
            System.out.println("FocusIn: " + control);
        });
        
        control.addListener(SWT.FocusOut, event -> {
            System.out.println("FocusOut: " + control);
        });
        
        // Mouse 事件
        control.addListener(SWT.MouseDown, event -> {
            System.out.println("MouseDown: " + control);
        });
        
        control.addListener(SWT.MouseUp, event -> {
            System.out.println("MouseUp: " + control);
        });
        
        // 递归添加到子控件
        if (control instanceof Composite) {
            Composite composite = (Composite) control;
            for (Control child : composite.getChildren()) {
                addAllListeners(child);
            }
        }
    }
}

// 使用示例
addAllListeners(shell);

2.4 本章小结

关键步骤回顾

环境搭建 → 编写程序 → 运行调试
   ↓          ↓          ↓
Maven/Gradle  Display → 事件循环
原生库        Shell     资源释放
            Label

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

  1. Display 是什么?

- 连接操作系统的桥梁 - 像电话机,让 Java 代码联系操作系统

  1. Shell 是什么?

- 窗口的容器 - 像相框,承载控件

  1. 事件循环是什么?

- 持续处理事件 - 像门卫,等待并处理事件

  1. 为什么要 dispose?

- SWT 资源由操作系统管理 - Java GC 不会自动回收 - 必须手动释放,否则泄漏

下一章预告

现在你已经能写出最简单的 SWT 程序了,
但你还不知道:
- 如何让窗口更漂亮?(布局管理器)
- 如何让控件响应用户操作?(事件处理)
- 如何创建复杂的界面?(组合控件)

下一章,我们将深入学习 SWT 的五大组件,
从 Display 到布局管理器,
让你真正掌握 SWT 的基础能力。

练习题:

  1. 修改 Hello World 程序,添加一个按钮(Button),点击按钮后改变 Label 的文本。
  2. 尝试在 Mac 上运行程序,设置菜单栏在屏幕顶部。
  3. 使用调试技巧,打印所有控件树和事件。

(提示:Button 的样式是 SWT.PUSH,事件监听使用 addListener(SWT.Selection, event -> ...)

← 返回目录