JavaFX怪(Key)EventBehavior [英] JavaFX weird (Key)EventBehavior

查看:208
本文介绍了JavaFX怪(Key)EventBehavior的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

所以我一直在尝试一下javaFX,我遇到了一些相当奇怪的行为,可能链接到 TableView#edit()方法。 / p>

我将再次在这个帖子的底部发布一个工作示例,因此您可以看到在哪个单元格(调试包括!)中确切发生了什么。

我会尝试自己解释所有的行为,虽然它的方式更容易看到自己。基本上,当使用 TableView#edit()方法时,事件就会变得混乱。



1:



如果您正在使用contextMenu添加一个新项目,则键escape和Enter的keyEvents(和可能的箭头键,虽然我现在不使用它们)在它们触发单元格上的事件之前被消耗(例如textField和单元格KeyEvents!)虽然它是在Parent节点上触发keyEvent。 (在这种情况下是AnchorPane)。



现在我知道这些键是被contextMenu默认行为捕获和消耗的。虽然它不应该发生,因为在添加新项目之后,contextMenu已经被隐藏。更进一步的是,TextField应该会收到这些事件,尤其是在集中注意时!



2:



TableView底部的按钮添加一个新的Item,KeyEvents在Parent节点(AnchorPane)和Cell上触发。虽然textField(即使是集中)也没有接收到keyEvents。我无法解释为什么TextField即使在输入时也不会收到任何事件,所以我认为这肯定会是一个错误?



3:



通过双击编辑单元格时,它会正确更新TableView的editingCellProperty(我检查了几次)。虽然通过contextMenu Item(仅调用testpurpose调用startEdit()开始编辑)它不会正确地更新编辑状态!有趣的是,它允许keyEvents像往常一样继续,与情况1& 2。



4:



当你编辑一个项目,然后添加一个项目这个问题)它会将editingCellProperty更新为当前的单元格,尽管当停止编辑时,它以某种方式恢复到最后一个单元格?这是有趣的事情发生的部分,我真的不能解释。



请注意,startEdit()& cancelEdit()方法在奇怪的时刻被调用,并且错误的单元格!



现在我不明白这个逻辑。如果这是预期的行为,那么一些解释将不胜感激!



这是一个例子:

  package testpacket; 

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;



public class EditStateTest extends Application
{
private static ObservableList< SimpleStringProperty> exampleList = FXCollections.observableArrayList();
//按钮的占位符
private static SimpleStringProperty PlaceHolder = new SimpleStringProperty();

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

@Override
public void start(Stage primaryStage)throws异常
{
// basic ui setup
AnchorPane parent = new AnchorPane();
场景场景=新场景(父);
primaryStage.setScene(scene); (int i = 0; i< 20; i ++)
exampleList.add(new SimpleStringProperty(Hello Test));

//填写支持数据

exampleList.add(PlaceHolder);

//创建一个基本的tableView
TableView< SimpleStringProperty> listView = new TableView< SimpleStringProperty>();
listView.setEditable(true);

TableColumn< SimpleStringProperty,String> column = new TableColumn< SimpleStringProperty,String>();
column.setCellFactory(E - > new TableCellTest< SimpleStringProperty,String>());
column.setCellValueFactory(E - > E.getValue());
column.setEditable(true);

//设置listViews的支持列表
listView.setItems(exampleList);


listView.getColumns()。clear();
listView.getColumns()。add(column);
parent.getChildren()。add(listView);

parent.setOnKeyReleased(E - > System.out.println(Parent - KeyEvent));


primaryStage.show();
}

//基本可编辑单元格示例
public static class TableCellTest< S,T>扩展TableCell< S,T>
{
//编辑textField。
protected static Button addButton = new Button(Add);
protected TextField textField = new TextField();;
保护ContextMenu菜单;


public TableCellTest()
{
this.setOnContextMenuRequested(E - > {
if(this.getTableView()。editingCellProperty()。 get()== null)
this.menu.show(this,E.getScreenX(),E.getScreenY());
});
this.menu = new ContextMenu();

MenuItem createNew = new MenuItem(create New);
createNew.setOnAction(E - > {
System.out.println(Cell ContextMenu+ this.getIndex()+ - createNew:onAction);
this.onNewItem this.getIndex()+ 1);
});
MenuItem edit = new MenuItem(edit);
edit.setOnAction(E - > {
System.out.println(Cell ContextMenu+ this.getIndex()+ - edit:onAction);
this.startEdit );
});

this.menu.getItems()。setAll(createNew,edit);

addButton.addEventHandler(ActionEvent.ACTION,E - > {
if(this.getIndex()== EditStateTest.exampleList.size() - 1)
{
System.out.println(Cell+ this.getIndex()+ - Button:onAction);
this.onNewItem(this.getIndex());
}
});
addButton.prefWidthProperty()。bind(this.widthProperty());

this.setOnKeyReleased(E - > System.out.println(Cell+ this.getIndex()+ - KeyEvent));
}

