首先需要了解两个概念:

  1. 事件层级
    首先上一张图:
    image.png
    举个例子,如果只在按键被释放时才响应,则可以使用KeyEvent.KEY_RELEASED作为过滤器或处理器的事件类型。如果使用KeyEvent.KEY_PRESSED作为过滤器或处理器的事件类型,则只会在按键按下时触发。如果我们想在按键按下或释放时都触发一个事件,那该怎么办?我们可以直接使用他们的父级类型,也就是KeyEvent.ANY作为事件类型。意思就是我们按键时,不仅会触发KeyEvent.KEY_PRESSED,也会触发KeyANY,还会触发InputEvent.ANY。子层级的优先级比父层级的优先级高。

  2. 事件派发链
    image.png
    当我们点击上图中的三角形时,事件派发链如下:
    image
    事件被程序的根节点派发并通过事件派发链向下传递到目标节点(事件捕获阶段)。
    如果派发链中的任何节点为所发生的事件类型注册了Event Filter,则该Event Filter将会被调用。当Event filter执行完成以后,对应的事件会向下传递到事件派发链中的下一个节点。如果该节点未注册过滤器,事件将被传递到事件派发链中的下一个节点。如果没有任何过滤器消费掉事件,则事件目标最终将会接收到该事件并处理之。
    当到达事件目标并且所有已注册的过滤器都处理完事件以后,该事件将顺着派发链从目标节点返回到根节点(事件冒泡阶段)。
    如果在事件派发链中有节点为特定类型的事件注册了Event Handler,则在对应类型的事件发生时对应的Event Handler将会被调用。当Event Handler执行完成后,对应的事件将会向上传递给事件派发链中的上一个节点。如果么有任何Handler消费掉事件,则根节点最终将接收到对应的事件并且完成处理过程。
    Event filter和Event Handler执行的方向不一样,执行的时间不一样。
    一个节点可以注册多个Event Handler。Event Handler执行的顺序取决于事件类型的层级。特定事件类型的Event Handler会先于通用事件类型的Event Handler执行。例如,KeyEvent.KEY_TYPED事件的过滤器会在InputEvent.ANY事件的处理器执行。
    事件可以被Event Filter或 Event Handler在事件派发链中的任意节点上通过调用consume()方法消耗掉。

带有事件处理快捷方法的类

用户动作事件类型所在类
按下键盘上的按键KeyEventNode、Scene
移动鼠标或者按下鼠标按键MouseEventNode、Scene
执行完整的“按下-拖拽-释放”鼠标动作MouseDragEventNode、Scene
在一个节点中,底层输入法提示其文本的改变。编辑中的文本被生成/改变/移除时,底层输入法会提交最终结果,或者改变插入符位置。InputMethodEventNode、Scene
执行受所在平台支持的拖拽动作DragEventNode、Scene
滚动某对象ScrollEventNode、Scene
在某对象上执行旋转手势RotateEventNode、Scene
在某对象上执行滑动手势SwipeEventNode、Scene
触摸某对象TouchEventNode、Scene
在某对象上执行缩放手势ZoomEventNode、Scene
请求上下文菜单ContextMenuEventNode、Scene
按下按钮、显示或隐藏组合框、选择菜单项ActionEventButtonBase、ComboBoxBase、ContextMenu、MenuItem、TextField
编辑列表、表格或者树的子项ListView.EditEvent TableColumn.CellEditEvent TreeView.EditEventListView TableColumn TreeView
媒体播放器遇到错误MediaErrorEventMediaView
菜单被显示或者隐藏EventMenu
弹出式窗口被隐藏EventPopupWindow
选项卡被选择或者关闭EventTab
窗口被关闭、显示或者隐藏WindowEventWindow

使用便捷方法注册事件
接下来是注册事件处理器的代码格式:

setOnEvent-type(EventHandler<? super event-class> value)

Event-type表示该Event Handler处理的事件类型,例如,setOnKeyTyped表示处理KEY_TYPED事件、setOnMouseClicked表示处理MOUSE_CLICKED事件。event-class表示事件类型的定义类,例如KeyEvent表示与键盘输入有关的事件、MouseEvent表示与鼠标输入有关的事件。字符串<? super event-class>表示该方法接收一个处理event-class类型或其父类型事件的处理器作为参数。例如,当事件是鼠标事件或者键盘事件时都可以使用InputEvent类型的Event Handler。

