第6章:事件处理 - 让控件响应你的心意

第6章:事件处理 - 让控件响应你的心意


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


6.1 事件系统的原理

监听器的本质

Button button = new Button(shell, SWT.PUSH);
button.addListener(SWT.Selection, event -> {
    System.out.println("按钮被点击了");
});

费曼解释:监听器是什么?

监听器 = 守门人
- 守在门口(按钮)
- 等待事件(点击)
- 事件来了,立即处理(执行代码)

类比:
监听器 = 门铃
- 你安装了门铃(添加监听器)
- 有人按门铃(事件发生)
- 你听到铃声,去开门(执行代码)

事件对象的含义

button.addListener(SWT.Selection, event -> {
    System.out.println("事件类型:" + event.type);
    System.out.println("控件:" + event.widget);
    System.out.println("时间:" + event.time);
    System.out.println("数据:" + event.data);
});

费曼解释:事件对象携带了什么信息?

事件对象 = 快递包裹
- 包裹上有标签(事件类型)
- 包裹里有内容(数据)
- 收件人是你(处理代码)

类比:
事件对象 = 110 报警
- 你拨打 110(添加监听器)
- 接警员记录信息(事件对象)
  - 报警时间(event.time)
  - 报警类型(event.type)
  - 报警地点(event.data)
- 派出警力(执行代码)

Event 对象的常用属性:

属性类型含义类比
typeint事件类型快递单类型
widgetWidget触发事件的控件发件人
timeint事件发生的时间时间戳
dataObject用户自定义数据包裹内容
detailint事件详细信息额外信息

6.2 常用事件监听器

SelectionListener:点击与选择

// 方法 1:使用 addListener
Button button = new Button(shell, SWT.PUSH);
button.addListener(SWT.Selection, event -> {
    System.out.println("按钮被点击了");
});

// 方法 2:使用 SelectionListener
button.addSelectionListener(new SelectionListener() {
    @Override
    public void widgetSelected(SelectionEvent e) {
        System.out.println("按钮被选中");
    }
    
    @Override
    public void widgetDefaultSelected(SelectionEvent e) {
        System.out.println("按钮默认选中");
    }
});

// 方法 3:使用 Lambda(推荐)
button.addSelectionListener(event -> {
    System.out.println("按钮被选中");
});

费曼解释:SelectionListener 的两个方法

widgetSelected = 用户点击
- 按钮被点击
- 复选框被选中/取消
- 列表项被选择

widgetDefaultSelected = 用户双击
- 文本框双击
- 列表项双击
- 树节点双击

类比:
widgetSelected = 单击
- 你单击鼠标,选择一个选项

widgetDefaultSelected = 双击
- 你双击鼠标,打开一个文件

代码示例:各种 SelectionListener

public class SelectionListenerExample {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("SelectionListener 示例");
        shell.setLayout(new FillLayout(SWT.VERTICAL));
        
        // 按钮
        Button button = new Button(shell, SWT.PUSH);
        button.setText("点击我");
        button.addListener(SWT.Selection, event -> {
            System.out.println("按钮被点击");
        });
        
        // 复选框
        Button checkBox = new Button(shell, SWT.CHECK);
        checkBox.setText("记住密码");
        checkBox.addListener(SWT.Selection, event -> {
            boolean selected = checkBox.getSelection();
            System.out.println("复选框状态:" + selected);
        });
        
        // 单选按钮
        Composite radioGroup = new Composite(shell, SWT.NONE);
        radioGroup.setLayout(new RowLayout(SWT.VERTICAL));
        
        Label radioLabel = new Label(radioGroup, SWT.NONE);
        radioLabel.setText("选择主题:");
        
        Button radio1 = new Button(radioGroup, SWT.RADIO);
        radio1.setText("深色");
        
        Button radio2 = new Button(radioGroup, SWT.RADIO);
        radio2.setText("浅色");
        
        Button radio3 = new Button(radioGroup, SWT.RADIO);
        radio3.setText("跟随系统");
        radio3.setSelection(true);
        
        radio1.addListener(SWT.Selection, event -> {
            if (radio1.getSelection()) {
                System.out.println("选择了深色主题");
            }
        });
        
