JavaFX:鼠标事件和状态管理

JavaFX:鼠标事件和状态管理

JavaFX的几乎所有组件都支持鼠标事件,而鼠标事件也有不少种类,这其中就存在一些种类的事件相互影响和重叠,当然,多数时候这没有什么问题,但是如果需要更精细的事件处理,直接使用本身的EventHandler似乎就不太够了。

例如,它存在这样一个问题,鼠标事件总会按照如此的流程触发——MouseDown,MouseRelease,MouseClick——而如果需要在前面处理拖拽,在Click处理选择,那么,麻烦的事情就来了,无论什么时候,是否进行了拖拽,最终都会触发一个Click。

所以,得有另外的办法来处理这种情况,不能直接监听并处理这些事件。

状态的流转

鼠标按下,鼠标松开,鼠标点击,鼠标移动,最开始我把它们作为State,并且构造了如下的逻辑:

/**
 * 鼠标事件状态管理。
 */
public class MouseEventManager {
    private MouseState currentState = MouseState.IDLE;
    private Map<Pair<MouseState,MouseState>,EventHandler<MouseEvent>> stateHandleMap = new HashMap<>();
    //监听鼠标事件
    public void initOwner(Node parent) {
        parent.setOnMousePressed(e -> {
           stateChanged(MouseState.MOUSE_DOWN,e);
        });
        parent.setOnMouseReleased(e -> {
            stateChanged(MouseState.MOUSE_UP,e);
        });
        parent.setOnMouseClicked(e -> {
            stateChanged(MouseState.MOUSE_CLICK,e);
        });
        parent.setOnMouseMoved(e -> {
            stateChanged(MouseState.MOUSE_MOVE,e);
        });
    }
    // 注册状态转换时的处理方法
    public void registerHandler(MouseState before, MouseState after, EventHandler<MouseEvent> handler) {
        Pair<MouseState,MouseState> statePair = null;
        for (Pair<MouseState,MouseState> pair: stateHandleMap.keySet()) {
            if (pair.getKey().equals(before) && pair.getValue().equals(after)) {
                statePair = pair;
                break;
            }
        }
        if (statePair == null) {
            statePair = new Pair<>(before,after);
        }
        stateHandleMap.put(statePair,handler);
    }
    // 触发状态转换
    private void stateChanged(MouseState nextState,MouseEvent event) {
        if (nextState.equals(currentState)) {
            return;
        }
        Pair<MouseState,MouseState> statePair = null;
        for (Pair<MouseState,MouseState> pair: stateHandleMap.keySet()) {
            if (pair.getKey().equals(currentState) && pair.getValue().equals(nextState)) {
                statePair = pair;
                break;
            }
        }
        System.out.println(currentState.name() + " -> " + nextState.name());
        if (statePair == null) {
            currentState = nextState;
            return;
        }
        stateHandleMap.get(statePair).handle(event);
    }
}

但是很快我就意识到一个问题——无论发生了什么,这个状态流转的过程都不会改变,而且都将会是MouseDown-MouseUp-Click这样的过程,状态机当然不能是这种的

那么,问题到底在哪里呢?

最终给我启发的是IDLE,也就是空闲,它和其他的几种完全不同,我意识到类似这种东西才能称得上是状态,它必须是持续性的, 而状态的流转则是指的这种可持续性的状态在不同的事件的驱动下发生变化的过程

一旦事件发生,就应该执行一点什么,通过这个来得到接下来的新状态,而执行并且获得新状态这一部分,就是对一个过程进行精细控制的实现方式 ,接下来,我将MouseState重命名为MouseAction,并且增加了另一个枚举类型作为真正的State,并且得到了这样的EventManager类:

/**
 * 鼠标事件状态机。
 */
public class MouseEventManager {
    /**
     * 当前状态,
     * 他表示一个现状,当前正处于的情景/状况/持续中的状态,
     * 之所以我特别强调这些,是因为这个“状态”得区别于“事件”。
     */
    private MouseState currentState = MouseState.IDLE;
    private Map<Pair<MouseAction, MouseState>,MouseStateHandler> stateHandleMap = new HashMap<>();
    public void initOwner(Node parent) {
        parent.setOnMousePressed(e -> {
            trigger(MouseAction.MOUSE_DOWN,e);
        });
        parent.setOnMouseReleased(e -> {
            trigger(MouseAction.MOUSE_UP,e);
        });
        parent.setOnMouseClicked(e -> {
            trigger(MouseAction.MOUSE_CLICK,e);
        });
        parent.setOnMouseMoved(e -> {
            trigger(MouseAction.MOUSE_MOVE,e);
        });
    }
    /**
     * 注册一个状态处理方法
     * @param action 发生了什么事件
     * @param state 在什么情况下发生
     * @param handler 应该怎么办,新的状态是什么
     */
    public void registerHandler(MouseAction action, MouseState state, MouseStateHandler handler) {
        Pair<MouseAction, MouseState> statePair = null;
        for (Pair<MouseAction, MouseState> pair: stateHandleMap.keySet()) {
            if (pair.getKey().equals(action) && pair.getValue().equals(state)) {
                statePair = pair;
                break;
            }
        }
        if (statePair == null) {
            statePair = new Pair<>(action,state);
        }
        stateHandleMap.put(statePair,handler);
    }
    /**
     * 在当前的状态下事件发生。
     *
     * @param action 发生的事件。
     * @param event MouseEvent,鼠标事件的对象
     */
    private void trigger(MouseAction action, MouseEvent event) {
        Pair<MouseAction, MouseState> statePair = null;
        for (Pair<MouseAction, MouseState> pair: stateHandleMap.keySet()) {
            // 检索在当前的状态的当前事件的Key
            // 这个Key通常对应了一个当前“状态”时,本“事件”发生的时候的处理方法。
            if (pair.getKey().equals(action) && pair.getValue().equals(currentState)) {
                statePair = pair;
                break;
            }
        }
        if (statePair == null) {
            // 本“状态”下发生此“事件”无需处理的情况。
            return;
        }
        // 调用处理方法,并且更新当前状态。
        // 处理方法可能会返回一个不同的状态。
        currentState = stateHandleMap.get(statePair).handle(event);
    }
}

接下来的测试中,一切变得非常顺利,EventHandler变成了State的转换,相互之间的干扰也就消除了,单纯看上述的逻辑,似乎非常简单,甚至简易,性能也不怎么样,之所以我把他单独拿出来,是因为状态机或者说状态流转这一个东西让我困惑了相当长的时间,就像我在一开始犯的错误那样,把事件和状态混淆,导致这个东西只能监听而不能做出变化,事实上,它们不是一个东西。

所以状态机应该描述了这样的过程:“在特定的状态下,发生了特定的事件,进行特定的处理之后维持状态或者进入一个新状态的过程”,所谓状态,既是可维持一段时间或者长期维持一个现状而事件则是在某一个状态的基础上发生的而状态的转变也就发生在“对处于某一状态的事件进行处理”的过程中,作为这一过程的结果成为新的状态

Fantastic Soft

风铃之书是个人的工作和生活的总结和分享的站点,欢迎来访和留言,有时也会提供自家软件的发布版本和开源项目。