一个简单的动作事件和鼠标事件实例:

public class TestEvent extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Hello World");
        Group root = new Group();
        Scene scene = new Scene(root, 300, 250);
        Button btn = new Button();
        btn.setLayoutX(100);
        btn.setLayoutY(80);
        btn.setText("Hello World");
        btn.setOnAction(new EventHandler<ActionEvent>() {
            public void handle(ActionEvent event) {
                System.out.println("鼠标点击了");
            }
        });
        btn.setOnMousePressed(new EventHandler<MouseEvent>() {//鼠标按下时释放时触发
            @Override
            public void handle(MouseEvent event) {
                System.out.println("鼠标按下了");
            }
        });
        root.getChildren().add(btn);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

使用事件过滤器注册事件
直接上例子:

public final class TestEventFilter extends Application {
    private final BooleanProperty dragModeActiveProperty = new SimpleBooleanProperty(this, "dragModeActive", true);

    @Override
    public void start(final Stage stage) {
        final Node loginPanel = makeDraggable(createLoginPanel());
        final Node confirmationPanel = makeDraggable(createConfirmationPanel());
        final Node progressPanel = makeDraggable(createProgressPanel());

        loginPanel.relocate(0, 0);
        confirmationPanel.relocate(0, 67);
        progressPanel.relocate(0, 106);

        final Pane panelsPane = new Pane();
        panelsPane.getChildren().addAll(loginPanel, confirmationPanel, progressPanel);

        final BorderPane sceneRoot = new BorderPane();

        BorderPane.setAlignment(panelsPane, Pos.TOP_LEFT);
        sceneRoot.setCenter(panelsPane);

        final CheckBox dragModeCheckbox = new CheckBox("Drag mode");
        BorderPane.setMargin(dragModeCheckbox, new Insets(6));
        sceneRoot.setBottom(dragModeCheckbox);

        dragModeActiveProperty.bind(dragModeCheckbox.selectedProperty());

        final Scene scene = new Scene(sceneRoot, 400, 300);
        stage.setScene(scene);
        stage.setTitle("Draggable Panels Example");
        stage.show();
    }

    public static void main(final String[] args) {
        launch(args);
    }

    private Node makeDraggable(final Node node) {
        final DragContext dragContext = new DragContext();
        final Group wrapGroup = new Group(node);

        wrapGroup.addEventFilter(MouseEvent.ANY,
                mouseEvent -> {
                    if (dragModeActiveProperty.get()) {
                        mouseEvent.consume();
                    }
                });

        wrapGroup.addEventFilter(
                MouseEvent.MOUSE_PRESSED,
                mouseEvent -> {
                    if (dragModeActiveProperty.get()) {
                        // remember initial mouse cursor coordinates
                        // and node position
                        dragContext.mouseAnchorX = mouseEvent.getX();
                        dragContext.mouseAnchorY = mouseEvent.getY();
                        dragContext.initialTranslateX =
                                node.getTranslateX();
                        dragContext.initialTranslateY =
                                node.getTranslateY();
                    }
                });

        wrapGroup.addEventFilter(
                MouseEvent.MOUSE_DRAGGED,
                mouseEvent -> {
                    if (dragModeActiveProperty.get()) {
                        // shift node from its initial position by delta
                        // calculated from mouse cursor movement
                        node.setTranslateX(
                                dragContext.initialTranslateX
                                        + mouseEvent.getX()
                                        - dragContext.mouseAnchorX);
                        node.setTranslateY(
                                dragContext.initialTranslateY
                                        + mouseEvent.getY()
                                        - dragContext.mouseAnchorY);
                    }
                });

        return wrapGroup;
    }

