I'm trying to implement my own VirtualFlow but I'm having some performance issues and I don't understand where the bottleneck could be and what I could do to improve it.
Here's a showcase with two lists, one with simple cells and one with checkboxes too, both have 100_000 items (sorry for the gif quality):
As you can see, I have no issues scrolling through simple cells with just a label, but the list with checkboxes can be quite laggy at the beginning.
To reach such performance (would be much worse otherwise) I memoize my cell factory function so that all cells are ready in memory when needed, I was wondering if there's anything else to make it even faster/smoother.
The code I use to show the cells is:
long start = System.currentTimeMillis();
builtNodes.clear();
for (int i = from; i <= to; i++) {
C cell = cellForIndex.apply(i);
Node node = cell.getNode();
builtNodes.add(node);
}
manager.virtualFlow.container.getChildren().setAll(builtNodes);
long elapsed = System.currentTimeMillis() - start;
System.out.println("Show elapsed: " + elapsed);
I even added this little log, the heavy part seems to be when I call getChildren().setAll(...), both the cell building and the layout are almost immediate
Oh another note on the from and to parameters. When I scroll, I take the scrollbar's value and calculate the first and last visible indexes like this:
public int firstVisible() {
return (int) Math.floor(scrolled / cellHeight);
}
public int lastVisible() {
return (int) Math.ceil((scrolled + virtualFlow.getHeight()) / cellHeight - 1);
}
Edit to answer some questions:
Why implementing my own VirtualFlow?
The ultimate goal would be to make my own ListView implementation, in order to do that I also need a VirtualFlow, also I know it's a pretty low level and hard task, but I think it would be a good way to learn more about programming in general and about JavaFX too
Why memoization?
By caching the cell factory results in memory, it makes subsequent scrolls much faster since nodes are already built they just need to be laid out which is fairly easy. The issue is at the start because for some reason the VirtualFlow lags for more complex cells and I can't understand why since the popular library, Flowless, also uses memoization and caches the nodes in memory, but it is very fluent already from the start. JavaFX's VirtualFlow (revised from JFX16) is also very efficient without memoization, but it's much more complex to comprehend
I finally found a solution that is fast and also efficient for memory.
Instead of building new cells every time the view scrolls and update the children list which is super expensive, I build just the needed amount of cells to fill the viewport.
On scroll, the cells are laid out and updated. Updating the cells rather than creating and change the children list is much, much faster.
So, the new implementation is very similar to Android's RecyclerView and maybe also to the JavaFX's VirtualFlow, the only difference being that Cells are dumb, quoting from Flowless:
This is the most important difference. For Flowless, cells are just Nodes and don't encapsulate any logic regarding virtual flow. A cell does not even necessarily store the index of the item it is displaying. This allows VirtualFlow to have complete control over when the cells are created and/or updated.
Everyone can extend the interface and define its own logic.
To show a little bit of code:
The Cell interface:
public interface Cell<T> {
/**
* Returns the cell's node.
* The ideal way to implement a cell would be to extend a JavaFX's pane/region
* and override this method to return "this".
*/
Node getNode();
/**
* Automatically called by the VirtualFlow
* <p>
* This method must be implemented to correctly
* update the Cell's content on scroll.
* <p>
* <b>Note:</b> if the Cell's content is a Node this method should
* also re-set the Cell's children because (quoting from JavaFX doc)
* `A node may occur at most once anywhere in the scene graph` and it's
* possible that a Node may be removed from a Cell to be the content
* of another Cell.
*/
void updateItem(T item);
/**
* Automatically called by the VirtualFlow.
* <p>
* Cells are dumb, they have no logic, no state.
* This method allow cells implementations to keep track of a cell's index.
* <p>
* Default implementation is empty.
*/
default void updateIndex(int index) {}
/**
* Automatically called after the cell has been laid out.
* <p>
* Default implementation is empty.
*/
default void afterLayout() {}
/**
* Automatically called before the cell is laid out.
* <p>
* Default implementation is empty.
*/
default void beforeLayout() {}
}
The CellsManager class, responsible for showing and updating cells:
/*
* Initialization, creates num cells
*/
protected void initCells(int num) {
int diff = num - cellsPool.size();
for (int i = 0; i <= diff; i++) {
cellsPool.add(cellForIndex(i));
}
// Add the cells to the container
container.getChildren().setAll(cellsPool.stream().map(C::getNode).collect(Collectors.toList()));
// Ensure that cells are properly updated
updateCells(0, num);
}
/*
* Update the cells in the given range.
* CellUpdate is a simple bean that contains the cell, the item and the item's index.
* The update() method calls updateIndex() and updateItem() of the Cell's interface
*/
protected void updateCells(int start, int end) {
// If the items list is empty return immediately
if (virtualFlow.getItems().isEmpty()) return;
// If the list was cleared (so start and end are invalid) cells must be rebuilt
// by calling initCells(numOfCells), then return since that method will re-call this one
// with valid indexes.
if (start == -1 || end == -1) {
int num = container.getLayoutManager().lastVisible();
initCells(num);
return;
}
// If range not changed or update is not triggered by a change in the list, return
NumberRange<Integer> newRange = NumberRange.of(start, end);
if (lastRange.equals(newRange) && !listChanged) return;
// If there are not enough cells build them and add to the container
Set<Integer> itemsIndexes = NumberRange.expandRangeToSet(newRange);
if (itemsIndexes.size() > cellsPool.size()) {
supplyCells(cellsPool.size(), itemsIndexes.size());
} else if (itemsIndexes.size() < cellsPool.size()) {
int overFlow = cellsPool.size() - itemsIndexes.size();
for (int i = 0; i < overFlow; i++) {
cellsPool.remove(0);
container.getChildren().remove(0);
}
}
// Items index can go from 0 to size() of items list,
// cells can go from 0 to size() of the cells pool,
// Use a counter to get the cells and itemIndex to
// get the correct item and index to call
// updateIndex() and updateItem() later
updates.clear();
int poolIndex = 0;
for (Integer itemIndex : itemsIndexes) {
T item = virtualFlow.getItems().get(itemIndex);
CellUpdate update = new CellUpdate(item, cellsPool.get(poolIndex), itemIndex);
updates.add(update);
poolIndex++;
}
// Finally, update the cells, the layout and the range of items processed
updates.forEach(CellUpdate::update);
processLayout(updates);
lastRange = newRange;
}
Maybe it's still not perfect, but it works, I still need to test it properly tough for situations like too many cells, not enough cells, the sizes of the container change so the number of cells must be recomputed again...
Performance now:
Related
I am creating a completer myself, using ComboBox and QTreeView (for the proposal list).
MyComboBox::MyComboBox( QWidget *p_parent ) : QComboBox( p_parent )
{
setEditable(true);
m_view = new QTreeView();
m_view->expandAll(); // this command does not work!!!
m_view->setItemDelegate( new CompleterDelegate(m_view));
CompleterSourceModel *m_sourceModel = new CompleterSourceModel(this);
CompleterProxyModel *m_proxyModel = new CompleterProxyModel(this);
m_proxyModel->setSourceModel(m_sourceModel);
setView(m_view);
setModel(m_proxyModel);
connect(this, &QComboBox::currentTextChanged, this, &MyComboBox::showProposalList);
}
The structure of my data for the tree model here is parent-child. With the constructor above, after I put my data into the model, the children are hidden, only the parents can be seen.
In order to see all the items (children) I have to use m_view->expandAll() after I put the data into the model. Is there any way that we can do it in constructor, so each time i put data into the model (whatever my data is), all the items (parents and children) are automatically expanded ?
Your best bet is probably to connect to the QAbstractItemModel::rowsInserted signal to make sure items are expanded on a just-in-time basis. So, immediately after setting the view's model use something like...
connect(m_view->model(), &QAbstractItemModel::rowsInserted,
[this](const QModelIndex &parent, int first, int last)
{
/*
* New rows have been added to parent. Make sure parent
* is fully expanded.
*/
m_view->expandRecursively(parent);
});
Edit: It was noted in the comments (#Patrick Parker) that simply calling m_view->expand(parent) will not work if the row inserted has, itself, one or more levels of descendants. Have changed the code to use m_view->expandRecursively(parent) (as suggested by #m7913d) to take care of that.
Firstly, i am sorry but i don't speak english very well. Secondly, i have a problem with nodes which are put in a gridpane. In fact, if the focus is taken by the first one wich is located on the top left side, when i push the tab key, the focus is not taken by the other which is located on the right.
People ask me to use the traversalEngine abstract class in order to solve this problem. Nevertheless, when i try to implement an engine object, it doesn't work if i put the parameters which are shown everywhere on the web:
TraversalEngine engine = new TraversalEngine(gridPane, false) {
It ask me to remove the parameters. If i do it, i don't have access to the trav method. In fact, it is the getRoot method which appears and can be implemented :
TraversalEngine engine = new TraversalEngine() {
#Override
protected Parent getRoot() {
// TODO Auto-generated method stub
return null;
}
}
Is there something which can be make in order to solve this problem ?
Thanks you for your help
Vinz
The traversal order for focusing nodes in a parent is the order in which they occur in the child list. Assuming every child contains at most one focusable node you could simply add the children line by line or reorder the children.
This could be done programmatically of course, but adding the children in the correct order in the first place would be more efficient...
public static int getColumnIndex(Node n) {
Integer i = GridPane.getColumnIndex(n);
return i == null ? 0 : i;
}
public static int getRowIndex(Node n) {
Integer i = GridPane.getRowIndex(n);
return i == null ? 0 : i;
}
grid.getChildren().sort(Comparator.comparingInt(ContainingClass::getRowIndex).thenComparingInt(ContainingClass::getColumnIndex));
I build a little JavaFX TableView for displaying data. The user should be able to edit the data in the tableview. The problem is: only specific values are allowed in certain fields. If the user entered a wrong value, the field is set to 0.
Here is my Class:
private ObservableList shots;
#FXML
void initialize() {
this.shots = FXCollections.observableArrayList(match.getShots()); // values from database
tblShots.setItems(shots);
tblShots.setEditable(true);
lblserienid.setText(GUIConstants.idPlaceHolder);
lblresult.setText(GUIConstants.idPlaceHolder);
colShotID.setCellValueFactory(new PropertyValueFactory<Schuss, String>("idSchuss"));
colRing.setCellValueFactory(new PropertyValueFactory<Schuss, String>("ringString"));
colRing.setCellFactory(TextFieldTableCell.forTableColumn());
colRing.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<Schuss, String>>() {
#Override
public void handle(TableColumn.CellEditEvent<Schuss, String> t) {
Schuss s = (Schuss) t.getTableView().getItems().get(
t.getTablePosition().getRow());
try {
int ring = Integer.parseInt(t.getNewValue());
s.setRing(ring);
} catch (Exception ex) {
s.setRing(0);
}
SerienauswertungViewController.this.refreshTable();
}
});
colRing.setEditable(true);
// .... omitted
}
private void refreshTable(){
if(shots.size()>0) {
btnDeleteAll.setDisable(false);
btnEdit.setDisable(false);
int res = 0;
for(int i=0;i<shots.size();i++){
Schuss s = (Schuss)shots.get(i);
res += s.getRing();
}
lblresult.setText(""+res);
}
else {
btnDeleteAll.setDisable(true);
btnEdit.setDisable(true);
lblresult.setText(GUIConstants.idPlaceHolder);
}
}
So when I edit a tableviewcell and enter "q" (this value is not allowed) and press enter, the debugger jumps in the above catch block, sets the specific value in the observablelist to 0 (I can see this in the debugger, when I expand this object) but the tableviewcell still displays q instead of 0 (which has been corrected by the system)...
Why does the tableview not show the right values of the observablelist-Object???
This was required but brandnew since Java8u60 (yes - they changed API in an udpate!?!) there is a refresh() method on the TableView itself.
/**
* Calling {#code refresh()} forces the TableView control to recreate and
* repopulate the cells necessary to populate the visual bounds of the control.
* In other words, this forces the TableView to update what it is showing to
* the user. This is useful in cases where the underlying data source has
* changed in a way that is not observed by the TableView itself.
*
* #since JavaFX 8u60
*/
public void refresh() {
getProperties().put(TableViewSkinBase.RECREATE, Boolean.TRUE);
}
It is so new, it´s not even in the official oracle docs... So I cannot provide a link.
cheers.
Okey this seems to be a bug. I used a work around which is mentioned here:
tblShots.getColumns().get(1).setVisible(false);
tblShots.getColumns().get(1).setVisible(true);
Though the refresh() definitely works in conjunction with an upgrade to 8u60, the large project on which I work is currently stuck on 8u51 and cannot reasonably move to u60 any time soon. I tried to implement the code in the refresh() that Rainer referenced in place of the other kluges mentioned above, specifically setting the column invisible/visible. However, simply implementing
getProperties().put(TableViewSkinBase.RECREATE, Boolean.TRUE);
did not work within u51. Upon doing more googling, I came across the JavaFx/Oracle Jira issue here:
https://bugs.openjdk.java.net/browse/JDK-8098085
If you open the rt22599.patch attachment you will find changes to various skins, specifically for TableViews, the TableViewSkinBase. This module is not delivered in the src-javafx.zip that comes with the jdk install. Looking for info on how to possibly incorporate the SkinBase change to a u51 install.
Since JavaFX 8u60 you can use(assuming tableView is an instance of TableView class):
tableView.refresh();
It worked for me
I am implementing drag and drag between two tree views. When a treeItem is dropped onto another treeView of treeItem a line connection is established between the two treeItems. This is working fine , but to have a connection initially without a drag and drop events is problem to me.
I am using treeCell for the drag and drop events.
final var treeCells = treeView.lookupAll( ".tree-cell" );
final var cells = new ArrayList<>( treeCells );
final var row = treeView.getRow( n );
final var node = cells.get( row );
if( node instanceof TreeCell ) {
#SuppressWarnings("rawtypes")
final var cell = (TreeCell) node;
System.out.println( "TREE CELL: " + cell );
}
As I thought, this turns out to be pretty difficult.
There is, by design, no way to get the TreeCell belonging to a given TreeItem. This is because of the "virtualization" design of the TreeView: a minimal number of TreeCells are created and these are updated with new TreeItems as required. Thus there typically won't be a TreeCell for every TreeItem, and the TreeItem represented by a given TreeCell may change at any point.
To make this work, first create an observable collection storing all the connections between the trees (e.g. an ObservableSet should work well). Represent the connections by some class that exposes start and end points which can be used for the lines.
Create custom cell factories for the trees. The cells they return should:
observe the item they are representing. If the item changes to one that is at an end of one or more connections, then the appropriate point on those connections should be bound to the appropriate transform of the coordinates of the cell.
If the item changes from one that is at the end of one or more connections, then unbind the appropriate end from the cell coordinates
observe the observable collection of connections. If one is added for which this cell's item is one end, then bind the coordinates as above
Note that when you bind the coordinates, you need to take into account the fact that the cells may move (e.g. via scrolling or via other changes in GUI layout). You also need to transform the coordinates from the cell's own coordinate system into the coordinate system of whichever pane is holding the connections (obviously, if these are connecting one tree to another, it must be some common scene graph ancestor of both trees).
And finally, you need some housekeeping. The connections need to make sure they either become invisible, or are removed from the scene if they are no longer bound at one or more ends.
I created an example. I just created some simple controls for generating the connections, but you could easily do this with drag and drop instead. The class encapsulating the view of the connection is AssignmentView; it uses Assignment to represent the actual data that is connected. The ConnectedTrees class is the main application and most of the interesting controller-type work is in there. The remaining classes are just data representation. The example is all Java 8; I think it would be much uglier in JavaFX 2.2.
This solution uses recursion calls to traverse the nodes tree of the tree view.
Normally this recursion shouldn't be dangerous as there is only limited nodes number (TreeCell's instances are reused by TreeView):
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import java.util.List;
public class TreeViewExplore {
/**
* Returns a cell for the tree item given. Or null.
*
* #param treeItem tree item
* #param treeView tree view
* #return tree cell
*/
public static TreeCell findCellByItem(TreeItem treeItem, TreeView treeView) {
return recursiveFindCellByItem(treeItem, treeView);
}
private static TreeCell recursiveFindCellByItem(TreeItem treeItem, Node node) {
if (node.getStyleClass().contains("tree-cell")
&& TreeCell.class.isAssignableFrom(node.getClass())
&& ((TreeCell) node).getTreeItem() == treeItem)
return (TreeCell) node;
if (!Parent.class.isAssignableFrom(node.getClass()))
return null;
List<Node> nodes = ((Parent) node).getChildrenUnmodifiable();
if (nodes == null) return null;
for (Node n : nodes) {
TreeCell cell = recursiveFindCellByItem(treeItem, n);
if (cell != null) return cell;
}
return null;
}
}
Usage:
// TreeItem treeItem = ...
TreeCell cell = TreeViewExplore.findCellByItem(treeItem, treeView);
// Check result:
System.out.println(
cell == null ? "cell is null" :
"(cell.getTreeItem() == treeItem) = "
+ (cell.getTreeItem() == treeItem));
Yet another solution using lookupAll() method. It is only for example here as it looks non very efficient for me because this method collects all nodes with CSS-selector given (and traverses all over the tree in any case):
public static TreeCell findCellByItem(TreeItem treeItem, TreeView treeView) {
return (TreeCell) treeView.lookupAll(".tree-cell").stream()
.filter(n -> ((TreeCell) n).getTreeItem() == treeItem)
.findFirst()
.orElse(null);
}
I am looking for a way to get the selected cell of a TableView control. Note that I don't just want the cell value, I want an actual TableCell object. One would expect that the method:
tableView.getSelectionModel().getSelectedCells().get(0)
does just that, but it returns a TablePosition object, which gives you row and column information, but I don't see a way to get TableCell object from that.
The reason I need this is because I want to respond to a key press, but attaching an event filter to TableCell does not work (probably because it is not editable). So I attach it to TableView, but then I need to get the currently selected cell.
EDIT: For future readers: DO NOT mess with TableCell objects, except in cell factory. Use the TableView the way designers intended, or you will be in lot of trouble. If you need data from multiple sources in single table, it is better to make a new class that aggregates all the data and use that as a TableView source.
I just posted an answer that uses this code to edit a Cell. I don't think you can get a reference to the actual table cell as that's internal to the table view.
tp = tv.getFocusModel().getFocusedCell();
tv.edit(tp.getRow(), tp.getTableColumn());
Your method also returns a TablePosition so you can use that as well.
Here's the link https://stackoverflow.com/a/21988562/2855515
This will probably get downvoted because the OP asked about returning the cell itself, rather than what I'll describe, but a Google search led me here for my issue.
I personally ran into issues trying to retrieve data from an individual cell.
java.is.for.desktop offered buggy code related to this matter, that throws an ArrayIndexOutOfBoundsException, but is on the right track. My goal is to offer a better example of that using a lambda.
To access data from a single TableCell:
tableView.getFocusModel().focusedCellProperty().addListener((ObservableValue<? extends TablePosition> observable, TablePosition oldPos, TablePosition pos) -> {
int row = pos.getRow();
int column = pos.getColumn();
String selectedValue = "";
/* pos.getColumn() can return -1 if the TableView or
* TableColumn instances are null. The JavaDocs state
* this clearly. Failing to check will produce an
* ArrayIndexOutOfBoundsException when underlying data is changed.
*/
if ((pos.getRow() != -1) && (pos.getColumn() != -1))
{
selectedValue = tableView.getItems()
.get(row)
.get(column);
if ((selectedValue != null) && (!selectedValue.isEmpty()))
{
// handling if contains data
}
else
{
// handling if doesn't contain data
}
}
});
Edit:
I meant to say ArrayIndexOutOfBoundsException, rather than NullPointerException, I updated this answer to reflect that. I also cleaned up spelling and grammar.
You want to respond to key press? Better don't.
Instead, you could register a listener for focusing of table cells, which would work with arrow keys and mouse clicks on table cells (and even with touch events, oh my, the future is already there).
table.getFocusModel().focusedCellProperty().addListener(
new ChangeListener<TablePosition>() {
#Override
public void changed(ObservableValue<? extends TablePosition> observable,
TablePosition oldPos, TablePosition pos) {
int row = pos.getRow();
int column = pos.getColumn();
String selectedValue = "";
if (table.getItems().size() > row
&& table.getItems().get(row).size() > column) {
selectedValue = table.getItems().get(row).get(column);
}
label.setText(selectedValue);
}
});
In this example, I am using a "classic" TableView with List<String> as column model. (So, your data type could be different than String.) And, of course, that label is just an example from my code.