        radio2.addListener(SWT.Selection, event -> {
            if (radio2.getSelection()) {
                System.out.println("选择了浅色主题");
            }
        });
        
        radio3.addListener(SWT.Selection, event -> {
            if (radio3.getSelection()) {
                System.out.println("选择了跟随系统主题");
            }
        });
        
        // 列表
        List list = new List(shell, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
        list.add("选项 1");
        list.add("选项 2");
        list.add("选项 3");
        list.addListener(SWT.Selection, event -> {
            String[] selectedItems = list.getSelection();
            System.out.println("选中项:" + String.join(", ", selectedItems));
        });
        
        // 下拉列表
        Combo combo = new Combo(shell, SWT.DROP_DOWN | SWT.READ_ONLY);
        combo.add("苹果");
        combo.add("香蕉");
        combo.add("橙子");
        combo.addListener(SWT.Selection, event -> {
            System.out.println("下拉列表选择:" + combo.getText());
        });
        
        shell.setBounds(100, 100, 400, 500);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
}

ModifyListener:内容改变

Text text = new Text(shell, SWT.BORDER);
text.addListener(SWT.Modify, event -> {
    System.out.println("文本内容:" + text.getText());
});

费曼解释:ModifyListener 的触发时机

ModifyListener = 看门狗
- 文本框的内容有任何变化
- 立即触发监听器
- 每个字符输入都会触发

类比:
ModifyListener = 文件监控
- 文件被修改
- 立即通知你
- 每次保存都会触发

代码示例:实时字数统计

public class ModifyListenerExample {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("ModifyListener 示例");
        shell.setLayout(new GridLayout(2, false));
        
        // 标签
        Label label = new Label(shell, SWT.NONE);
        label.setText("输入文本:");
        
        // 文本框
        Text text = new Text(shell, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
        GridData gridData = new GridData();
        gridData.horizontalAlignment = GridData.FILL;
        gridData.verticalAlignment = GridData.FILL;
        gridData.grabExcessHorizontalSpace = true;
        gridData.grabExcessVerticalSpace = true;
        text.setLayoutData(gridData);
        
        // 字数统计
        Label countLabel = new Label(shell, SWT.NONE);
        countLabel.setText("字数:0");
        GridData countData = new GridData();
        countData.horizontalSpan = 2;
        countLabel.setLayoutData(countData);
        
        // 监听文本改变
        text.addListener(SWT.Modify, event -> {
            String content = text.getText();
            int charCount = content.length();
            int wordCount = content.split("\\s+").length;
            int lineCount = text.getLineCount();
            
            countLabel.setText(String.format("字符数:%d,单词数:%d,行数:%d",
                    charCount, wordCount, lineCount));
        });
        
        shell.setBounds(100, 100, 400, 300);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
}

FocusListener:焦点获得与失去

Text text = new Text(shell, SWT.BORDER);
text.addListener(SWT.FocusIn, event -> {
    System.out.println("文本框获得焦点");
});

text.addListener(SWT.FocusOut, event -> {
    System.out.println("文本框失去焦点");
});

费曼解释:焦点的概念

焦点 = 舞光灯
- 只有一个控件有焦点(被舞台灯照亮)
- 这个控件接收键盘输入
- 其他控件处于"黑暗"中

类比:
焦点 = 主持人
- 只有一个人在说话(有焦点)
- 其他人听(无焦点)
- 切换焦点 = 换主持人

代码示例:焦点验证

public class FocusListenerExample {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("FocusListener 示例");
        shell.setLayout(new GridLayout(2, false));
        
        // 用户名
        Label usernameLabel = new Label(shell, SWT.NONE);
        usernameLabel.setText("用户名:");
        
        Text usernameText = new Text(shell, SWT.BORDER);
        GridData usernameData = new GridData();
        usernameData.horizontalAlignment = GridData.FILL;
        usernameData.grabExcessHorizontalSpace = true;
        usernameText.setLayoutData(usernameData);
        
        // 邮箱
        Label emailLabel = new Label(shell, SWT.NONE);
        emailLabel.setText("邮箱:");
        
        Text emailText = new Text(shell, SWT.BORDER);
        GridData emailData = new GridData();
        emailData.horizontalAlignment = GridData.FILL;
        emailData.grabExcessHorizontalSpace = true;
        emailText.setLayoutData(emailData);
        