    private static Node createLoginPanel() {
        final ToggleGroup toggleGroup = new ToggleGroup();

        final TextField textField = new TextField();
        textField.setPrefColumnCount(10);
        textField.setPromptText("Your name");

        final PasswordField passwordField = new PasswordField();
        passwordField.setPrefColumnCount(10);
        passwordField.setPromptText("Your password");

        final ChoiceBox<String> choiceBox = new ChoiceBox<>(
                FXCollections.observableArrayList(
                        "English", "\u0420\u0443\u0441\u0441\u043a\u0438\u0439",
                        "Fran\u00E7ais"));
        choiceBox.setTooltip(new Tooltip("Your language"));
        choiceBox.getSelectionModel().select(0);

        final HBox panel =
                createHBox(6,
                        createVBox(2, createRadioButton("High", toggleGroup, true),
                                createRadioButton("Medium", toggleGroup, false),
                                createRadioButton("Low", toggleGroup, false)),
                        createVBox(2, textField, passwordField),
                        choiceBox);
        panel.setAlignment(Pos.BOTTOM_LEFT);
        configureBorder(panel);

        return panel;
    }

    private static Node createConfirmationPanel() {
        final Label acceptanceLabel = new Label("Not Available");

        final Button acceptButton = new Button("Accept");
        acceptButton.setOnAction(event -> acceptanceLabel.setText("Accepted"));

        final Button declineButton = new Button("Decline");
        declineButton.setOnAction(event -> acceptanceLabel.setText("Declined"));

        final HBox panel = createHBox(6, acceptButton,
                declineButton,
                acceptanceLabel);
        panel.setAlignment(Pos.CENTER_LEFT);
        configureBorder(panel);

        return panel;
    }

    private static Node createProgressPanel() {
        final Slider slider = new Slider();

        final ProgressIndicator progressIndicator = new ProgressIndicator(0);
        progressIndicator.progressProperty().bind(
                Bindings.divide(slider.valueProperty(),
                        slider.maxProperty()));

        final HBox panel = createHBox(6, new Label("Progress:"),
                slider,
                progressIndicator);
        configureBorder(panel);

        return panel;
    }

    private static void configureBorder(final Region region) {
        region.setStyle("-fx-background-color: white;"
                + "-fx-border-color: black;"
                + "-fx-border-width: 1;"
                + "-fx-border-radius: 6;"
                + "-fx-padding: 6;");
    }

    private static RadioButton createRadioButton(final String text, final ToggleGroup toggleGroup, final boolean selected) {
        final RadioButton radioButton = new RadioButton(text);
        radioButton.setToggleGroup(toggleGroup);
        radioButton.setSelected(selected);
        return radioButton;
    }

    private static HBox createHBox(final double spacing, final Node... children) {
        final HBox hbox = new HBox(spacing);
        hbox.getChildren().addAll(children);
        return hbox;
    }

    private static VBox createVBox(final double spacing, final Node... children) {
        final VBox vbox = new VBox(spacing);
        vbox.getChildren().addAll(children);
        return vbox;
    }

    private static final class DragContext {
        public double mouseAnchorX;
        public double mouseAnchorY;
        public double initialTranslateX;
        public double initialTranslateY;
    }
}

使用事件处理器注册事件

public final class TestEventHandler extends Application {
    @Override
    public void start(final Stage stage) {
        final Keyboard keyboard = new Keyboard(new Key(KeyCode.A),
                new Key(KeyCode.S),
                new Key(KeyCode.D),
                new Key(KeyCode.F));

        final Scene scene = new Scene(new Group(keyboard.createNode()));
        stage.setScene(scene);
        stage.setTitle("Keyboard Example");
        stage.show();
    }

    public static void main(final String[] args) {
        launch(args);
    }

    private static final class Key {
        private final KeyCode keyCode;
        private final BooleanProperty pressedProperty;

        public Key(final KeyCode keyCode) {
            this.keyCode = keyCode;
            this.pressedProperty = new SimpleBooleanProperty(this, "pressed");
        }

        public KeyCode getKeyCode() {
            return keyCode;
        }

        public boolean isPressed() {
            return pressedProperty.get();
        }

        public void setPressed(final boolean value) {
            pressedProperty.set(value);
        }

        public Node createNode() {
            final StackPane keyNode = new StackPane();
            keyNode.setFocusTraversable(true);
            installEventHandler(keyNode);

            final Rectangle keyBackground = new Rectangle(50, 50);
            keyBackground.fillProperty().bind(
                    Bindings.when(pressedProperty)
                            .then(Color.RED)
                            .otherwise(Bindings.when(keyNode.focusedProperty())
                                    .then(Color.LIGHTGRAY)
                                    .otherwise(Color.WHITE)));
            keyBackground.setStroke(Color.BLACK);
            keyBackground.setStrokeWidth(2);
            keyBackground.setArcWidth(12);
            keyBackground.setArcHeight(12);

            final Text keyLabel = new Text(keyCode.getName());
            keyLabel.setFont(Font.font("Arial", FontWeight.BOLD, 20));

            keyNode.getChildren().addAll(keyBackground, keyLabel);

            return keyNode;
        }

