【问题标题】:TableView row selection on Mouse Middle Button click鼠标中键单击时的 TableView 行选择
【发布时间】:2019-07-11 08:39:59
【问题描述】:

目前在 TableView 中,当我们单击鼠标中键时不会发生行选择。如果我们右键或左键单击,则选择该行。我正在尝试具有在单击中间按钮时选择行的功能。

我已经知道在表格行工厂中包含一个事件处理程序可以解决这个问题。但是我有一个自定义表格视图,其中包含许多适用于我的应用程序的自定义功能。这个自定义的 TableView 在我的应用程序中被广泛使用。我的主要问题是,我不能要求每个表格行工厂都包含此修复程序。

我正在寻找一种在更高级别(可能在 TableView 级别)上执行此操作的方法,以便行工厂不需要关心。

非常感谢任何想法/帮助。

下面是我想要实现的一个简单示例。

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;

public class TableRowSelectionOnMiddleButtonDemo extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        ObservableList<Person> persons = FXCollections.observableArrayList();
        for (int i = 0; i < 4; i++) {
            persons.add(new Person("First name" + i, "Last Name" + i));
        }

        CustomTableView<Person> tableView = new CustomTableView<>();
        TableColumn<Person, String> fnCol = new TableColumn<>("First Name");
        fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());

        TableColumn<Person, String> lnCol = new TableColumn<>("Last Name");
        lnCol.setCellValueFactory(param -> param.getValue().lastNameProperty());

        tableView.getColumns().addAll(fnCol, lnCol);
        tableView.getItems().addAll(persons);

        tableView.setRowFactory(new Callback<TableView<Person>, TableRow<Person>>() {
            @Override
            public TableRow<Person> call(TableView<Person> param) {
                return new TableRow<Person>(){
                    {
                        /* This will fix my issue, but I dont want each tableView rowfactory to set this behavior.*/
//                        addEventHandler(MouseEvent.MOUSE_PRESSED,e->{
//                            getTableView().getSelectionModel().select(getItem());
//                        });
                    }
                    @Override
                    protected void updateItem(Person item, boolean empty) {
                        super.updateItem(item, empty);
                    }
                };
            }
        });

        VBox sp = new VBox();
        sp.setAlignment(Pos.TOP_LEFT);
        sp.getChildren().addAll(tableView);
        Scene sc = new Scene(sp);
        primaryStage.setScene(sc);
        primaryStage.show();
    }

    public static void main(String... a) {
        Application.launch(a);
    }

    /**
     * My custom tableView.
     * @param <S>
     */
    class CustomTableView<S> extends TableView<S> {
        public CustomTableView() {
            // A lot of custom behavior is included to this TableView.
        }
    }

    class Person {
        private StringProperty firstName = new SimpleStringProperty();
        private StringProperty lastName = new SimpleStringProperty();

        public Person(String fn, String ln) {
            setFirstName(fn);
            setLastName(ln);
        }

        public String getFirstName() {
            return firstName.get();
        }

        public StringProperty firstNameProperty() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName.set(firstName);
        }

        public String getLastName() {
            return lastName.get();
        }

        public StringProperty lastNameProperty() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName.set(lastName);
        }

    }
}

【问题讨论】:

  • 使用自定义rowFactory 几乎是在TableView 级别处理这个问题。执行此操作的任何其他方式至少要复杂两倍,并且其中大多数方式更难维护。如果您害怕失去使用自定义rowFactory 添加的其他一些功能,那么调整由另一个行工厂创建的行非常容易。使用自定义 rowFactory 是解决此问题的最佳方法,我无法找到不使用此方法的理由。(您不需要为每个 TableView 一遍又一遍地编写此代码您创建的应该获得该功能。)
  • @fabian 感谢您的建议。如果我能用更少的努力调整,我会再试一次,否则会听从你的建议。

标签: javafx tableview javafx-8


【解决方案1】:

任何全局自定义(每个控件)功能/行为的入口点都是控件的外观。更具体地说:用户交互由皮肤的行为控制 - 不幸的是,它没有进入公共范围,因此它的访问/修改需要变脏(如访问内部类/方法,部分通过反射)。