        // 验证邮箱
        emailText.addListener(SWT.FocusOut, event -> {
            String email = emailText.getText();
            if (!email.contains("@")) {
                emailText.setBackground(display.getSystemColor(SWT.COLOR_RED));
                emailText.setMessage("邮箱格式不正确(需要包含 @)");
            } else {
                emailText.setBackground(null);
                emailText.setMessage("");
            }
        });
        
        // 电话
        Label phoneLabel = new Label(shell, SWT.NONE);
        phoneLabel.setText("电话:");
        
        Text phoneText = new Text(shell, SWT.BORDER);
        GridData phoneData = new GridData();
        phoneData.horizontalAlignment = GridData.FILL;
        phoneData.grabExcessHorizontalSpace = true;
        phoneText.setLayoutData(phoneData);
        
        // 验证电话(必须是数字)
        phoneText.addListener(SWT.FocusOut, event -> {
            String phone = phoneText.getText();
            if (!phone.matches("\\d+")) {
                phoneText.setBackground(display.getSystemColor(SWT.COLOR_RED));
                phoneText.setMessage("电话格式不正确(必须是数字)");
            } else {
                phoneText.setBackground(null);
                phoneText.setMessage("");
            }
        });
        
        // 提交按钮
        Button submitButton = new Button(shell, SWT.PUSH);
        submitButton.setText("提交");
        GridData submitData = new GridData();
        submitData.horizontalSpan = 2;
        submitData.horizontalAlignment = GridData.CENTER;
        submitButton.setLayoutData(submitData);
        
        submitButton.addListener(SWT.Selection, event -> {
            System.out.println("用户名:" + usernameText.getText());
            System.out.println("邮箱:" + emailText.getText());
            System.out.println("电话:" + phoneText.getText());
        });
        
        shell.setBounds(100, 100, 400, 200);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
}

KeyListener:键盘按键

Text text = new Text(shell, SWT.BORDER);
text.addListener(SWT.KeyDown, event -> {
    System.out.println("按下按键:" + event.keyCode);
    System.out.println("字符:" + event.character);
    
    // 检测是否按下 Enter 键
    if (event.keyCode == SWT.CR || event.keyCode == SWT.LF) {
        System.out.println("按下了 Enter 键");
    }
    
    // 检测是否按下 Ctrl+C
    if (event.stateMask == SWT.CTRL && event.keyCode == 'c') {
        System.out.println("按下了 Ctrl+C");
    }
});

费曼解释:按键事件的两个属性

keyCode = 按键的代码
- 键盘上的每个键都有唯一的代码
- 例如:'A' 键的代码是 65
- 例如:Enter 键的代码是 SWT.CR

character = 输入的字符
- 按键对应的字符
- 例如:按下 'A' 键,character = 'A'
- 例如:按下 Shift+'A',character = 'a'

类比:
keyCode = 按钮的位置
- 第 1 行第 1 列的按钮

character = 按钮上的字
- 按钮上写着 'A'

常用键码:

键码含义类比
SWT.CREnter回车键
SWT.LFLine Feed换行键
SWT.TABTab制表键
SWT.ESCEsc退出键
SWT.BSBackspace退格键
SWT.DELDelete删除键
SWT.INSERTInsert插入键
SWT.HOMEHome首页键
SWT.ENDEnd结尾键
SWT.PAGE_UPPage Up上翻页
SWT.PAGE_DOWNPage Down下翻页
SWT.F1 ~ SWT.F12F1 ~ F12功能键

修饰键:

修饰键含义类比
SWT.SHIFTShift上档键
SWT.CTRLCtrl控制键
SWT.ALTAlt替换键
SWT.COMMANDCommand (Mac)命令键

代码示例:键盘快捷键

public class KeyListenerExample {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("KeyListener 示例");
        shell.setLayout(new GridLayout(1, false));
        