        private void installEventHandler(final Node keyNode) {
            // handler for enter key press / release events, other keys are
            // handled by the parent (keyboard) node handler
            final EventHandler<KeyEvent> keyEventHandler =
                    keyEvent -> {
                        if (keyEvent.getCode() == KeyCode.ENTER) {
                            setPressed(keyEvent.getEventType()
                                    == KeyEvent.KEY_PRESSED);

                            keyEvent.consume();
                        }
                    };

            keyNode.setOnKeyPressed(keyEventHandler);
            keyNode.setOnKeyReleased(keyEventHandler);
        }
    }

    private static final class Keyboard {
        private final Key[] keys;

        public Keyboard(final Key... keys) {
            this.keys = keys.clone();
        }

        public Node createNode() {
            final HBox keyboardNode = new HBox(6);
            keyboardNode.setPadding(new Insets(6));

            final List<Node> keyboardNodeChildren = keyboardNode.getChildren();
            for (final Key key : keys) {
                keyboardNodeChildren.add(key.createNode());
            }

            installEventHandler(keyboardNode);
            return keyboardNode;
        }

        private void installEventHandler(final Parent keyboardNode) {
            // handler for key pressed / released events not handled by
            // key nodes
            final EventHandler<KeyEvent> keyEventHandler =
                    keyEvent -> {
                        final Key key = lookupKey(keyEvent.getCode());
                        if (key != null) {
                            key.setPressed(keyEvent.getEventType()
                                    == KeyEvent.KEY_PRESSED);

                            keyEvent.consume();
                        }
                    };

            keyboardNode.setOnKeyPressed(keyEventHandler);
            keyboardNode.setOnKeyReleased(keyEventHandler);

            keyboardNode.addEventHandler(KeyEvent.KEY_PRESSED,
                    keyEvent -> handleFocusTraversal(
                            keyboardNode,
                            keyEvent));
        }

        private Key lookupKey(final KeyCode keyCode) {
            for (final Key key : keys) {
                if (key.getKeyCode() == keyCode) {
                    return key;
                }
            }
            return null;
        }

        private static void handleFocusTraversal(final Parent traversalGroup,
                                                 final KeyEvent keyEvent) {
            final Node nextFocusedNode;
            switch (keyEvent.getCode()) {
                case LEFT:
                    nextFocusedNode =
                            getPreviousNode(traversalGroup,
                                    (Node) keyEvent.getTarget());
                    keyEvent.consume();
                    break;

                case RIGHT:
                    nextFocusedNode =
                            getNextNode(traversalGroup,
                                    (Node) keyEvent.getTarget());
                    keyEvent.consume();
                    break;

                default:
                    return;
            }

            if (nextFocusedNode != null) {
                nextFocusedNode.requestFocus();
            }
        }

        private static Node getNextNode(final Parent parent,
                                        final Node node) {
            final Iterator<Node> childIterator =
                    parent.getChildrenUnmodifiable().iterator();

            while (childIterator.hasNext()) {
                if (childIterator.next() == node) {
                    return childIterator.hasNext() ? childIterator.next()
                            : null;
                }
            }

            return null;
        }

        private static Node getPreviousNode(final Parent parent,
                                            final Node node) {
            final Iterator<Node> childIterator =
                    parent.getChildrenUnmodifiable().iterator();
            Node lastNode = null;

            while (childIterator.hasNext()) {
                final Node currentNode = childIterator.next();
                if (currentNode == node) {
                    return lastNode;
                }

                lastNode = currentNode;
            }

            return null;
        }
    }
}

其他
常用的一些事件及其处理方式已经讲解完毕,掌握这些已经足够日常开发,至于某些不常用的事件(例如触摸和拖拽相关事件),将不在入门级文章中讲述,后续如果有使用需求,可以阅读相关文档。

Q.E.D.


擅长前端的Java程序员