public void onNewItem(int index)
{
EditStateTest.exampleList.add(index,new SimpleStringProperty(New Item));
this.getTableView()。edit(index,this.getTableColumn());
textField.requestFocus();
}

@Override
public void startEdit()
{
if(!isEditable()
||(this.getTableView )!= null&&!this.getTableView()。isEditable())
||(this.getTableColumn()!= null&!this.getTableColumn()。isEditable()))
返回;

System.out.println(Cell+ this.getIndex()+ - StartEdit);
super.startEdit();

this.createTextField();

textField.setText((String)this.getItem());
this.setGraphic(textField);
textField.selectAll();
this.setText(null);
}

@Override
public void cancelEdit()
{
if(!this.isEditing())
return;

System.out.println(Cell+ this.getIndex()+ - CancelEdit);
super.cancelEdit();

this.setText((String)this.getItem());
this.setGraphic(null);
}

@Override
protected void updateItem(T item,boolean empty)
{
System.out.println(Cell+ this。 getIndex()+ - UpdateItem);
super.updateItem(item,empty);

if(empty || item == null)
{
if(this.getIndex()== EditStateTest.exampleList.size() - 1)
{
this.setText();
this.setGraphic(addButton);
}
else
{
this.setText(null);
this.setGraphic(null);
}
}
else
{
//需要这些检查才能确保此单元格是处于编辑模式的特定单元格。
//技术上,这个#isEditing()可以被省略,因为在这一点上它不够准确。
if(this.getTableView()。getEditingCell()!= null
&& this.getTableView()。getEditingCell()。getRow()== this.getIndex())
{
//更改为TextField
this.setText(null);
this.setGraphic(textField);
}
else
{
//更改为实际值
this.setText((String)this.getItem());
this.setGraphic(null);
}
}
}

@SuppressWarnings(unchecked)
public void createTextField()
{
textField。 setOnKeyReleased(E - > {
System.out.println(TextField+ this.getIndex()+ - KeyEvent);
System.out.println(this.getTableView()。getEditingCell ());
// if(this.getTableView()。getEditingCell()。getRow()== this.getIndex())
if(E.getCode()== KeyCode.ENTER)
{
this.setItem((T)textField.getText());
this.commitEdit(this.getItem());
}
else if E.getCode()== KeyCode.ESCAPE)
this.cancelEdit();
});
}
}
}

我希望有人能帮助我进一步与此。如果您有建议/解决方案或解决方法,请通知我!
感谢你的时间!

解决方案

这是Josh Bloch的继承破解封装 。我的意思是,当您创建现有类的子类( TableCell 在这种情况下)时,您需要了解很多关于实现,以使子类与超类玩得很好。你在代码中做出了很多关于 TableView 和它的单元格之间的交互的不正确的假设,以及(以及一些错误和一般的奇怪的事件处理实现在某些控件中)是您的代码破坏的原因。



我不认为我可以解决每一个问题,但我可以在这里给出一些一般的指针,并提供我认为是实现你正在尝试的工作代码实现



