最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

java - How to prevent edit mode on double-click in JavaFX TableView while keeping single-click edit? - Stack Overflow

programmeradmin1浏览0评论

I have a JavaFX TableView with editable cells where I need:

  • Single click → selects cell (standard behavior)
  • Single click on selected cell → enters edit mode (standard behavior)
  • Double click → selects cell and custom action ONLY (without entering edit mode)

This is the table that shows current and desired behaviors:

Action Current Behavior Desired Behavior
Single-click Selects cell (keep) Selects cell
Single-click selected Enters edit mode (keep) Enters edit mode
Double-click Custom action + edit mode Selects cell + custom action ONLY

I have a JavaFX TableView with editable cells where I need:

  • Single click → selects cell (standard behavior)
  • Single click on selected cell → enters edit mode (standard behavior)
  • Double click → selects cell and custom action ONLY (without entering edit mode)

This is the table that shows current and desired behaviors:

Action Current Behavior Desired Behavior
Single-click Selects cell (keep) Selects cell
Single-click selected Enters edit mode (keep) Enters edit mode
Double-click Custom action + edit mode Selects cell + custom action ONLY

This is my code:

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class NewMain1 extends Application {

    public static class Person {
        private final StringProperty firstName;
        private final StringProperty lastName;

        public Person(String firstName, String lastName) {
            this.firstName = new SimpleStringProperty(firstName);
            this.lastName = new SimpleStringProperty(lastName);
        }

        public StringProperty firstNameProperty() { return firstName; }
        public StringProperty lastNameProperty() { return lastName; }
    }

    @Override
    public void start(Stage primaryStage) {
        TableView<Person> tableView = new TableView<>();
        tableView.setEditable(true);

        TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
        firstNameCol.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
        firstNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
        firstNameCol.setOnEditStart(e -> System.out.println("First Name Edit Start"));

        TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
        lastNameCol.setCellValueFactory(cellData -> cellData.getValue().lastNameProperty());
        lastNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
        lastNameCol.setOnEditStart(e -> System.out.println("Last Name Edit Start"));

        tableView.getColumns().addAll(firstNameCol, lastNameCol);

        ObservableList<Person> data = FXCollections.observableArrayList(
            new Person("John", "Smith"),
            new Person("Emily", "Johnson"),
            new Person("Michael", "Williams"),
            new Person("Sarah", "Brown")
        );
        tableView.setItems(data);

        tableView.setRowFactory(tv -> {
            TableRow<Person> row = new TableRow<>();

            row.setOnMouseClicked(event -> {
                if (event.getClickCount() == 2 && !row.isEmpty()) {
                    Person person = row.getItem();
                    System.out.println("DoubleClick on "
                            + person.firstNameProperty().get() + " " + person.lastNameProperty().get());
                    event.consume();
                }
            });

            return row;
        });

        VBox root = new VBox(tableView);
        Scene scene = new Scene(root, 400, 300);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Could anyone say how to do it?

Share Improve this question edited Mar 30 at 23:42 Sai Dandem 10.2k14 silver badges27 bronze badges asked Mar 30 at 14:53 SilverCubeSilverCube 6403 silver badges12 bronze badges 4
  • Perhaps adding Platform.runLater(row::cancelEdit); or if (tableView.getEditingCell() != null) Platform.runLater(() -> tableView.getEditingCell().cancelEdit()); right after event.consume() will suffice? – VGR Commented Mar 30 at 15:37
  • Have you tried an event filter, instead of using setOnMouseClicked? – James_D Commented Mar 30 at 16:52
  • @James_D I've just tried row.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {... event.consume(); }). It didn't help - same result. – SilverCube Commented Mar 30 at 17:17
  • This does not answer your question, but I have never been a fan of TableView cell edits. I make use of the selected item a lot. From there, I use something like ContextMenu to select things I want to happen, like opening a dialog that allows any data on the row to be updated. For me, this approach led to less headaches. Good luck coding! – SedJ601 Commented Mar 30 at 17:55
Add a comment  | 

3 Answers 3

Reset to default 3

I agree with the comments mentioned by @James_D regarding predicting for next click. By default the single click mouse pressed will do the selection or get to edit mode. If you are desperate to fix this problem, you need to separate the event processing of double click from single click.

You can achieve this by creating some custom events and a custom event dispatcher that can process the events separately.

The below solution involved four steps:

Step#1: Create a custom event and event dispatcher

You can create a custom double pressed event and event dispatcher to separate the events. The general idea is,

  • when we receive a single click event, we start a timeline to execute the event after certain duration.
  • If we receive another event within this duration, we consider this as a double pressed event, and will fire the custom double pressed event and cancel the previous single pressed event. That way you can separate the two events and handle them separately.

The code will be as below. In

/**
 * Custom double pressed mouse event.
 */
interface CustomMouseEvent {
    EventType<MouseEvent> MOUSE_DOUBLE_PRESSED = new EventType<>(MouseEvent.ANY, "MOUSE_DOUBLE_PRESSED");
}

/**
 * Custom EventDispatcher to differentiate from double click and single click.
 */
class DoubleClickEventDispatcher implements EventDispatcher {

    /** Default delay to fire a double click event in milliseconds. */
    private static final long DEFAULT_DOUBLE_CLICK_DELAY = 215;

    /** Default event dispatcher of a node. */
    private final EventDispatcher defaultEventDispatcher;

    /** Timeline for dispatching mouse clicked event. */
    private Timeline singleClickTimeline;

    /**
     * Constructor.
     *
     * @param initial Default event dispatcher of a node
     */
    public DoubleClickEventDispatcher(final EventDispatcher initial) {
        defaultEventDispatcher = initial;
    }

    @Override
    public Event dispatchEvent(final Event event, final EventDispatchChain tail) {
        final EventType<? extends Event> type = event.getEventType();
        if (type == MouseEvent.MOUSE_PRESSED && ((MouseEvent)event).getButton()== MouseButton.PRIMARY) {
            final MouseEvent mouseEvent = (MouseEvent) event;
            final EventTarget eventTarget = event.getTarget();
            // If it is a double click , stop the single click timeline and fire the double pressed event manually
            if (mouseEvent.getClickCount() > 1) {
                if (singleClickTimeline != null) {
                    singleClickTimeline.stop();
                    singleClickTimeline = null;
                    final MouseEvent dblClickedEvent = copy(mouseEvent, CustomMouseEvent.MOUSE_DOUBLE_PRESSED);
                    Event.fireEvent(eventTarget, dblClickedEvent);
                }
                return mouseEvent;
            }

            // If it is single click, start a timeline to fire the single click after a certain duration.
            if (singleClickTimeline == null) {
                final MouseEvent singleClickEvent = copy(mouseEvent, mouseEvent.getEventType());
                singleClickTimeline = new Timeline(new KeyFrame(Duration.millis(DEFAULT_DOUBLE_CLICK_DELAY), e -> {
                    Event.fireEvent(eventTarget, singleClickEvent);
                    // Because we are firing the pressed event, we have to fire the release event, to clear the cached values in Table classes.
                    final MouseEvent releaseEvent = copy(singleClickEvent, MouseEvent.MOUSE_RELEASED);
                    Event.fireEvent(eventTarget, releaseEvent);
                    singleClickTimeline = null;
                }));
                // Start a timeline to see if we get a double click in future.
                singleClickTimeline.play();
                return mouseEvent;
            }
        }
        return defaultEventDispatcher.dispatchEvent(event, tail);
    }

    /**
     * Creates a copy of the provided mouse event type with the mouse event.
     *
     * @param e         MouseEvent
     * @param eventType Event type that need to be created
     * @return New mouse event instance
     */
    private MouseEvent copy(final MouseEvent e, final EventType<? extends MouseEvent> eventType) {
        return new MouseEvent(eventType, e.getSceneX(), e.getSceneY(), e.getScreenX(), e.getScreenY(),
                e.getButton(), e.getClickCount(), e.isShiftDown(), e.isControlDown(), e.isAltDown(),
                e.isMetaDown(), e.isPrimaryButtonDown(), e.isMiddleButtonDown(),
                e.isSecondaryButtonDown(), e.isSynthesized(), e.isPopupTrigger(),
                e.isStillSincePress(), e.getPickResult());
    }
}

Step#2: Set the custom event dispatcher on tableView

Now we have created the dispatcher that separates the two types of events. Set this event dispatcher on the node we are interested in (TableView).

tableView.setEventDispatcher(new DoubleClickEventDispatcher(tableView.getEventDispatcher()));

Step#3: Add the double pressed event on the table row.

row.addEventHandler(CustomMouseEvent.MOUSE_DOUBLE_PRESSED, event -> {
   // your code
});

Step#4: Handle the missing functionality that we loose for not firing the single pressed.

Because we are diverting the single pressed event, we want to handle all the unwanted cases in the double pressed event handler. Particularly here, we need to handle the row selection and cancel the edit (if any)

tableView.getSelectionModel().select(person);
if (tableView.getEditingCell() != null) {
    tableView.edit(-1, null);
}

Check the below demo and code when combining all the above metioned steps. I hope this can give you some direction on how to approach this requirement.

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.*;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class CustomEventClickOnTableCellDemo extends Application {

    public static class Person {
        private final StringProperty firstName;
        private final StringProperty lastName;

        public Person(String firstName, String lastName) {
            this.firstName = new SimpleStringProperty(firstName);
            this.lastName = new SimpleStringProperty(lastName);
        }

        public StringProperty firstNameProperty() {
            return firstName;
        }

        public StringProperty lastNameProperty() {
            return lastName;
        }
    }

    @Override
    public void start(Stage primaryStage) {
        TableView<Person> tableView = new TableView<>();
        tableView.setEditable(true);

        TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
        firstNameCol.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
        firstNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
        firstNameCol.setOnEditStart(e -> System.out.println("First Name Edit Start"));

        TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
        lastNameCol.setCellValueFactory(cellData -> cellData.getValue().lastNameProperty());
        lastNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
        lastNameCol.setOnEditStart(e -> System.out.println("Last Name Edit Start"));

        tableView.getColumns().addAll(firstNameCol, lastNameCol);

        ObservableList<Person> data = FXCollections.observableArrayList(
                new Person("John", "Smith"),
                new Person("Emily", "Johnson"),
                new Person("Michael", "Williams"),
                new Person("Sarah", "Brown")
        );
        tableView.setItems(data);
        /* STEP#2 : Set the custom event dispatcher to tableView */
        tableView.setEventDispatcher(new DoubleClickEventDispatcher(tableView.getEventDispatcher()));

        tableView.setRowFactory(tv -> {
            TableRow<Person> row = new TableRow<>();
            /* STEP#3 : Add custom mouse double pressed event. */
            row.addEventHandler(CustomMouseEvent.MOUSE_DOUBLE_PRESSED, event -> {
                if (!row.isEmpty()) {
                    Person person = row.getItem();
                    System.out.println("DoubleClick on "
                            + person.firstNameProperty().get() + " " + person.lastNameProperty().get());

                    /* STEP#4 : On double-click, select the row and cancel the current editing(if any), as we are not processing the single click. */
                    tableView.getSelectionModel().select(person);
                    if (tableView.getEditingCell() != null) {
                        tableView.edit(-1, null);
                    }
                    event.consume();
                }
            });

            return row;
        });

        VBox root = new VBox(tableView);
        Scene scene = new Scene(root, 400, 300);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

    /* STEP#1 : Create custom events and event dispatcher to separate single and double click events. */

    /**
     * Custom double pressed mouse event.
     */
    interface CustomMouseEvent {
        EventType<MouseEvent> MOUSE_DOUBLE_PRESSED = new EventType<>(MouseEvent.ANY, "MOUSE_DOUBLE_PRESSED");
    }

    /**
     * Custom EventDispatcher to differentiate from double click and single click.
     */
    class DoubleClickEventDispatcher implements EventDispatcher {

        /**
         * Default delay to fire a double click event in milliseconds.
         */
        private static final long DEFAULT_DOUBLE_CLICK_DELAY = 215;

        /**
         * Default event dispatcher of a node.
         */
        private final EventDispatcher defaultEventDispatcher;

        /**
         * Timeline for dispatching mouse clicked event.
         */
        private Timeline singleClickTimeline;

        /**
         * Constructor.
         *
         * @param initial Default event dispatcher of a node
         */
        public DoubleClickEventDispatcher(final EventDispatcher initial) {
            defaultEventDispatcher = initial;
        }

        @Override
        public Event dispatchEvent(final Event event, final EventDispatchChain tail) {
            final EventType<? extends Event> type = event.getEventType();
            if (type == MouseEvent.MOUSE_PRESSED && ((MouseEvent)event).getButton()== MouseButton.PRIMARY) {
                final MouseEvent mouseEvent = (MouseEvent) event;
                final EventTarget eventTarget = event.getTarget();
                // If it is a double click , stop the single click timeline and fire the double pressed event manually
                if (mouseEvent.getClickCount() > 1) {
                    if (singleClickTimeline != null) {
                        singleClickTimeline.stop();
                        singleClickTimeline = null;
                        final MouseEvent dblClickedEvent = copy(mouseEvent, CustomMouseEvent.MOUSE_DOUBLE_PRESSED);
                        Event.fireEvent(eventTarget, dblClickedEvent);
                    }
                    return mouseEvent;
                }

                // If it is single click, start a timeline to fire the single click after a certain duration.
                if (singleClickTimeline == null) {
                    final MouseEvent singleClickEvent = copy(mouseEvent, mouseEvent.getEventType());
                    singleClickTimeline = new Timeline(new KeyFrame(Duration.millis(DEFAULT_DOUBLE_CLICK_DELAY), e -> {
                        Event.fireEvent(eventTarget, singleClickEvent);
                        // Because we are firing the pressed event, we have to fire the release event, to clear the cached values in Table classes.
                        final MouseEvent releaseEvent = copy(singleClickEvent, MouseEvent.MOUSE_RELEASED);
                        Event.fireEvent(eventTarget, releaseEvent);
                        singleClickTimeline = null;
                    }));
                    // Start a timeline to see if we get a double click in future.
                    singleClickTimeline.play();
                    return mouseEvent;
                }
            }
            return defaultEventDispatcher.dispatchEvent(event, tail);
        }

        /**
         * Creates a copy of the provided mouse event type with the mouse event.
         *
         * @param e         MouseEvent
         * @param eventType Event type that need to be created
         * @return New mouse event instance
         */
        private MouseEvent copy(final MouseEvent e, final EventType<? extends MouseEvent> eventType) {
            return new MouseEvent(eventType, e.getSceneX(), e.getSceneY(), e.getScreenX(), e.getScreenY(),
                    e.getButton(), e.getClickCount(), e.isShiftDown(), e.isControlDown(), e.isAltDown(),
                    e.isMetaDown(), e.isPrimaryButtonDown(), e.isMiddleButtonDown(),
                    e.isSecondaryButtonDown(), e.isSynthesized(), e.isPopupTrigger(),
                    e.isStillSincePress(), e.getPickResult());
        }
    }
}

This answer comes with a big caveat: modifying the behavior of JavaFX controls is really not well supported. Doing so typically relies on undocumented implementation details of the control, which are subject to change in later releases. Hence implementing any such functionality is really not very robust.

That said, the current table cell implementation appears to manage selection and editing via listeners for mouse pressed and released events. So consuming a mouse pressed event (note: mouse pressed, not mouse clicked) in an event filter (note: filter not handler, so it is invoked before the cell's internal handlers) will prevent the default selection and editing behavior. The following (as a replacement for your existing row.setOnMouseClicked(...)) seems to result in the functionality you specify:

            row.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
                if (event.getClickCount() == 2 && !row.isEmpty()) {
                    Person person = row.getItem();
                    System.out.println("DoubleClick on "
                            + person.firstNameProperty().get() + " " + person.lastNameProperty().get());
                    event.consume();
                }
            });

I would record the time of the first click in a variable. Then, compare it to the time of the next click before deciding what to do. If the second click is within X milliseconds, then consider it a "double-click" and execute the special action. Else, consider the second click the trigger for entering edit mode.

OK, what happens if someone clicks a field, which selects it, and then double-clicks? With this scenario, it would go into Edit mode and then the second click of the double would do whatever a click does when editing that field.

If that is a problem, one possibility is to set the "edit" click to wait X millis before going to edit mode, giving time for a possible "double click" to assert itself.

I've done this sort of thing with Java, using Timer, and JavaScript, using setTimeout. I can't recall if JavaFX has a preferred timer.

For similar logic, check the topic of "debouncing". A quick search gave me a JS example here What is debouncing? but I'm only seeing a couple articles in Medium that show debouncing using Java or JavaFX. I haven't clicked on them because I am conserving my Medium freebies.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论