假设允许此类访问,将鼠标交互调整为与 TableCell 的主按钮一样的中间反应方式的步骤是

  • 实现自定义 TableCellSkin
  • 反思性地访问其行为
  • 在行为的 inputMap 中找到 mousePressed 处理程序
  • 将原始处理程序替换为自定义处理程序,该处理程序将来自中间按钮的 mouseEvent 替换为来自主按钮的 mouseEvent
  • 通过 css 将自定义 TableCellSkin 设为默认

注意:表格右侧空闲空间中负责处理mouseEvents的TableRowSkin没有分离出中间按钮,所以目前无事可做。如果将来发生变化,只需应用与表格单元格相同的技巧即可。

例子:

public class TableRowCustomMouse extends Application {

    public static class CustomMouseTableCellSkin<T, S> extends TableCellSkin<T, S> {

        EventHandler<MouseEvent> original;

        public CustomMouseTableCellSkin(TableCell<T, S> control) {
            super(control);
            adjustMouseBehavior();
        }

        private void adjustMouseBehavior() {
            // dirty: reflective access to behavior, use your custom reflective accessor
            TableCellBehavior<T, S> behavior = 
                (TableCellBehavior<T, S>) FXUtils.invokeGetFieldValue(TableCellSkin.class, this, "behavior");
            InputMap<TableCell<T, S>> inputMap = behavior.getInputMap();
            ObservableList<Mapping<?>> mappings = inputMap.getMappings();
            List<Mapping<?>> pressedMapping = mappings.stream()
                    .filter(mapping -> mapping.getEventType() == MouseEvent.MOUSE_PRESSED)
                    .collect(Collectors.toList());
            if (pressedMapping.size() == 1) {
                Mapping<?> originalMapping = pressedMapping.get(0);
                original = (EventHandler<MouseEvent>) pressedMapping.get(0).getEventHandler();
                if (original != null) {
                    EventHandler<MouseEvent> replaced = this::replaceMouseEvent;
                    mappings.remove(originalMapping);
                    mappings.add(new MouseMapping(MouseEvent.MOUSE_PRESSED, replaced));
                }
            }
        }

        private void replaceMouseEvent(MouseEvent e) {
            MouseEvent replaced = e;
            if (e.isMiddleButtonDown()) {
                replaced = new MouseEvent(e.getSource(), e.getTarget(), e.getEventType(),
                    e.getX(), e.getY(),
                    e.getScreenX(), e.getScreenY(),
                    MouseButton.PRIMARY,
                    e.getClickCount(),
                    e.isShiftDown(), e.isControlDown(), e.isAltDown(), e.isMetaDown(),
                    true, false, false,
                    e.isSynthesized(), e.isPopupTrigger(), e.isStillSincePress(),
                    null
                    );
            }
            original.handle(replaced);
        }

    }
    private Parent createContent() {
        TableView<Person> table = new TableView<>(Person.persons());
        TableColumn<Person, String> first = new TableColumn("First Name");
        first.setCellValueFactory(cc -> cc.getValue().firstNameProperty());
        TableColumn<Person, String> last = new TableColumn<>("Last Name");
        last.setCellValueFactory(cc -> cc.getValue().lastNameProperty());
        table.getColumns().addAll(first, last);
        BorderPane content = new BorderPane(table);
        return content;
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        // load the default css 
        stage.getScene().getStylesheets()
            .add(getClass().getResource("customtablecellskin.css").toExternalForm());
        stage.setTitle(FXUtils.version());
        stage.show();
    }

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

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(TableRowCustomMouse.class.getName());

}

TableCell 自定义皮肤的 css:

.table-cell {
    -fx-skin: "<yourpackage>.TableRowCustomMouse$CustomMouseTableCellSkin";
}

【讨论】:

  • 感谢您告诉我替代方法。事实上,我正是在寻找这种方法来解决这个问题,比如在中间点击时调用主点击实现(没有反射)。但看起来这是不可避免的。我无法运行您的演示,因为我缺少一些自定义库/实现。将尝试修复所有编译错误并决定方法。这帮助我对 TableCellBehavior 有了更多的了解。再次感谢您的宝贵时间:)
【解决方案2】:

接受@kleopatra 的方法作为答案。然而,我的问题的解决方案与@kleopatra 的回答有点不同。但核心思想还是一样的。

我用的方法覆盖了TableCellBehavior的doSelect方法

    @Override
    protected void doSelect(double x, double y, MouseButton button, int clickCount, boolean shiftDown, boolean shortcutDown) {
        MouseButton btn = button;
        if (button == MouseButton.MIDDLE) {
            btn = MouseButton.PRIMARY;
        }
        super.doSelect(x, y, btn, clickCount, shiftDown, shortcutDown);
    }