        // 文本框
        Text text = new Text(shell, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
        GridData gridData = new GridData();
        gridData.horizontalAlignment = GridData.FILL;
        gridData.verticalAlignment = GridData.FILL;
        gridData.grabExcessHorizontalSpace = true;
        gridData.grabExcessVerticalSpace = true;
        text.setLayoutData(gridData);
        
        // 监听按键
        text.addListener(SWT.KeyDown, event -> {
            // Ctrl+S:保存
            if (event.stateMask == SWT.CTRL && event.keyCode == 's') {
                event.doit = false;  // 阻止默认行为(不在文本框中输入 's')
                System.out.println("快捷键:Ctrl+S(保存)");
                System.out.println("文本内容:" + text.getText());
            }
            
            // Ctrl+C:复制(默认行为)
            if (event.stateMask == SWT.CTRL && event.keyCode == 'c') {
                System.out.println("快捷键:Ctrl+C(复制)");
            }
            
            // Ctrl+V:粘贴(默认行为)
            if (event.stateMask == SWT.CTRL && event.keyCode == 'v') {
                System.out.println("快捷键:Ctrl+V(粘贴)");
            }
            
            // Ctrl+A:全选
            if (event.stateMask == SWT.CTRL && event.keyCode == 'a') {
                event.doit = false;
                text.selectAll();
                System.out.println("快捷键:Ctrl+A(全选)");
            }
            
            // Enter:换行
            if (event.keyCode == SWT.CR || event.keyCode == SWT.LF) {
                System.out.println("按下 Enter 键");
            }
            
            // Tab:插入 4 个空格
            if (event.keyCode == SWT.TAB) {
                event.doit = false;
                text.insert("    ");
                System.out.println("按下 Tab 键");
            }
        });
        
        // 状态栏
        Label statusLabel = new Label(shell, SWT.BORDER);
        GridData statusData = new GridData();
        statusData.horizontalAlignment = GridData.FILL;
        statusData.grabExcessHorizontalSpace = true;
        statusLabel.setLayoutData(statusData);
        
        text.addListener(SWT.Modify, event -> {
            statusLabel.setText("字符数:" + text.getText().length());
        });
        
        shell.setBounds(100, 100, 400, 300);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
}

MouseListener:鼠标点击

Button button = new Button(shell, SWT.PUSH);
button.setText("点击我");
button.addListener(SWT.MouseDown, event -> {
    System.out.println("鼠标按下:x=" + event.x + ", y=" + event.y);
    System.out.println("按钮:" + event.button);
});

button.addListener(SWT.MouseUp, event -> {
    System.out.println("鼠标释放:x=" + event.x + ", y=" + event.y);
});

button.addListener(SWT.MouseDoubleClick, event -> {
    System.out.println("鼠标双击:x=" + event.x + ", y=" + event.y);
});

费曼解释:鼠标事件的三个类型

MouseDown = 按下鼠标
- 鼠标按键按下
- 还没释放

MouseUp = 释放鼠标
- 鼠标按键释放
- 完成一次点击

MouseDoubleClick = 双击鼠标
- 快速点击两次
- 系统判断为双击

类比:
MouseDown = 按门铃
- 手指按下门铃

MouseUp = 松开手指
- 手指松开

MouseDoubleClick = 按两下门铃
- 快速按两次

鼠标按键:

按钮含义类比
event.button == 1左键主要按钮
event.button == 2中键滚轮按下
event.button == 3右键上下文菜单

代码示例:鼠标事件追踪

public class MouseListenerExample {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("MouseListener 示例");
        shell.setLayout(new GridLayout(2, false));
        
        // 绘制区域
        Canvas canvas = new Canvas(shell, SWT.BORDER);
        GridData canvasData = new GridData();
        canvasData.horizontalAlignment = GridData.FILL;
        canvasData.verticalAlignment = GridData.FILL;
        canvasData.grabExcessHorizontalSpace = true;
        canvasData.grabExcessVerticalSpace = true;
        canvasData.widthHint = 300;
        canvasData.heightHint = 200;
        canvas.setLayoutData(canvasData);
        
        // 绘制圆圈的位置
        int[] circle = new int[]{150, 100};  // x, y
        
        // 绘制
        canvas.addPaintListener(event -> {
            GC gc = event.gc;
            gc.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
            gc.fillRectangle(canvas.getClientArea());
            gc.setBackground(display.getSystemColor(SWT.COLOR_BLUE));
            gc.fillOval(circle[0] - 20, circle[1] - 20, 40, 40);
        });
        