首先,单元格被重用。这是一件好事,因为当有大量数据时,表格非常有效,但这使得它变得复杂。基本思想是,单元格本质上仅为表中的可见项创建。随着用户滚动,或者随着表内容的变化,不再需要的单元格被重用于可见的不同项目。这大大节省了内存消耗和CPU时间(如果正确使用)。为了能够改进实现,JavaFX团队故意不指定它的工作原理,以及单元格如何以及何时被重用。因此,您必须小心对单元格的项目或索引字段的连续性进行假设(相反,哪个单元格分配给给定项目或索引),特别是如果更改表的结构。



您基本保证的是:




  • 任何时候单元格被重用于不同的项目, updateItem()方法在单元格渲染之前被调用。

  • 任何时候单元格的索引都会改变因为一个项目被插入到列表中,或者可能是由于单元格被重用,或者两者都是),所以在单元格呈现之前调用 updateIndex()方法。 / li>


但是,请注意,在两者都更改的情况下,不保证这些调用的顺序。因此,如果您的单元格呈现取决于项目和索引(这是这种情况:您检查项目和您的updateItem(...)方法中的索引),您需要确保单元格更新时的这些属性改变。实现此目的的最佳方式(imo)是创建一个私有方法来执行更新,并从updateItem()和updateIndex()委托给它。这样,当调用其中第二个方法时,您的更新方法将以一致的状态被调用。



如果更改表的结构,比方说添加一个新行,这些单元格将需要重新排列,其中一些可能被重用于不同的项目(和索引)。但是,这种重新排列只发生在表格布局时,默认情况下不会发生,直到下一帧渲染。 (从性能角度来说,这是有意义的:想象一下,你可以对循环中的表进行1000次不同的更改;你不希望在每次更改时重新计算单元格,只需要在下一次呈现表时重新计算单元格屏幕。)这意味着,如果您向表中添加行,则不能依赖任何单元格的索引或项目是正确的。这就是为什么在添加新行后立即对table.edit(...)的调用是如此不可预测的原因。这里的诀窍是通过在添加行之后调用 TableView.layout()来强制表的布局。



请注意,当单元格聚焦时按Enter将导致该单元格进入编辑模式。如果您使用键释放的事件处理程序处理单元格中的文本字段上的提交,这些处理程序将以不可预测的方式进行交互。我认为这就是为什么你看到你看到的奇怪的关键处理效果(还要注意,文本字段消耗他们内部处理的关键事件)。解决方法是在文本字段上使用一个onAction处理程序(这可以说是更多的语义)。



不要让按钮静止(我不知道你为什么要这么做) 静态表示该按钮是整个类的属性,而不是该类的实例。所以在这种情况下,所有的单元格共享一个单一按钮的引用。由于单元重用机制未指定,您不知道只有一个单元格将按钮设置为其图形。这可能会造成灾难。例如,如果您使用按钮滚动单元格,然后返回到视图,则不能保证在返回到视图时将使用相同的单元格显示最后一个项目。以前显示最后一个项目的单元格(可能是虚拟流容器的一部分,但被裁剪出视图)是可能的(我不知道实现),并且没有更新。在这种情况下,按钮会在场景图中出现两次,这会抛出异常或导致不可预知的行为。基本上没有任何有效的原因使场景图节点成为静态的,这里是一个特别糟糕的主意。



要编码这样的功能,您应该广泛阅读文档细胞机制 TableView TableColumn 的TableCell 。在某些时候,您可能会发现需要深入了解源代码看看提供的单元格实现如何工作。



这是(我想,我不知道我已经完全测试了)我认为你是一个工作版本寻找。我对结构做了一些微小的改变(不需要 StringProperty 作为数据类型, String 的工作原理很好只要你没有相同的重复),添加一个onEditCommit处理程序等。

  import javafx.application.Application; 
import javafx.beans.value.ObservableValueBase;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class TableViewWithAddAtEnd extends Application {

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

TableColumn< String,String> column = new TableColumn(Data);
column.setPrefWidth(150);
table.getColumns()。add(column);

//使用字符串数据的简单包装:
column.setCellValueFactory(cellData - > new ObservableValueBase< String>(){
@Override
public String getValue (){
return cellData.getValue();
}
});

column.setCellFactory(col - > new EditingCellWithMenuEtc());

column.setOnEditCommit(e - >
table.getItems()。set(e.getTablePosition()。getRow(),e.​​getNewValue())); (int i = 1; i< = 20; i ++){
table.getItems()。add(Item+ i);


}
//空白为添加按钮:
table.getItems()。add();

BorderPane root = new BorderPane(table);
primaryStage.setScene(new Scene(root,600,600));
primaryStage.show();

}