以下是解决我的问题的工作演示:

DemoClass:

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;

public class TableRowSelectionOnMiddleButtonDemo extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        ObservableList<Person> persons = FXCollections.observableArrayList();
        for (int i = 0; i < 4; i++) {
            persons.add(new Person("First name" + i, "Last Name" + i));
        }

        CustomTableView<Person> tableView = new CustomTableView<>();
        TableColumn<Person, String> fnCol = new TableColumn<>("First Name");
        fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());

        TableColumn<Person, String> lnCol = new TableColumn<>("Last Name");
        lnCol.setCellValueFactory(param -> param.getValue().lastNameProperty());

        tableView.getColumns().addAll(fnCol, lnCol);
        tableView.getItems().addAll(persons);

        tableView.setRowFactory(new Callback<TableView<Person>, TableRow<Person>>() {
            @Override
            public TableRow<Person> call(TableView<Person> param) {
                return new TableRow<Person>() {
                    {
                        /* This will fix my issue, but I dont want each tableView rowfactory to set this behavior.*/
//                        addEventHandler(MouseEvent.MOUSE_PRESSED,e->{
//                            getTableView().getSelectionModel().select(getItem());
//                        });
                    }

                    @Override
                    protected void updateItem(Person item, boolean empty) {
                        super.updateItem(item, empty);
                    }
                };
            }
        });

        VBox sp = new VBox();
        sp.setAlignment(Pos.TOP_LEFT);
        sp.getChildren().addAll(tableView);
        Scene sc = new Scene(sp);
        sc.getStylesheets().add(this.getClass().getResource("tableRowSelectionOnMiddleButton.css").toExternalForm());
        primaryStage.setScene(sc);
        primaryStage.show();
    }

    public static void main(String... a) {
        Application.launch(a);
    }

    /**
     * My custom tableView.
     *
     * @param <S>
     */
    class CustomTableView<S> extends TableView<S> {
        public CustomTableView() {
            // A lot of custom behavior is included to this TableView.
        }
    }


    class Person {
        private StringProperty firstName = new SimpleStringProperty();
        private StringProperty lastName = new SimpleStringProperty();

        public Person(String fn, String ln) {
            setFirstName(fn);
            setLastName(ln);
        }

        public String getFirstName() {
            return firstName.get();
        }

        public StringProperty firstNameProperty() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName.set(firstName);
        }

        public String getLastName() {
            return lastName.get();
        }

        public StringProperty lastNameProperty() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName.set(lastName);
        }

    }
}

CustomTableCellSkin 类:

import com.sun.javafx.scene.control.behavior.TableCellBehavior;
import com.sun.javafx.scene.control.skin.TableCellSkinBase;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.input.MouseButton;

public class CustomTableCellSkin<S, T> extends TableCellSkinBase<TableCell<S, T>, TableCellBehavior<S, T>> {

    private final TableColumn<S, T> tableColumn;

    public CustomTableCellSkin(TableCell<S, T> tableCell) {
        super(tableCell, new CustomTableCellBehavior<S, T>(tableCell));
        this.tableColumn = tableCell.getTableColumn();
        super.init(tableCell);
    }

    @Override
    protected BooleanProperty columnVisibleProperty() {
        return tableColumn.visibleProperty();
    }

    @Override
    protected ReadOnlyDoubleProperty columnWidthProperty() {
        return tableColumn.widthProperty();
    }
}

class CustomTableCellBehavior<S, T> extends TableCellBehavior<S, T> {
    public CustomTableCellBehavior(TableCell<S, T> control) {
        super(control);
    }

    @Override
    protected void doSelect(double x, double y, MouseButton button, int clickCount, boolean shiftDown, boolean shortcutDown) {
        MouseButton btn = button;
        if (button == MouseButton.MIDDLE) {
            btn = MouseButton.PRIMARY;
        }
        super.doSelect(x, y, btn, clickCount, shiftDown, shortcutDown);
    }
}

tableRowSelectionOnMiddleButton.css

.table-cell {
    -fx-skin: "<package>.CustomTableCellSkin";
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-01-12
    • 2020-02-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-18
    相关资源
    最近更新 更多