        // 鼠标移动
        canvas.addListener(SWT.MouseMove, event -> {
            System.out.println("鼠标移动:x=" + event.x + ", y=" + event.y);
        });
        
        // 鼠标按下
        canvas.addListener(SWT.MouseDown, event -> {
            System.out.println("鼠标按下:x=" + event.x + ", y=" + event.y + ", 按钮=" + event.button);
            
            // 左键:移动圆圈
            if (event.button == 1) {
                circle[0] = event.x;
                circle[1] = event.y;
                canvas.redraw();
            }
            
            // 右键:改变颜色
            if (event.button == 3) {
                circle[0] = event.x;
                circle[1] = event.y;
                canvas.redraw();
            }
        });
        
        // 鼠标双击
        canvas.addListener(SWT.MouseDoubleClick, event -> {
            System.out.println("鼠标双击:x=" + event.x + ", y=" + event.y);
            // 双击:重置圆圈位置
            circle[0] = 150;
            circle[1] = 100;
            canvas.redraw();
        });
        
        // 信息面板
        Composite infoPanel = new Composite(shell, SWT.NONE);
        infoPanel.setLayout(new GridLayout(1, false));
        
        Label infoLabel = new Label(infoPanel, SWT.NONE);
        infoLabel.setText("鼠标事件信息:");
        
        Text infoText = new Text(infoPanel, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
        GridData infoData = new GridData();
        infoData.horizontalAlignment = GridData.FILL;
        infoData.verticalAlignment = GridData.FILL;
        infoData.grabExcessHorizontalSpace = true;
        infoData.grabExcessVerticalSpace = true;
        infoText.setLayoutData(infoData);
        
        // 添加日志的辅助方法
        canvas.addListener(SWT.MouseDown, event -> {
            String info = String.format("按下:x=%d, y=%d, 按钮=%d\n",
                    event.x, event.y, event.button);
            infoText.append(info);
        });
        
        canvas.addListener(SWT.MouseUp, event -> {
            String info = String.format("释放:x=%d, y=%d, 按钮=%d\n",
                    event.x, event.y, event.button);
            infoText.append(info);
        });
        
        canvas.addListener(SWT.MouseDoubleClick, event -> {
            String info = String.format("双击:x=%d, y=%d\n",
                    event.x, event.y);
            infoText.append(info);
        });
        
        shell.setBounds(100, 100, 500, 300);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
}

MouseMoveListener:鼠标移动

Canvas canvas = new Canvas(shell, SWT.NONE);
canvas.addListener(SWT.MouseMove, event -> {
    System.out.println("鼠标位置:x=" + event.x + ", y=" + event.y);
});

费曼解释:MouseMoveListener 的触发频率

MouseMoveListener = 雷达
- 持续追踪鼠标位置
- 每次移动都会触发
- 触发频率很高

类比:
MouseMoveListener = 摄像头追踪
- 摄像头持续追踪目标
- 目标移动,摄像头跟随
- 实时更新位置

6.3 匿名类 vs Lambda 表达式

从冗长到简洁的演进

匿名类(Java 7 及之前):

button.addSelectionListener(new SelectionListener() {
    @Override
    public void widgetSelected(SelectionEvent e) {
        System.out.println("按钮被点击");
    }
    
    @Override
    public void widgetDefaultSelected(SelectionEvent e) {
        // 不需要实现,空着
    }
});

匿名内部类(简化):

button.addSelectionListener(new SelectionAdapter() {
    @Override
    public void widgetSelected(SelectionEvent e) {
        System.out.println("按钮被点击");
    }
});

Lambda 表达式(Java 8+):

button.addListener(SWT.Selection, event -> {
    System.out.println("按钮被点击");
});

费曼解释:Lambda 的优势

匿名类 = 手写信
- 写得很详细
- 占很多纸
- 看起来很正式

Lambda = 短信
- 写得很简练
- 占很少字
- 一目了然

类比:
匿名类 = 正式场合穿西装
- 庄重,但繁琐

Lambda = 休闲场合穿 T 恤
- 轻松,效率高

Lambda 的局限(需要 Java 8+)

Lambda 只适用于函数式接口(只有一个抽象方法的接口)

// 可以用 Lambda
button.addListener(SWT.Selection, event -> {
    System.out.println("按钮被点击");
});

// 不能用 Lambda(SelectionListener 有两个抽象方法)
button.addSelectionListener(new SelectionAdapter() {
    @Override
    public void widgetSelected(SelectionEvent e) {
        System.out.println("按钮被点击");
    }
});

费曼解释:为什么 SelectionListener 不能用 Lambda?

Lambda = 单一任务
- 只能处理一种情况
- SelectionListener 有两种情况(widgetSelected、widgetDefaultSelected)
- 无法用 Lambda

SelectionAdapter = 默认实现
- 实现了 SelectionListener
- 提供了默认实现
- 可以只重写需要的方法

类比:
Lambda = 单功能工具
- 只能拧螺丝

SelectionAdapter = 多功能工具
- 可以拧螺丝
- 可以锯木头
- 可以锤钉子
- 你只拿你需要的功能

6.4 事件的传播与取消

doit 字段的秘密

Text text = new Text(shell, SWT.BORDER);
text.addListener(SWT.KeyDown, event -> {
    // 检测是否按下 Enter 键
    if (event.keyCode == SWT.CR || event.keyCode == SWT.LF) {
        System.out.println("按下了 Enter 键");
        
        // 阻止默认行为(不在文本框中输入换行)
        event.doit = false;
        
        // 执行自定义行为
        System.out.println("执行提交操作");
    }
});

费曼解释:doit 字段的作用

doit = 放行标志
- doit = true:放行(执行默认行为)
- doit = false:拦截(阻止默认行为)

类比:
doit = 保安
- doit = true:放行,让你进入
- doit = false:拦截,不让你进入

代码示例:阻止文本框输入特殊字符

public class EventCancelExample {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("事件取消示例");
        shell.setLayout(new GridLayout(2, false));
        