public static class EditingCellWithMenuEtc extends TableCell< String,String> {
private TextField textField;
私人按钮按钮;
private ContextMenu contextMenu;

//更新依赖于知道项目和索引
//由于我们不知道(或至少不应该依赖)订单
/ /其中项目和索引被更新,我们只是将
//将updateItem和updateIndex的实现委托给一般的
//方法。这样doUpdate()总是最后一次调用一致的
//状态,所以当
//单元格被渲染时,我们保证处于一致状态,即使我们暂时处于不一致的
//对updateItem和updateIndex的调用之间的状态。

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

@Override
public void updateIndex(int index){
super.updateIndex(index);
doUpdate(getItem(),index,isEmpty());
}

//更新单元格。这更新了文本,图形,上下文菜单
//(空单元格和特殊按钮单元格没有上下文菜单)
//和可编辑状态(空单元格和特殊按钮单元格不能
//被编辑)
private void doUpdate(String item,int index,boolean empty){
if(empty){
setText(null);
setGraphic(null);
setContextMenu(null);
setEditable(false);
} else {
if(index == getTableView()。getItems()。size() - 1){
setText(null);
setGraphic(getButton());
setContextMenu(null);
setEditable(false);
} else if(isEditing()){
setText(null);
getTextField()。setText(item);
setGraphic(getTextField());
getTextField()。requestFocus();
setContextMenu(null);
setEditable(true);
} else {
setText(item);
setGraphic(null);
setContextMenu(getMenu());
setEditable(true);
}
}
}

@Override
public void startEdit(){
if(!isEditable()
| |!getTableColumn()。isEditable()
||!getTableView()。isEditable()){
return;
}
super.startEdit();
getTextField()。setText(getItem());
setText(null);
setGraphic(getTextField());
setContextMenu(null);
textField.selectAll();
textField.requestFocus();
}

@Override
public void cancelEdit(){
super.cancelEdit();
setText(getItem());
setGraphic(null);
setContextMenu(getMenu());
}

@Override
public void commitEdit(String newValue){
//注意在列上对onEditCommit处理程序进行触发:
super.commitEdit(newValue );
setText(getItem());
setGraphic(null);
setContextMenu(getMenu());
}

private void addNewItem(int index){
getTableView()。getItems()。add(index,New Item);
//强制重新计算单元格:
getTableView()。layout();
//开始编辑:
getTableView()。edit(index,getTableColumn());
}

private ContextMenu getMenu(){
if(contextMenu == null){
createContextMenu();
}
return contextMenu;
}

private void createContextMenu(){
MenuItem addNew = new MenuItem(Add new);
addNew.setOnAction(e - > addNewItem(getIndex()+ 1));
MenuItem edit = new MenuItem(Edit);
//注意我们调用TableView.edit(),而不是this.startEdit()来确保
//表的编辑状态保持一致:
edit.setOnAction(e - > getTableView ).edit(getIndex(),getTableColumn()));
contextMenu = new ContextMenu(addNew,edit);
}

private Button getButton(){
if(button == null){
createButton();
}
返回按钮;
}

private void createButton(){
button = new Button(Add);
button.prefWidthProperty()。bind(widthProperty());
button.setOnAction(e - > addNewItem(getTableView()。getItems()。size() - 1));
}

private TextField getTextField(){
if(textField == null){
createTextField();
}
return textField;
}

private void createTextField(){
textField = new TextField();
//使用setOnAction进行输入,以避免与单元格中的输入冲突:
textField.setOnAction(e - > commitEdit(textField.getText()));
//使用释放的转义键:注意文本字段做注意消费
//他们不处理的键释放:
textField.setOnKeyReleased(e - > {
if (e.getCode()== KeyCode.ESCAPE){
cancelEdit();
}
});
}
}

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


