JavaFX8 - 定制的 EditCell class - 如何禁用 TableView 的鼠标点击,同时仍然允许在正在编辑的 TableCell 内点击?

JavaFX8 - customised EditCell class - how to disable mouse clicks for a TableView while still allowing clicks inside a TableCell that's being edited?

我正在编写一个将有很多数据输入的应用程序 windows,每个数据输入都有一个可编辑的 TableView。

我正在使用用户 James_D 非常有用的 EditCell 代码 https://gist.github.com/james-d/be5bbd6255a4640a5357 作为 TableCell 编辑的基础,并将其扩展为包括数字和日期数据类型。

我正在验证输入的数据。如果没有错误,我会让用户通过单击另一个单元格或通过按 Tab 键或按 Shift+Tab 键移出单元格来离开该单元格。如果有错误,我不会让用户离开单元格,直到错误被更正。

我的代码与一件事分开工作。

我在 TableView 上使用鼠标事件处理程序来检测用户何时试图单击离开单元格。如果出现错误,我会使用点击来阻止焦点离开单元格。

那部分工作正常。

但是,处理程序也会消耗点击 inside 正在编辑的单元格,因此如果用户想要点击以将光标定位在相关位置以更正错误,他们可以't.

有没有办法解决这个问题。允许在正在编辑的单元格内单击,同时在表格视图级别禁用单击?

我也尝试过在 TableView 级别使用 setMouseTransparent 而不是消耗点击次数,但同样的事情发生了。

这是我的代码的摘录。

我在 FXML 控制器超类型中声明了鼠标事件处理程序。 dataEntryError 标志在应用级别声明。

import static ztestform.ZTestForm.dataEntryError;
//...
public static EventHandler<MouseEvent> tvMousePressedHandler;
//...
public void defineMouseEventHandler() {
    tvMousePressedHandler = (MouseEvent event) -> {
        if ( dataEntryError ) event.consume();
    };   
}

我将鼠标事件处理程序添加到每个相关 FXML 控制器中的 TableView。

import static ztestform.ZTestForm.dataEntryError;
//...
private void initialiseTableView() {
    //...
    defineMouseEventHandler();
    tvTestModel.addEventFilter(MouseEvent.MOUSE_PRESSED, tvMousePressedHandler);
    //...
}

根据 James_D 的示例,我有一个 EditCell class,它由 TableColumns 上的单元格工厂实例化。它管理单元格中的开始编辑以及根据需要提交或取消编辑。

在 EditCell 中,我使用单元格的 textProperty() 上的更改侦听器捕获数据输入错误。如果出现错误,我会添加 TableView 鼠标事件处理程序来处理单击。如果没有错误,则删除鼠标事件处理程序。

textField.textProperty().addListener((ObservableValue<? extends String> observable, String oldValue, String newValue) -> {
    getTableView().removeEventFilter(MouseEvent.MOUSE_PRESSED, controllerRef.tvMousePressedHandler);
    if ( isDataValid(classType, newValue) ) {
        dataEntryError = false;
        textField.setStyle("");
        DAOGenUtil.clearSystemMessage(controllerRef);
    } else {
        dataEntryError = true;
        textField.setStyle("-fx-background-color: pink;");
        displayErrorMessage(classType, controllerRef);
        getTableView().addEventFilter(MouseEvent.MOUSE_PRESSED, controllerRef.tvMousePressedHandler);
    }
});        

在这里的某个地方我想说 "allow clicks in textField even though they're consumed at the TableView level" 但不知道该怎么做。