        // 文本框:只允许输入数字
        Label numberLabel = new Label(shell, SWT.NONE);
        numberLabel.setText("数字输入(只允许数字):");
        
        Text numberText = new Text(shell, SWT.BORDER);
        GridData numberData = new GridData();
        numberData.horizontalAlignment = GridData.FILL;
        numberData.grabExcessHorizontalSpace = true;
        numberText.setLayoutData(numberData);
        
        numberText.addListener(SWT.Verify, event -> {
            char c = event.character;
            if (c >= '0' && c <= '9') {
                // 数字,允许输入
                event.doit = true;
            } else {
                // 非数字,阻止输入
                event.doit = false;
                System.out.println("阻止输入:" + c);
            }
        });
        
        // 文本框:限制长度
        Label lengthLabel = new Label(shell, SWT.NONE);
        lengthLabel.setText("长度限制(最多 10 字符):");
        
        Text lengthText = new Text(shell, SWT.BORDER);
        GridData lengthData = new GridData();
        lengthData.horizontalAlignment = GridData.FILL;
        lengthData.grabExcessHorizontalSpace = true;
        lengthText.setLayoutData(lengthData);
        
        lengthText.addListener(SWT.Verify, event -> {
            int currentLength = lengthText.getText().length();
            int selectionLength = lengthText.getSelectionCount();
            int newLength = currentLength - selectionLength + event.text.length();
            
            if (newLength > 10) {
                event.doit = false;
                System.out.println("阻止输入:超过最大长度 10");
            }
        });
        
        // 文本框:不允许粘贴
        Label pasteLabel = new Label(shell, SWT.NONE);
        pasteLabel.setText("不允许粘贴:");
        
        Text pasteText = new Text(shell, SWT.BORDER);
        GridData pasteData = new GridData();
        pasteData.horizontalAlignment = GridData.FILL;
        pasteData.grabExcessHorizontalSpace = true;
        pasteText.setLayoutData(pasteData);
        
        pasteText.addListener(SWT.KeyDown, event -> {
            // 检测是否按下 Ctrl+V(粘贴)
            if (event.stateMask == SWT.CTRL && event.keyCode == 'v') {
                event.doit = false;
                System.out.println("阻止粘贴");
            }
        });
        