So I have been experimenting with it a litle bit with javaFX and I came across some rather weird behavior which might be linked to the TableView#edit() method.

I'll post a working example on the bottom of this post again, so you can see what exactually is happening on which cell (debuging included!).

I'll try to explain all the behavior myself, though its way easier to see it for yourself. Basically the events are messed up when using the TableView#edit() method.

1:

If you are using the contextMenu to add a new item, the keyEvents for the the keys 'escape' and 'Enter' (and propably the arrow keys, though I dont use them right now) are consumed before they fire the events on the Cells (e.g. textField and cell KeyEvents!) Though it is firing the keyEvent on the Parent node. (the AnchorPane in this case).

Now I know for a fact that these keys are captured and consumed by the contextMenu default behavior. Though it shouldn't be happening since the contextMenu is already hidden after the new item is added. further more the textField should recieve the events, especially when it is focused!

2:

When you use the button at the bottom of the TableView to add a new Item, The keyEvents are fired on the Parent node (the AnchorPane) and the Cell. Though the textField (even when focused) recieve no keyEvents at all. I cannot explain why the TextField wouldn't recieve any event even when typed in, so I assume that would definitely be a bug?

3:

When editing a cell through double click, it updates the editingCellProperty of the TableView correctly (which I check for several times). Though when start editing though the contextMenu Item (which only calls startEdit() for testpurpose) It doesnt update the editing state correctly! Funny enough it allows the keyEvents to continue as usual, unlike situation 1 & 2.

4:

When you edit an item, and then add an item (either way will cause this problem) it will update the editingCellProperty to the current cell, though when stop editing, it somehow revert back to the last Cell?!? Thats the part where funny things are happening, which I really cannot explain.

Note that the startEdit() & cancelEdit() methods are called in weird moments, and on the wrong Cells!

Right now I dont understand any of this logic. If this is intended behavior, some explanation of it would be greatly appreciated!

This is the example:

package testpacket;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;



public class EditStateTest extends Application
{
    private static ObservableList<SimpleStringProperty> exampleList = FXCollections.observableArrayList();
    //Placeholder for the button
    private static SimpleStringProperty PlaceHolder = new SimpleStringProperty();

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

    @Override
    public void start(Stage primaryStage) throws Exception
    {
        // basic ui setup
        AnchorPane parent = new AnchorPane();
        Scene scene = new Scene(parent);
        primaryStage.setScene(scene);

        //fill backinglist with data
        for(int i = 0 ; i < 20; i++)
            exampleList.add(new SimpleStringProperty("Hello Test"));
        exampleList.add(PlaceHolder);

        //create a basic tableView
        TableView<SimpleStringProperty> listView = new TableView<SimpleStringProperty>();
        listView.setEditable(true);

        TableColumn<SimpleStringProperty, String> column = new TableColumn<SimpleStringProperty, String>();
        column.setCellFactory(E -> new TableCellTest<SimpleStringProperty, String>());
        column.setCellValueFactory(E -> E.getValue());
        column.setEditable(true);

        // set listViews' backing list
        listView.setItems(exampleList);


        listView.getColumns().clear();
        listView.getColumns().add(column);
        parent.getChildren().add(listView);

        parent.setOnKeyReleased(E -> System.out.println("Parent - KeyEvent"));


        primaryStage.show();
    }

    // basic editable cell example
    public static class TableCellTest<S, T> extends TableCell<S, T>
    {
        // The editing textField.
        protected static Button addButton = new Button("Add");
        protected TextField textField = new TextField();;
        protected ContextMenu menu;