有人能帮帮我吗?我已经坚持了四天了。 :-(

我正在使用 JavaFX8、NetBeans 8.2 和 Scene Builder 8.3。

如果您需要查看,这里是我扩展的 EditCell class 的完整代码。感谢 James_D 发布原始代码,也感谢 Whosebug 上非常聪明的人回答问题;这些解决方案对我来说是宝贵的知识来源!

package ztestform;

import fxmlcontrollers.FXMLControllerSuperType;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import javafx.beans.value.ObservableValue;
import javafx.event.Event;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.util.StringConverter;
import javafx.util.converter.DoubleStringConverter;
import javafx.util.converter.LocalDateStringConverter;
import javafx.util.converter.LongStringConverter;
import static ztestform.ZTestForm.COLOUR_DARK_RED;
import static ztestform.ZTestForm.FORMAT_DATE_DISPLAY;
import static ztestform.ZTestForm.FORMAT_DOUBLE_FOUR_DECIMALS;
import static ztestform.ZTestForm.FORMAT_INTEGER;
import static ztestform.ZTestForm.dataEntryError;

public class EditCell<S, T> extends TableCell<S, T> {

    private final TextField textField = new TextField();

    //Converter for converting the text in the text field to the user type, and vice-versa:
    private final StringConverter<T> converter ;

    public static DAOGenUtil DAOGenUtil = new DAOGenUtil();
    private final Class<T> classType = null;

    public EditCell(StringConverter<T> converter, Class<T> classType, FXMLControllerSuperType controllerRef) {

        this.converter = converter ;

        itemProperty().addListener((obx, oldItem, newItem) -> {
            if (newItem == null) {
                setText(null);
            } else {
                setText(converter.toString(newItem));
            }
        });
        setGraphic(textField);
        setContentDisplay(ContentDisplay.TEXT_ONLY);

//*******************************************************************************************************************    
//If the user hits ENTER and there are no data entry errors, commit the edit

        textField.setOnAction(evt -> {
            if ( ! dataEntryError ) {
                commitEdit(this.converter.fromString(textField.getText()));
            }
        });

//*******************************************************************************************************************    
//If the cell loses focus and there are no data entry errors, commit the edit

        textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
            if (! isNowFocused && ! dataEntryError ) {
                commitEdit(this.converter.fromString(textField.getText()));
            }
        });

//*******************************************************************************************************************    
//Validate data as it's entered

        textField.textProperty().addListener((ObservableValue<? extends String> observable, String oldValue, String newValue) -> {

            getTableView().removeEventFilter(MouseEvent.MOUSE_PRESSED, controllerRef.tvMousePressedHandler);

            if ( isDataValid(classType, newValue) ) {
                dataEntryError = false;
                textField.setStyle("");
                DAOGenUtil.clearSystemMessage(controllerRef);
            } else {
                dataEntryError = true;
                textField.setStyle("-fx-background-color: pink;");
                displayErrorMessage(classType, controllerRef);
                getTableView().addEventFilter(MouseEvent.MOUSE_PRESSED, controllerRef.tvMousePressedHandler);
            }

        });        

//*******************************************************************************************************************    
//Trap and process ESCAPE, TAB and SHIFT+TAB

        textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {

            TablePosition<S, ?> pos = getTableView().getFocusModel().getFocusedCell();
            int maximumVisibleColumnNumber = DAOGenUtil.getMaximumVisibleColumnNumber(getTableView());

            if (event.getCode() == KeyCode.ESCAPE) {

                textField.setText(converter.toString(getItem()));
                cancelEdit();
                event.consume();
                dataEntryError = false;
                DAOGenUtil.clearSystemMessage(controllerRef);
                getTableView().setMouseTransparent(false);

            } else if ( event.isShiftDown() && event.getCode() == KeyCode.TAB ) {

                if ( dataEntryError ) {
                    event.consume();
                } else {
                    getTableView().setMouseTransparent(false);
                    getTableView().getSelectionModel().selectLeftCell();
                    if ( pos.getColumn() == 0 ) {
                        //We're at the start of a row so position to the end of the previous row
                        getTableView().getSelectionModel().select(pos.getRow()-1, getTableView().getColumns().get(maximumVisibleColumnNumber));
                        event.consume();
                    }
                }

            } else if ( event.getCode() == KeyCode.TAB ) {

                if ( dataEntryError ) {
                    event.consume();
                } else {
                    getTableView().setMouseTransparent(false);
                    getTableView().getSelectionModel().selectRightCell();
                    if ( pos.getColumn() == maximumVisibleColumnNumber ) {
                        //We're at the end of a row so position to the start of the next row
                        getTableView().getSelectionModel().select(pos.getRow()+1, getTableView().getColumns().get(0));
                        event.consume();
                    }
                }

            }

        });

    }

//*******************************************************************************************************************    
//Create EditCells for String data types

    public static final StringConverter<String> IDENTITY_CONVERTER_STRING = new StringConverter<String>() {

        @Override
        public String toString(String object) {
            return object;
        }

        @Override
        public String fromString(String string) {
            return string;
        }

    };

    public static <S> EditCell<S, String> createStringEditCell(FXMLControllerSuperType controllerRef) {
        return new EditCell<S, String>(IDENTITY_CONVERTER_STRING, String.class, controllerRef);
    }

//*******************************************************************************************************************    
//Create EditCells for Long data types

    public static final LongStringConverter IDENTITY_CONVERTER_LONG = new LongStringConverter() {

        public String toString(Long object) {

            String object2 = DAOGenUtil.formatValue(FORMAT_INTEGER, Long.toString(object));
            return ( object == null ? "0" : object2 );

        }

        public Long fromString(String object) {

            Long object3 = Long.parseLong(object.replaceAll(",",""));
            return ( object.isEmpty() ? 0 : object3 );

        }

    };

    public static <S> EditCell<S, Long> createLongEditCell(FXMLControllerSuperType controllerRef) {
        return new EditCell<S, Long>(IDENTITY_CONVERTER_LONG, Long.class, controllerRef);
    }

//*******************************************************************************************************************    
//Create EditCells for Double data types

    public static final DoubleStringConverter IDENTITY_CONVERTER_DOUBLE = new DoubleStringConverter() {

        public String toString(Double object) {

            String object2 = DAOGenUtil.formatValue(FORMAT_DOUBLE_FOUR_DECIMALS, Double.toString(object));
            return ( object == null ? "0" : object2 );

        }

        public Double fromString(String object) {

            Double object3 = Double.parseDouble(object.replaceAll(",",""));
            return ( object.isEmpty() ? 0 : object3 );

        }

    };

    public static <S> EditCell<S, Double> createDoubleEditCell(FXMLControllerSuperType controllerRef) {
        return new EditCell<S, Double>(IDENTITY_CONVERTER_DOUBLE, Double.class, controllerRef);
    }

