第3章:Display 与 Shell - 舞台与幕布

第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();
    }
}

测试方法:

  1. 点击 PRIMARY_MODAL:主窗口无法操作
  2. 点击 APPLICATION_MODAL:所有窗口无法操作
  3. 点击 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读取并分发事件快递员
sleepCPU 休息打盹
模态锁定其他窗口警报灯
样式窗口的外观和功能装饰品

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

  1. 为什么只能有一个 Display?

- Display 是与操作系统的连接 - 就像只有一部电话,无法同时拨打两个号码

  1. 模态窗口和非模态窗口的区别?

- 模态:锁定其他窗口,必须先处理 - 非模态:其他窗口正常,可以自由切换

  1. 为什么事件循环需要 sleep()?

- 没有 sleep():CPU 使用率 100% - 有 sleep():有事件立即处理,无事件 CPU 休息

  1. 如何创建自定义样式的窗口?

- 用 | 组合多个样式,如 SWT.TITLE | SWT.BORDER | SWT.CLOSE

下一章预告

现在你已经掌握了 Display 和 Shell,
你可以:
- 创建多个窗口
- 控制窗口的样式
- 让窗口响应事件

但你的窗口还是空的,
下一章,我们将学习基础控件:
- 按钮、标签、文本框...
- 让窗口真正"有用"起来

练习题:

  1. 创建一个主窗口,包含三个按钮,分别打开不同样式的子窗口(对话框、工具窗口、纯净窗口)。
  2. 创建一个模态对话框,包含两个按钮(确定、取消),点击任意按钮关闭对话框。
  3. 修改事件循环,打印每个事件的信息(事件类型、控件类型)。

(提示:事件类型可以通过 event.type 获取,控件类型可以通过 event.widget.getClass().getSimpleName() 获取)

← 返回目录