        public TableCellTest()
        {
            this.setOnContextMenuRequested(E -> {
                if(this.getTableView().editingCellProperty().get() == null)
                    this.menu.show(this, E.getScreenX(), E.getScreenY());
            });
            this.menu = new ContextMenu();

            MenuItem createNew = new MenuItem("create New");
            createNew.setOnAction(E -> {
                System.out.println("Cell ContextMenu " + this.getIndex() + " - createNew: onAction");
                this.onNewItem(this.getIndex() + 1);
            });
            MenuItem edit = new MenuItem("edit");
            edit.setOnAction(E -> {
                System.out.println("Cell ContextMenu " + this.getIndex() + " - edit: onAction");
                this.startEdit();
            });

            this.menu.getItems().setAll(createNew, edit);

            addButton.addEventHandler(ActionEvent.ACTION, E -> {
                if(this.getIndex() == EditStateTest.exampleList.size() - 1)
                {
                    System.out.println("Cell " + this.getIndex() + " - Button: onAction");
                    this.onNewItem(this.getIndex());
                }
            });
            addButton.prefWidthProperty().bind(this.widthProperty());

            this.setOnKeyReleased(E -> System.out.println("Cell " + this.getIndex() + " - KeyEvent"));
        }

        public void onNewItem(int index)
        {
            EditStateTest.exampleList.add(index, new SimpleStringProperty("New Item"));
            this.getTableView().edit(index, this.getTableColumn());
            textField.requestFocus();
        }

        @Override
        public void startEdit()
        {
            if (!isEditable()
                    || (this.getTableView() != null && !this.getTableView().isEditable())
                    || (this.getTableColumn() != null && !this.getTableColumn().isEditable()))
                return;

            System.out.println("Cell " + this.getIndex() + " - StartEdit");
            super.startEdit();

            this.createTextField();

            textField.setText((String)this.getItem());
            this.setGraphic(textField);
            textField.selectAll();
            this.setText(null);
        }

        @Override
        public void cancelEdit()
        {
            if (!this.isEditing())
                return;

            System.out.println("Cell " + this.getIndex() + " - CancelEdit");
            super.cancelEdit();

            this.setText((String)this.getItem());
            this.setGraphic(null);
        }

        @Override
        protected void updateItem(T item, boolean empty)
        {
            System.out.println("Cell " + this.getIndex() + " - UpdateItem");
            super.updateItem(item, empty);

            if(empty || item == null)
            {
                if(this.getIndex() == EditStateTest.exampleList.size() - 1)
                {
                    this.setText("");
                    this.setGraphic(addButton);
                }
                else
                {
                    this.setText(null);
                    this.setGraphic(null);
                }
            }
            else
            {
                // These checks are needed to make sure this cell is the specific cell that is in editing mode.
                // Technically this#isEditing() can be left out, as it is not accurate enough at this point.
                if(this.getTableView().getEditingCell() != null 
                        && this.getTableView().getEditingCell().getRow() == this.getIndex())
                {
                    //change to TextField
                    this.setText(null);
                    this.setGraphic(textField);
                }
                else
                {
                    //change to actual value
                    this.setText((String)this.getItem());
                    this.setGraphic(null);
                }
            }
        }

        @SuppressWarnings("unchecked")
        public void createTextField()
        {
            textField.setOnKeyReleased(E -> {
                System.out.println("TextField " + this.getIndex() + " - KeyEvent");
                System.out.println(this.getTableView().getEditingCell());
//              if(this.getTableView().getEditingCell().getRow() == this.getIndex())
                    if(E.getCode() == KeyCode.ENTER)
                    {
                        this.setItem((T) textField.getText());
                        this.commitEdit(this.getItem());
                    }
                    else if(E.getCode() == KeyCode.ESCAPE)
                        this.cancelEdit();
            });
        }
    }
}

I hope somebody could help me further with this. If you have suggestions/solutions or workarounds for this, please let me know! Thanks for your time!

解决方案

This is kind of the poster child for Josh Bloch's "Inheritance breaks Encapsulation" mantra. What I mean by that is that when you create a subclass of an existing class (TableCell in this case), you need to know a lot about the implementation of that class in order to make the subclass play nicely with the superclass. You make a lot of assumptions in your code about the interaction between the TableView and its cells that are not true, and that (along with some bugs and general weird implementations of event handling in some controls) is why your code is breaking.

I don't think I can address every single issue, but I can give some general pointers here and provide what I think is working code that achieves what you are trying to achieve.