//*******************************************************************************************************************    
//Create EditCells for LocalDate data types

    public static final LocalDateStringConverter IDENTITY_CONVERTER_LOCAL_DATE = new LocalDateStringConverter() {

        public String toString(LocalDate object) {

            DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(FORMAT_DATE_DISPLAY);
            String object2 = dateFormatter.format( (TemporalAccessor) object);
            return ( object == null ? "0" : object2 );

        }

        public LocalDate fromString(String object) {

            DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("d/M/yyyy");
            LocalDate object3 = LocalDate.parse(object, dateFormatter);

            return object.isEmpty() ? null : object3;

        }

    };

    public static <S> EditCell<S, LocalDate> createLocalDateEditCell(FXMLControllerSuperType controllerRef) {
        return new EditCell<S, LocalDate>(IDENTITY_CONVERTER_LOCAL_DATE, LocalDate.class, controllerRef);
    }

//*******************************************************************************************************************    
//Code to start, cancel and commit edits

    @Override
    public void startEdit() {

        super.startEdit();
        textField.setText(converter.toString(getItem()));
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        textField.requestFocus();

    }

    @Override
    public void cancelEdit() {

        super.cancelEdit();
        setContentDisplay(ContentDisplay.TEXT_ONLY);

    }

    @Override
    public void commitEdit(T item) {

        // This block is necessary to support commit on losing focus, because the baked-in mechanism
        // sets our editing state to false before we can intercept the loss of focus.
        // The default commitEdit(...) method simply bails if we are not editing...

        if (! isEditing() && ! item.equals(getItem())) {
            TableView<S> table = getTableView();
            if (table != null) {
                TableColumn<S, T> column = getTableColumn();
                CellEditEvent<S, T> event = new CellEditEvent<>(table, 
                        new TablePosition<S,T>(table, getIndex(), column), 
                        TableColumn.editCommitEvent(), item);
                Event.fireEvent(column, event);
            }
        }

        super.commitEdit(item);

        setContentDisplay(ContentDisplay.TEXT_ONLY);

    }

//*******************************************************************************************************************    
//Validate data

    public boolean isDataValid(Class<T> classType, String enteredData) {

        boolean dataOK = true;
        String enteredDataWithoutCommas = "";

        if ( classType == Long.class || classType == Double.class ) {
            enteredDataWithoutCommas = enteredData.replaceAll(",", "");
        }

        if ( ( classType == Long.class && ! DAOGenUtil.isIntegerOrLong(enteredDataWithoutCommas) )
             || classType == Double.class && ! DAOGenUtil.isNumeric(enteredDataWithoutCommas)
             || classType == LocalDate.class && ! DAOGenUtil.isDate(enteredData) ) {
            dataOK = false;

        } else {

            dataOK = true;

        }

        return dataOK;

    }

//*******************************************************************************************************************    
//Display data entry error messages

    public void displayErrorMessage(Class<T> classType, FXMLControllerSuperType controllerRef) {

        if ( classType == Long.class ) {

            DAOGenUtil.setSystemMessage(controllerRef, "Invalid number (expected format 9,999).", COLOUR_DARK_RED);

        } else if ( classType == Double.class ) {

            DAOGenUtil.setSystemMessage(controllerRef, "Invalid number (expected format 9,999.9999).", COLOUR_DARK_RED);

        } else if ( classType == LocalDate.class ) {

            DAOGenUtil.setSystemMessage(controllerRef, "Invalid date (expected format DAY/MONTH/4-digit YEAR).", COLOUR_DARK_RED);

        }

    }

}

MouseEvent 中,单击的节点可通过 pickResult 访问。请注意,这可能是皮肤引入的控件的子控件,而不是控件本身。您仍然可以通过向上遍历场景层次结构来找到单击的单元格。这使您可以确定点击是否在正在编辑的单元格之外,并根据以下信息决定是否使用该事件:

public static void registerEditingHandler(final TableView<?> tableView) {
    EventHandler<MouseEvent> handler = event -> {
        TablePosition<?, ?> position = tableView.getEditingCell();
        if (position != null) {
            Node n = event.getPickResult().getIntersectedNode();

            while (n != tableView
                    && !(n instanceof TableCell)) {
                n = n.getParent();
            }

            // consume cells outside of cells or on cells not matching the
            // editedPosition
            if (n == tableView) {
                event.consume();
            } else {
                TableCell<?, ?> cell = (TableCell<?, ?>) n;
                if (cell.getIndex() != position.getRow()
                        || cell.getTableColumn() != position.getTableColumn()) {
                    event.consume();
                }
            }
        }
    };
    tableView.addEventFilter(MouseEvent.MOUSE_CLICKED, handler);
    tableView.addEventFilter(MouseEvent.MOUSE_PRESSED, handler);
    tableView.addEventFilter(MouseEvent.MOUSE_RELEASED, handler);
}