        shell.setBounds(100, 100, 400, 200);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
}

事件链与优先级

Shell shell = new Shell(display);
shell.addListener(SWT.MouseDown, event -> {
    System.out.println("Shell 鼠标按下");
});

Composite composite = new Composite(shell, SWT.NONE);
composite.addListener(SWT.MouseDown, event -> {
    System.out.println("Composite 鼠标按下");
});

Button button = new Button(composite, SWT.PUSH);
button.setText("按钮");
button.addListener(SWT.MouseDown, event -> {
    System.out.println("Button 鼠标按下");
    event.doit = false;  // 阻止事件继续传播
});

费曼解释:事件传播的顺序

事件传播 = 泼水
- 从下往上(Button → Composite → Shell)
- 每一层都可以拦截
- 拦截后,不再向上传播

类比:
事件传播 = 汇报工作
- 员工(Button)汇报给经理(Composite)
- 经理汇报给老板(Shell)
- 任何一层都可以"拦截"(不汇报)

代码示例:事件传播测试

public class EventPropagationExample {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setText("事件传播示例");
        shell.setLayout(new FillLayout());
        
        shell.addListener(SWT.MouseDown, event -> {
            System.out.println("Shell: MouseDown");
        });
        
        shell.addListener(SWT.MouseUp, event -> {
            System.out.println("Shell: MouseUp");
        });
        
        Composite composite = new Composite(shell, SWT.NONE);
        composite.addListener(SWT.MouseDown, event -> {
            System.out.println("Composite: MouseDown");
        });
        
        composite.addListener(SWT.MouseUp, event -> {
            System.out.println("Composite: MouseUp");
        });
        
        Button button = new Button(composite, SWT.PUSH);
        button.setText("点击我");
        button.addListener(SWT.MouseDown, event -> {
            System.out.println("Button: MouseDown");
            event.doit = false;  // 阻止事件传播
        });
        
        button.addListener(SWT.MouseUp, event -> {
            System.out.println("Button: MouseUp");
            // 不设置 event.doit = false,允许事件传播
        });
        
        shell.setBounds(100, 100, 400, 300);
        shell.open();
        
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        
        display.dispose();
    }
}

运行结果(点击按钮):

Button: MouseDown
Button: MouseUp
Composite: MouseUp
Shell: MouseUp

费曼解释:为什么 MouseUp 传播了?

MouseDown:
- 按钮拦截了(event.doit = false)
- 不向上传播

MouseUp:
- 按钮没有拦截(默认 event.doit = true)
- 向上传播到 Composite
- 继续传播到 Shell

6.5 本章小结

事件监听器总结

监听器事件类型用途类比
SelectionListenerSWT.Selection点击、选择门铃
ModifyListenerSWT.Modify内容改变看门狗
FocusListenerSWT.FocusIn/Out焦点获得/失去舞光灯
KeyListenerSWT.KeyDown/Up键盘按键钢琴键
MouseListenerSWT.MouseDown/Up/DoubleClick鼠标点击门铃
MouseMoveListenerSWT.MouseMove鼠标移动雷达

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

  1. event.doit = false 的作用?

- 阻止事件的默认行为

  1. widgetSelected 和 widgetDefaultSelected 的区别?

- widgetSelected:单击 - widgetDefaultSelected:双击

  1. 为什么 SelectionListener 不能用 Lambda?

- SelectionListener 有两个抽象方法,不是函数式接口

  1. 事件传播的顺序?

- 从下往上(Button → Composite → Shell)

下一章预告

现在你已经掌握了事件处理,
控件可以响应用户的每一次操作,
但你还只能创建简单的界面。

下一章,我们将学习高级控件,
包括:表格、树、菜单、工具栏...
让界面功能更强大,体验更丰富。

练习题:

  1. 创建一个文本框,监听键盘输入,按下 Enter 键时打印文本内容。
  2. 创建一个 Canvas,监听鼠标移动,实时显示鼠标坐标。
  3. 创建一个表单,包含用户名、邮箱,失去焦点时验证格式,错误时背景变红。

(提示:邮箱验证可以检查是否包含 '@',文本框背景用 setBackground(display.getSystemColor(SWT.COLOR_RED))

← 返回目录