First, cells are reused. This is a good thing, because it makes the table perform very efficiently when there is a large amount of data, but it makes it complicated. The basic idea is that cells are essentially only created for the visible items in the table. As the user scrolls around, or as the table content changes, cells that are no longer needed are reused for different items that become visible. This massively saves on memory consumption and CPU time (if used properly). In order to be able to improve the implementation, the JavaFX team deliberately don't specify how this works, and how and when cells are likely to be reused. So you have to be careful about making assumptions about the continuity of the item or index fields of a cell (and conversely, which cell is assigned to a given item or index), particularly if you change the structure of the table.

What you are basically guaranteed is:

  • Any time the cell is reused for a different item, the updateItem() method is invoked before the cell is rendered.
  • Any time the index of the cell changes (which may be because an item is inserted in the list, or may be because the cell is reused, or both), the updateIndex() method is invoked before the cell is rendered.

However, note that in the case where both change, there is no guarantee of the order in which these are invoked. So, if your cell rendering depends on both the item and the index (which is the case here: you check both the item and the index in your updateItem(...) method), you need to ensure the cell is updated when either of those properties change. The best way (imo) to achieve this is to create a private method to perform the update, and to delegate to it from both updateItem() and updateIndex(). This way, when the second of those is invoked, your update method is invoked with consistent state.

