第3章:Display 与 Shell - 舞台与幕布
作者:步子哥 (steper@foxmail.com)
3.1 Display:操作系统的对话者
为什么必须先创建 Display?
想象你是一个导演:
- 你要拍摄一场戏(GUI 程序)
- 你需要一个摄影棚(操作系统)
- Display 就是摄影棚的钥匙
没有钥匙:
- 你进不了摄影棚
- 无法搭建布景(创建控件)
- 无法开始拍摄(运行程序)
有了钥匙:
- 你可以自由使用摄影棚
- 搭建任意布景
- 拍摄任何场景
费曼解释:Display 的本质
Display 是"操作系统的代理":
- 代表操作系统与你的 Java 代码对话
- 所有 SWT 操作都通过 Display 进行
- Display 是整个 SWT 程序的入口和出口
类比:
Display = 酒店前台
- 你(Java 代码)住酒店(操作系统)
- 所有需求都要找前台(Display)
- 要房间(创建窗口)→ 找 Display
- 要服务(处理事件)→ 找 Display
- 退房(程序结束)→ 找 Display
多 Display 的陷阱
错误的代码(多 Display):
public class MultipleDisplayBadExample {
public static void main(String[] args) {
// 创建第一个 Display
Display display1 = new Display();
Shell shell1 = new Shell(display1);
shell1.setText("窗口 1");
shell1.open();
// 创建第二个 Display(错误!)
Display display2 = new Display();
Shell shell2 = new Shell(display2);
shell2.setText("窗口 2");
shell2.open();
// 现在有两个事件循环!
// 怎么办?
}
}
问题在哪里?
两个 Display = 两套独立的系统:
- display1 管理自己的控件和事件
- display2 管理自己的控件和事件
- 两个事件循环无法同时运行
类比:
- 你有两部电话(两个 Display)
- 一部电话只能接一个人的电话(一个事件循环)
- 你无法同时接两部电话
正确的做法(单 Display):
public class SingleDisplayGoodExample {
public static void main(String[] args) {
// 只创建一个 Display
Display display = new Display();
// 创建多个 Shell(窗口)
Shell shell1 = new Shell(display);
shell1.setText("窗口 1");
shell1.setBounds(100, 100, 300, 200);
shell1.open();
Shell shell2 = new Shell(display);
shell2.setText("窗口 2");
shell2.setBounds(450, 100, 300, 200);
shell2.open();
// 只需要一个事件循环
while (!shell1.isDisposed() && !shell2.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
费曼解释:为什么单 Display?
一个 Display = 一个与操作系统的连接:
- 就像你只有一部电话
- 可以给不同的人打电话(创建多个窗口)
- 但只能同时和一个对话(一个事件循环)
类比:
Display = 电话机
- 一部电话可以拨打多个号码(多个 Shell)
- 但你只能同时和一个通话(事件循环)
- 如果你有两部电话,你需要"分身"才能同时使用(不可能)
Display 的生命周期管理
创建 Display
Display display = new Display();
费曼解释:创建 Display 时发生了什么?
创建 Display = 连接操作系统:
1. 加载原生库(.dll, .so, .dylib)
2. 初始化操作系统资源
3. 分配内存和句柄
4. 准备接收事件
类比:
- 你拨打 10086(创建 Display)
- 话务员接通(初始化)
- 你可以开始咨询了(使用 Display)
使用 Display
// 获取屏幕信息
Monitor[] monitors = display.getMonitors();
for (Monitor monitor : monitors) {
Rectangle bounds = monitor.getBounds();
Rectangle clientArea = monitor.getClientArea();
System.out.println("屏幕大小: " + bounds);
System.out.println("可用区域: " + clientArea);
}
// 获取系统字体
FontData[] fontDatas = display.getFontList(null, true);
for (FontData fd : fontDatas) {
System.out.println("系统字体: " + fd.getName() + " " + fd.getHeight());
}
// 获取系统颜色
Color color = display.getSystemColor(SWT.COLOR_RED);
System.out.println("系统红色: " + color.getRGB());
常用方法总结:
| 方法 | 作用 | 类比 |
|---|---|---|
getMonitors() | 获取所有显示器 | 查看有多少个屏幕 |
getBounds() | 获取屏幕大小 | 查看屏幕有多大 |
getSystemColor() | 获取系统颜色 | 查看系统的调色板 |
getFontList() | 获取系统字体 | 查看系统有哪些字体 |
getCursor() | 获取系统光标 | 查看系统的鼠标样式 |
销毁 Display
display.dispose();
费曼解释:销毁 Display 时发生了什么?
销毁 Display = 断开与操作系统的连接:
1. 释放所有控件资源
2. 释放操作系统句柄
3. 关闭原生库连接
4. 释放内存
类比:
- 你挂断电话(dispose Display)
- 通话记录清除(资源释放)
- 话务员关闭连接(断开系统)
重要:
- dispose() 会自动释放所有控件
- 你不需要手动 dispose 每个 Shell 和控件
- 但 Color、Font、Image 等需要手动 dispose(第 11 章详解)
3.2 Shell:窗口的容器
主窗口 vs 弹出窗口
// 主窗口
Shell mainShell = new Shell(display);
mainShell.setText("主窗口");
mainShell.open();
// 弹出窗口(指定父 Shell)
Shell popupShell = new Shell(mainShell, SWT.DIALOG_TRIM);
popupShell.setText("弹出窗口");
popupShell.open();
费曼解释:两种窗口的区别
主窗口:
- 应用的"门面"
- 用户启动应用看到的第一个窗口
- 可以独立存在
弹出窗口:
- 从主窗口"弹出"
- 依赖主窗口存在
- 通常用于对话框、设置面板等
类比:
主窗口 = 正门
- 进门的地方
- 独立存在
弹出窗口 = 房间
- 在正门里面
- 依赖正门存在
- 关闭正门,房间也关闭
代码示例:主窗口与弹出窗口
public class MainAndPopupShell {
public static void main(String[] args) {
Display display = new Display();
// 主窗口
Shell mainShell = new Shell(display);
mainShell.setText("主窗口");
mainShell.setBounds(100, 100, 400, 300);
// 按钮:打开弹出窗口
Button openPopupButton = new Button(mainShell, SWT.PUSH);
openPopupButton.setText("打开弹出窗口");
openPopupButton.setBounds(150, 100, 100, 30);
openPopupButton.addListener(SWT.Selection, event -> {
// 创建弹出窗口
Shell popupShell = new Shell(mainShell, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
popupShell.setText("弹出窗口");
popupShell.setBounds(200, 200, 300, 200);
// 弹出窗口的内容
Label label = new Label(popupShell, SWT.NONE);
label.setText("这是一个弹出窗口");
label.setBounds(80, 50, 140, 30);
Button closeButton = new Button(popupShell, SWT.PUSH);
closeButton.setText("关闭");
closeButton.setBounds(100, 120, 80, 30);
closeButton.addListener(SWT.Selection, e -> {
popupShell.dispose(); // 关闭弹出窗口
});
// 打开弹出窗口
popupShell.open();
});
mainShell.open();
while (!mainShell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
模态 vs 非模态
什么是模态?
模态 = "锁定"其他窗口
- 模态窗口打开时,其他窗口不能操作
- 用户必须先处理模态窗口
非模态 = 其他窗口正常
- 非模态窗口打开时,其他窗口仍然可以操作
- 用户可以自由切换窗口
类比:
模态窗口 = 警报灯
- 警报亮了(模态窗口打开)
- 必须先处理警报(关闭模态窗口)
- 否则不能做其他事
非模态窗口 = 灯泡
- 打开灯泡(非模态窗口打开)
- 不影响你做其他事
- 你可以随时开关
模态窗口的类型:
| 样式 | 含义 | 类比 |
|---|---|---|
SWT.PRIMARY_MODAL | 阻塞父窗口 | 对话框(必须先处理) |
SWT.APPLICATION_MODAL | 阻塞整个应用 | 系统对话框(必须先处理) |
SWT.MODELESS | 不阻塞任何窗口 | 普通窗口(可以自由切换) |
代码示例:三种模态
public class ModalExamples {
public static void main(String[] args) {
Display display = new Display();
Shell mainShell = new Shell(display);
mainShell.setText("主窗口");
mainShell.setBounds(100, 100, 400, 300);
// 按钮:打开 PRIMARY_MODAL 窗口
Button primaryButton = new Button(mainShell, SWT.PUSH);
primaryButton.setText("PRIMARY_MODAL");
primaryButton.setBounds(10, 10, 150, 30);
primaryButton.addListener(SWT.Selection, event -> {
Shell modalShell = new Shell(mainShell, SWT.DIALOG_TRIM | SWT.PRIMARY_MODAL);
modalShell.setText("PRIMARY_MODAL 窗口");
modalShell.setBounds(150, 150, 300, 200);
modalShell.open();
});
// 按钮:打开 APPLICATION_MODAL 窗口
Button applicationButton = new Button(mainShell, SWT.PUSH);
applicationButton.setText("APPLICATION_MODAL");
applicationButton.setBounds(10, 50, 150, 30);
applicationButton.addListener(SWT.Selection, event -> {
Shell modalShell = new Shell(mainShell, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
modalShell.setText("APPLICATION_MODAL 窗口");
modalShell.setBounds(150, 150, 300, 200);
modalShell.open();
});
// 按钮:打开 MODELESS 窗口
Button modelessButton = new Button(mainShell, SWT.PUSH);
modelessButton.setText("MODELESS");
modelessButton.setBounds(10, 90, 150, 30);
modelessButton.addListener(SWT.Selection, event -> {
Shell modelessShell = new Shell(mainShell, SWT.DIALOG_TRIM | SWT.MODELESS);
modelessShell.setText("MODELESS 窗口");
modelessShell.setBounds(150, 150, 300, 200);
modelessShell.open();
});
mainShell.open();
while (!mainShell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
测试方法:
- 点击 PRIMARY_MODAL:主窗口无法操作
- 点击 APPLICATION_MODAL:所有窗口无法操作
- 点击 MODELESS:所有窗口都可以操作
Shell 的样式(Styles)
Shell 的样式用于控制窗口的外观和行为
// 基本窗口
Shell shell = new Shell(display, SWT.SHELL_TRIM);
常用样式:
| 样式 | 作用 | 外观 |
|---|---|---|
SWT.TITLE | 显示标题栏 | 有标题 |
SWT.CLOSE | 显示关闭按钮 | 可以关闭 |
SWT.MIN | 显示最小化按钮 | 可以最小化 |
SWT.MAX | 显示最大化按钮 | 可以最大化 |
SWT.BORDER | 显示边框 | 有边框 |
SWT.RESIZE | 可以调整大小 | 可以拖动边缘调整大小 |
SWT.NO_TRIM | 无任何装饰 | 纯净窗口 |
SWT.TOOL | 工具窗口 | 小标题栏 |
SWT.SHELL_TRIM | 标准窗口(包含 TITLE, CLOSE, MIN, MAX, BORDER, RESIZE) | 普通窗口 |
SWT.DIALOG_TRIM | 对话框(包含 TITLE, BORDER, CLOSE) | 对话框 |
样式可以组合使用(用 <code>|</code> 连接):
// 只有标题栏和关闭按钮
Shell shell = new Shell(display, SWT.TITLE | SWT.BORDER | SWT.CLOSE);
// 工具窗口 + 模态
Shell toolShell = new Shell(display, SWT.TOOL | SWT.APPLICATION_MODAL);
// 纯净窗口(无标题栏,无边框)
Shell noTrimShell = new Shell(display, SWT.NO_TRIM);
代码示例:不同样式的 Shell
public class ShellStyles {
public static void main(String[] args) {
Display display = new Display();
Shell mainShell = new Shell(display);
mainShell.setText("主窗口");
mainShell.setBounds(100, 100, 600, 400);
// 按钮:打开标准窗口
Button standardButton = new Button(mainShell, SWT.PUSH);
standardButton.setText("标准窗口");
standardButton.setBounds(10, 10, 100, 30);
standardButton.addListener(SWT.Selection, event -> {
Shell shell = new Shell(display, SWT.SHELL_TRIM);
shell.setText("标准窗口");
shell.open();
});
// 按钮:打开对话框
Button dialogButton = new Button(mainShell, SWT.PUSH);
dialogButton.setText("对话框");
dialogButton.setBounds(10, 50, 100, 30);
dialogButton.addListener(SWT.Selection, event -> {
Shell shell = new Shell(display, SWT.DIALOG_TRIM);
shell.setText("对话框");
shell.open();
});
// 按钮:打开工具窗口
Button toolButton = new Button(mainShell, SWT.PUSH);
toolButton.setText("工具窗口");
toolButton.setBounds(10, 90, 100, 30);
toolButton.addListener(SWT.Selection, event -> {
Shell shell = new Shell(display, SWT.TOOL);
shell.setText("工具窗口");
shell.open();
});
// 按钮:打开纯净窗口
Button noTrimButton = new Button(mainShell, SWT.PUSH);
noTrimButton.setText("纯净窗口");
noTrimButton.setBounds(10, 130, 100, 30);
noTrimButton.addListener(SWT.Selection, event -> {
Shell shell = new Shell(display, SWT.NO_TRIM);
shell.setBounds(200, 200, 300, 200);
// 添加关闭按钮(因为 NO_TRIM 没有关闭按钮)
Button closeButton = new Button(shell, SWT.PUSH);
closeButton.setText("关闭");
closeButton.setBounds(110, 150, 80, 30);
closeButton.addListener(SWT.Selection, e -> shell.dispose());
shell.open();
});
// 按钮:打开自定义样式的窗口
Button customButton = new Button(mainShell, SWT.PUSH);
customButton.setText("自定义样式");
customButton.setBounds(10, 170, 100, 30);
customButton.addListener(SWT.Selection, event -> {
Shell shell = new Shell(display, SWT.TITLE | SWT.BORDER | SWT.CLOSE);
shell.setText("自定义样式");
shell.open();
});
mainShell.open();
while (!mainShell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
3.3 事件循环:让程序活起来
readAndDispatch() 的秘密
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
费曼解释:readAndDispatch() 的含义
readAndDispatch = 读取并分发:
- read:从操作系统"读取"事件
- dispatch:把事件"分发"给对应的控件
类比:
事件循环 = 快递站
- 操作系统是快递公司
- 事件是快递包裹
- readAndDispatch() 是快递员
- 从快递公司取快递(read)
- 送到收件人手中(dispatch)
readAndDispatch() 的工作流程:
1. 检查操作系统有没有事件
- 有事件:读取事件
- 无事件:返回 false
2. 如果有事件,分发事件:
- 鼠标点击 → 分发给按钮
- 键盘输入 → 分发给文本框
- 窗口关闭 → 分发给 Shell
3. 返回值:
- 有事件处理:返回 true
- 无事件:返回 false
代码示例:查看事件
public class EventTracer {
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("事件追踪器");
shell.setBounds(100, 100, 400, 300);
Button button = new Button(shell, SWT.PUSH);
button.setText("点击我");
button.setBounds(150, 100, 100, 30);
button.addListener(SWT.Selection, event -> {
System.out.println("按钮被点击了!");
});
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
System.out.println("没有事件,休息一下");
display.sleep();
} else {
System.out.println("处理了事件");
}
}
display.dispose();
}
}
运行效果:
没有事件,休息一下
没有事件,休息一下
没有事件,休息一下
按钮被点击了!
处理了事件
没有事件,休息一下
...
sleep() 的作用
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
费曼解释:为什么需要 sleep()?
没有 sleep():
- 程序会不断检查事件(readAndDispatch)
- CPU 使用率 100%
- 系统变慢
有了 sleep():
- 没事件时,让 CPU 休息
- CPU 使用率接近 0%
- 系统流畅
类比:
没有 sleep() = 一直在转圈的陀螺
- 停不下来
- 浪费能量
有 sleep() = 正常人
- 有事就做(处理事件)
- 没事就休息(sleep)
费曼问题:sleep() 会影响响应速度吗?
不会!
原因:
- sleep() 只是"休息一小会儿"
- 一旦有事件,立即唤醒
- 响应速度几乎不受影响
类比:
sleep() = 门卫打盹
- 没人时打盹(sleep)
- 有人敲门,立即醒来(事件来了)
- 不会错过任何客人
为什么要这样写循环?
完整的事件循环:
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
费曼解释:每一步的含义
while (!shell.isDisposed()) {
// 只要窗口没有关闭,就继续循环
}
if (!display.readAndDispatch()) {
// 如果没有事件需要处理
display.sleep();
// 就让 CPU 休息一下
}
隐含的逻辑:
// 如果有事件,readAndDispatch() 会自动处理
// 不需要 sleep(),继续下一轮循环
为什么这样写?
方案 1:一直调用 readAndDispatch()
while (!shell.isDisposed()) {
display.readAndDispatch();
}
问题:
- CPU 使用率 100%
- 系统变慢
方案 2:一直 sleep()
while (!shell.isDisposed()) {
display.sleep();
display.readAndDispatch();
}
问题:
- 响应延迟
- 用户体验差
方案 3:按需 sleep()(标准写法)
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
优势:
- 有事件:立即处理
- 无事件:CPU 休息
- 响应快,CPU 占用低
3.4 完整示例:多窗口应用
public class MultiWindowApp {
public static void main(String[] args) {
Display display = new Display();
// 主窗口
Shell mainShell = new Shell(display);
mainShell.setText("多窗口应用");
mainShell.setBounds(100, 100, 500, 400);
// 标签
Label label = new Label(mainShell, SWT.NONE);
label.setText("这是一个多窗口应用");
label.setBounds(150, 20, 200, 30);
// 按钮:打开新窗口
Button openButton = new Button(mainShell, SWT.PUSH);
openButton.setText("打开新窗口");
openButton.setBounds(150, 80, 150, 30);
openButton.addListener(SWT.Selection, event -> {
createSecondaryWindow(display, "新窗口 " + (windowCounter++));
});
// 按钮:打开模态对话框
Button modalButton = new Button(mainShell, SWT.PUSH);
modalButton.setText("打开模态对话框");
modalButton.setBounds(150, 130, 150, 30);
modalButton.addListener(SWT.Selection, event -> {
createModalDialog(display, mainShell);
});
// 按钮:打开工具窗口
Button toolButton = new Button(mainShell, SWT.PUSH);
toolButton.setText("打开工具窗口");
toolButton.setBounds(150, 180, 150, 30);
toolButton.addListener(SWT.Selection, event -> {
createToolWindow(display);
});
mainShell.open();
// 事件循环
while (!mainShell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
private static int windowCounter = 1;
private static void createSecondaryWindow(Display display, String title) {
Shell shell = new Shell(display);
shell.setText(title);
shell.setBounds(200 + windowCounter * 20, 200 + windowCounter * 20, 300, 200);
Label label = new Label(shell, SWT.NONE);
label.setText(title);
label.setBounds(100, 50, 100, 30);
Button closeButton = new Button(shell, SWT.PUSH);
closeButton.setText("关闭");
closeButton.setBounds(100, 120, 80, 30);
closeButton.addListener(SWT.Selection, event -> {
shell.dispose();
});
shell.open();
}
private static void createModalDialog(Display display, Shell parent) {
Shell dialog = new Shell(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
dialog.setText("模态对话框");
dialog.setBounds(300, 300, 300, 200);
Label label = new Label(dialog, SWT.NONE);
label.setText("这是一个模态对话框");
label.setBounds(70, 50, 160, 30);
Button okButton = new Button(dialog, SWT.PUSH);
okButton.setText("确定");
okButton.setBounds(50, 120, 80, 30);
okButton.addListener(SWT.Selection, event -> {
dialog.dispose();
});
Button cancelButton = new Button(dialog, SWT.PUSH);
cancelButton.setText("取消");
cancelButton.setBounds(160, 120, 80, 30);
cancelButton.addListener(SWT.Selection, event -> {
dialog.dispose();
});
dialog.open();
}
private static void createToolWindow(Display display) {
Shell toolWindow = new Shell(display, SWT.TOOL);
toolWindow.setText("工具窗口");
toolWindow.setBounds(400, 400, 200, 150);
Label label = new Label(toolWindow, SWT.NONE);
label.setText("工具窗口");
label.setBounds(60, 20, 80, 30);
toolWindow.open();
}
}
3.5 本章小结
关键概念回顾
| 概念 | 定义 | 类比 |
|---|---|---|
| Display | 操作系统的代理 | 电话机 |
| Shell | 窗口的容器 | 相框 |
| 事件循环 | 持续处理事件 | 门卫 |
| readAndDispatch | 读取并分发事件 | 快递员 |
| sleep | CPU 休息 | 打盹 |
| 模态 | 锁定其他窗口 | 警报灯 |
| 样式 | 窗口的外观和功能 | 装饰品 |
费曼测试:你能解释清楚吗?
- 为什么只能有一个 Display?
- Display 是与操作系统的连接 - 就像只有一部电话,无法同时拨打两个号码
- 模态窗口和非模态窗口的区别?
- 模态:锁定其他窗口,必须先处理 - 非模态:其他窗口正常,可以自由切换
- 为什么事件循环需要 sleep()?
- 没有 sleep():CPU 使用率 100% - 有 sleep():有事件立即处理,无事件 CPU 休息
- 如何创建自定义样式的窗口?
- 用 | 组合多个样式,如 SWT.TITLE | SWT.BORDER | SWT.CLOSE
下一章预告
现在你已经掌握了 Display 和 Shell,
你可以:
- 创建多个窗口
- 控制窗口的样式
- 让窗口响应事件
但你的窗口还是空的,
下一章,我们将学习基础控件:
- 按钮、标签、文本框...
- 让窗口真正"有用"起来
练习题:
- 创建一个主窗口,包含三个按钮,分别打开不同样式的子窗口(对话框、工具窗口、纯净窗口)。
- 创建一个模态对话框,包含两个按钮(确定、取消),点击任意按钮关闭对话框。
- 修改事件循环,打印每个事件的信息(事件类型、控件类型)。
(提示:事件类型可以通过 event.type 获取,控件类型可以通过 event.widget.getClass().getSimpleName() 获取)