If you change the structure of the table, say by adding a new row, the cells will need to be rearranged, and some of them are likely to be reused for different items (and indexes). However, this rearrangement only happens when the table is laid out, which by default will not happen until the next frame rendering. (This makes sense from a performance perspective: imagine you make 1000 different changes to a table in a loop; you don't want the cells to be recalculated on every change, you just want them recalculated once the next time the table is rendered to the screen.) This means, if you add rows to the table, you cannot rely on the index or item of any cell being correct. This is why your call to table.edit(...) immediately after adding a new row is so unpredictable. The trick here is to force a layout of the table by calling TableView.layout() after adding the row.

Note that pressing "Enter" when a table cell is focused will cause that cell to go into editing mode. If you handle commits on the text field in a cell with a key released event handler, these handlers will interact in an unpredictable way. I think this is why you see the strange key handling effects you see (also note that text fields consume the key events they process internally). The workaround for that is to use an onAction handler on the text field (which is arguably more semantic anyway).

Don't make the button static (I have no idea why you would want to do this anyway). "Static" means that the button is a property of the class as a whole, not of the instances of that class. So in this case, all the cells share a reference to a single button. Since the cell reuse mechanism is unspecified, you don't know that only one cell will have the button set as its graphic. This can cause disaster. For example, if you scroll the cell with the button out of view and then back into view, there is no guarantee the same cell will be used to display that last item when it comes back into view. It is possible (I don't know the implementation) that the cell that previously displayed the last item is sitting unused (perhaps part of the virtual flow container, but clipped out of view) and is not updated. In that case, the button would then appear twice in the scene graph, which would either throw an exception or cause unpredictable behavior. There's basically no valid reason to ever make a scene graph node static, and here it's a particularly bad idea.

To code functionality like this, you should read extensively the documentation for the cell mechanism and for TableView, TableColumn, and TableCell. At some point you might find you need to dig into the source code to see how the provided cell implementations work.

Here's (I think, I'm not sure I've fully tested) a working version of what I think you were looking for. I made some slight changes to the structure (no need for StringPropertys as the data type, String works just fine as long as you have no identical duplicates), added an onEditCommit handler, etc.

import javafx.application.Application;
import javafx.beans.value.ObservableValueBase;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class TableViewWithAddAtEnd extends Application {

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

        TableColumn<String, String> column = new TableColumn<>("Data");
        column.setPrefWidth(150);
        table.getColumns().add(column);

        // use trivial wrapper for string data:
        column.setCellValueFactory(cellData -> new ObservableValueBase<String>() {
            @Override
            public String getValue() {
                return cellData.getValue();
            }
        });

        column.setCellFactory(col -> new EditingCellWithMenuEtc());

        column.setOnEditCommit(e -> 
            table.getItems().set(e.getTablePosition().getRow(), e.getNewValue()));

        for (int i = 1 ; i <= 20; i++) {
            table.getItems().add("Item "+i);
        }
        // blank for "add" button:
        table.getItems().add("");

        BorderPane root = new BorderPane(table);
        primaryStage.setScene(new Scene(root, 600, 600));
        primaryStage.show();

    }

    public static class EditingCellWithMenuEtc extends TableCell<String, String> {
        private TextField textField ;
        private Button button ;
        private ContextMenu contextMenu ;

        // The update relies on knowing both the item and the index
        // Since we don't know (or at least shouldn't rely on) the order
        // in which the item and index are updated, we just delegate
        // implementations of both updateItem and updateIndex to a general
        // method. This way doUpdate() is always called last with consistent
        // state, so we are guaranteed to be in a consistent state when the
        // cell is rendered, even if we are temporarily in an inconsistent 
        // state between the calls to updateItem and updateIndex.

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

        @Override
        public void updateIndex(int index) {
            super.updateIndex(index);
            doUpdate(getItem(), index, isEmpty());
        }

        // update the cell. This updates the text, graphic, context menu
        // (empty cells and the special button cell don't have context menus)
        // and editable state (empty cells and the special button cell can't
        // be edited)
        private void doUpdate(String item, int index, boolean empty) {
            if (empty) {
                setText(null);
                setGraphic(null);
                setContextMenu(null);
                setEditable(false);
            } else {
                if (index == getTableView().getItems().size() - 1) {
                    setText(null);
                    setGraphic(getButton());
                    setContextMenu(null);
                    setEditable(false);
                } else if (isEditing()) {
                    setText(null);
                    getTextField().setText(item);
                    setGraphic(getTextField());
                    getTextField().requestFocus();
                    setContextMenu(null);
                    setEditable(true);
                } else {
                    setText(item);
                    setGraphic(null);
                    setContextMenu(getMenu());
                    setEditable(true);
                }
            }
        }

        @Override
        public void startEdit() {
            if (! isEditable() 
                    || ! getTableColumn().isEditable()
                    || ! getTableView().isEditable()) {
                return ;
            }
            super.startEdit();
            getTextField().setText(getItem());
            setText(null);
            setGraphic(getTextField());
            setContextMenu(null);
            textField.selectAll();
            textField.requestFocus();
        }

        @Override
        public void cancelEdit() {
            super.cancelEdit();
            setText(getItem());
            setGraphic(null);
            setContextMenu(getMenu());
        }

        @Override
        public void commitEdit(String newValue) {
            // note this fires onEditCommit handler on column:
            super.commitEdit(newValue);
            setText(getItem());
            setGraphic(null);
            setContextMenu(getMenu());
        }

        private void addNewItem(int index) {
            getTableView().getItems().add(index, "New Item");
            // force recomputation of cells:
            getTableView().layout();
            // start edit:
            getTableView().edit(index, getTableColumn());
        }

        private ContextMenu getMenu() {
            if (contextMenu == null) {
                createContextMenu();
            }
            return contextMenu ;
        }

        private void createContextMenu() {
            MenuItem addNew = new MenuItem("Add new");
            addNew.setOnAction(e -> addNewItem(getIndex() + 1));
            MenuItem edit = new MenuItem("Edit");
            // note we call TableView.edit(), not this.startEdit() to ensure 
            // table's editing state is kept consistent:
            edit.setOnAction(e -> getTableView().edit(getIndex(), getTableColumn()));
            contextMenu = new ContextMenu(addNew, edit);
        }

        private Button getButton() {
            if (button == null) {
                createButton();
            }
            return button ;
        }

        private void createButton() {
            button = new Button("Add");
            button.prefWidthProperty().bind(widthProperty());
            button.setOnAction(e -> addNewItem(getTableView().getItems().size() - 1));
        }

        private TextField getTextField() {
            if (textField == null) {
                createTextField();
            }
            return textField ;
        }

        private void createTextField() {
            textField = new TextField();
            // use setOnAction for enter, to avoid conflict with enter on cell:
            textField.setOnAction(e -> commitEdit(textField.getText()));
            // use key released for escape: note text fields do note consume
            // key releases they don't handle:
            textField.setOnKeyReleased(e -> {
                if (e.getCode() == KeyCode.ESCAPE) {
                    cancelEdit();
                }
            });
        }
    }

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

这篇关于JavaFX怪(Key)EventBehavior的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