once again, I scratch my head while working with JavaFX-Charts... :-(
I have a usecase for which I need to replace all series in the chart, I create everything from scratch. When adding after clearing the previous data, the chart assigns the wrong style-classes for default-color to some of the added series...
More precisely, the assigned class-numbers get shifted... why on earth did they implement that with modulo, which by default limits to only 8 (!) different color-symbol-combinations...?!
The first 8 (hint: mod 8...) default-colors are correct:
default-color0
...
default-color7
and for the first run, it continues, also correct, again with 0:
default-color0
...
default-color6 (for 15 series in my example)
for the second run, I get:
default-color0
...
default-color7
default-color7 <-- shift from here
default-color0
...
default-color5
as you can see, the it is shifted, having two times 7 and then only running until 5.
For another run, the shift continues:
default-color0
...
default-color7
default-color6 <-- shift from here
default-color7
default-color0
...
default-color4
the amount of shifting is dependent on the number of series - for 16, everything is fine, for 14, it shifts 2 each time and so on.
In the chart this is nicely visible if you watch the legend changing...
I tried different methods of replacing the data, but none of them was successful, see comments in the code below.
An actual solution, but in my eyes not a very pretty one, is to manually overwrite the style-class, which isn't that easy as well, because of the aggressive habits of the task-class... wtf?!
whatever - if anyone has any helpful idea on that? maybe I'm missing something?
Note: To see the weired results, remove the lines marked as DIRTY-WORKAROUND-SOLUTION. Otherwise it would work... ;-)
Bonus-Question: maybe I'm silly, working with that JavaFX-Charts... some things are cool, but often times, I have a weired feeling about it... are there better alternatives you would recommend me to checo out?
Thanks!
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
public class ChartSeriesReplacing extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
VBox root = new VBox();
ScatterChart<Number, Number> chart = new ScatterChart<>(new NumberAxis(), new NumberAxis());
Button refresh = new Button("Refresh");
refresh.setOnAction(clicked -> {
List<XYChart.Series<Number, Number>> seriesList = new ArrayList<>();
for (int i = 0; i < 15; i++) {
XYChart.Series<Number, Number> series = new XYChart.Series<>();
series.setName(i + "");
XYChart.Data<Number, Number> data = new XYChart.Data<>(Math.random(), Math.random());
int finalI = i;
data.nodeProperty().addListener(__ -> {
if (data.getNode() != null) {
data.getNode().getStyleClass().addListener((InvalidationListener) ___ -> {
System.out.println("[pre] Data #" + finalI + ": Style = " + data.getNode().getStyleClass());
// DIRTY-WORKAROUND-SOLUTION:
// (has to live in listener, otherwise the chart erases any custom styling on adding... :-/ )
String colorString = "default-color" + finalI % 8;
data.getNode().getStyleClass().removeIf(s -> s.startsWith("default-color") && !s.equals(colorString));
if (!data.getNode().getStyleClass().contains(colorString)) {
data.getNode().getStyleClass().add(colorString);
}
// --------------------------
System.out.println("[post] Data #" + finalI + ": Style = " + data.getNode().getStyleClass());
});
}
});
series.getData().add(data);
seriesList.add(series);
}
System.out.println("-----------------");
// What I tried:
// 1)
chart.getData().setAll(seriesList);
// 2)
// chart.dataProperty().setValue(FXCollections.observableArrayList(seriesList));
// 3)
// chart.getData().clear();
// 3a)
// chart.getData().addAll(seriesList);
// 3b)
// seriesList.forEach(series->chart.getData().add(series));
});
root.getChildren().add(chart);
root.getChildren().add(refresh);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
Java-Version jdk1.8.0_171 (x64)
Related
I have a program that at some point (may) displays two warnings - one about errors - those are in red, and one about warnings - those are in orange.
I wonder however if there is a way - using css - to have just one warning with some text red and some text orange.
Here is an example of what I want to achieve (the two can be separated into "sections"):
RED ERROR1
RED ERROR2
RED ERROR3
ORANGE WARNING1
ORANGE WARNING2
I've seen some answers pointing to RichTextFX like this one, however I don't see (or don't know) how that could apply to generic Alerts. Is that even possible, without writing some custom ExpandedAlert class?
The Alert class inherits from Dialog, which provides a pretty rich API and allows arbitrarily complex scene graphs to be set via the content property.
If you just want static text with different colors, the simplest approach is probably to add labels to a VBox; though you could also use more complex structures such as TextFlow or the third-party RichTextFX mentioned in the question if you need.
A simple example is:
import java.util.Random;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class App extends Application {
private final Random rng = new Random();
private void showErrorAlert(Stage stage) {
Alert alert = new Alert(Alert.AlertType.ERROR);
int numErrors = 2 + rng.nextInt(3);
int numWarnings = 2 + rng.nextInt(3);
VBox errorList = new VBox();
for (int i = 1 ; i <= numErrors ; i++) {
Label label = new Label("Error "+i);
label.setStyle("-fx-text-fill: red; ");
errorList.getChildren().add(label);
}
for (int i = 1 ; i <= numWarnings ; i++) {
Label label = new Label("Warning "+i);
label.setStyle("-fx-text-fill: orange; ");
errorList.getChildren().add(label);
}
alert.getDialogPane().setContent(errorList);
alert.initOwner(stage);
alert.show();
}
#Override
public void start(Stage stage) {
Button showErrors = new Button("Show Errors");
showErrors.setOnAction(e -> showErrorAlert(stage));
BorderPane root = new BorderPane(showErrors);
Scene scene = new Scene(root, 400, 400);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
which gives this result:
Ok so from my stand point my code is pretty decent enough to get a passing grade but I am having trouble adding a simple refresh/shuffle button. NOT USING the aids of JOptionPane.
Eclipse doesnt seem to recognize that I have created the button which doesnt make sense at all for me because its telling me something about a Node which the Button is in fact a node and it is created. But when I go into another class and add another button with the 3 line example it simply works. But when I move it to my homework program it simply gives me an error on the add method which breaks the whole program!
Says
"The method add(Node) in the type List is not applicable for the arguements (Button)"
Could anyone shed some light of where I could be going wrong in my code? It has to be something along the a node to string conversion or something I just cant seem to figure it out. Willing to take any hints given to me but please DO NOT SOLVE THE PROBLEM FOR ME.
Here is the question from the book basically.
"Write a program that lets the user click the refresh button to display four cards from a deck of 54 cards."
I just need some help on the button thats all. I literally have the rest.
Here is my code so far.
I Have left the imports out as there is just too many.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.stage.Stage;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import java.awt.Button;
import java.io.File;
import java.util.ArrayList;
public class Cards extends Application
{
public void start(Stage primaryStage)
{
ArrayList<String> cards = new ArrayList<>(); //Array list
Shuffle(cards); //Shuffles the Cards
String file1 = new File("cards" + "/" + cards.get(1) + ".png").toURI().toString();
String file2 = new File("cards" + "/" + cards.get(2) + ".png").toURI().toString();
String file3 = new File("cards" + "/" + cards.get(3) + ".png").toURI().toString();
String file4 = new File("cards" + "/" + cards.get(4) + ".png").toURI().toString();
Pane pane = new HBox(20); //Creates the Box for the Images
pane.setPadding(new Insets(5, 5, 5, 5)); //Spreads the Images out
Image image = new Image(file1); //Creates the String Image
Image image2 = new Image(file2);
Image image3 = new Image(file3);
Image image4 = new Image(file4);
pane.getChildren().add(new ImageView(image)); //Adds the First Image
ImageView view1 = new ImageView(image);
view1.setFitHeight(100);
view1.setFitWidth(100);
pane.getChildren().add(new ImageView(image2)); //Adds the Second Image
ImageView view2 = new ImageView(image2);
view2.setFitHeight(100);
view2.setFitWidth(100);
pane.getChildren().add(new ImageView(image3)); //Add the Third Image
ImageView view3 = new ImageView(image3);
view3.setFitHeight(100);
view3.setFitWidth(100);
pane.getChildren().add(new ImageView(image4)); //Add the Fourth Image
ImageView view4 = new ImageView(image4);
view4.setFitHeight(100);
view4.setFitWidth(100);
HBox hbox = new HBox(5); //Creates the Box for the Button
Button shuffle = new Button("Shuffle"); //Creates the Button
hbox.getChildren().add(shuffle); //Should add the button but doesn't
shuffle.addActionListener( e -> //Listener for the button
{
Shuffle(cards);
});
BorderPane pane2 = new BorderPane();/ /Creates the Pane for the Button
pane2.setCenter(pane); //Sets the cards in the Center
pane2.setBottom(hbox); //Sets the Button on the bottom
BorderPane.setAlignment(hbox, Pos.CENTER);
hbox.setAlignment(Pos.BOTTOM_CENTER);//Aligns the Button to BOT_CENTER
Scene scene = new Scene(pane2); //Creates the Scene
primaryStage.setTitle("Cards");
primaryStage.setScene(scene);
primaryStage.show();
}
public void Shuffle(ArrayList<String> cards)
//Allows the cards to Shuffle when called.
{
for (int i = 0; i <= 53; i++) //Sets the Number of Cards in Deck
cards.add(String.valueOf(i+1));
java.util.Collections.shuffle(cards);
}
public static void main(String[] args)
{
launch(args);
}
}
You're using the AWT-button with your import java.awt.Button;, that's why you can use the method public void addActionListener(ActionListener l).
Replace your import to import javafx.scene.control.Button;. Furthermore you could use (analogue to your code) the following lambda:
shuffle.setOnAction( (x) -> //Listener for the button
{
Shuffle(cards);
});
Give it a try :)
I'm trying to generate an audio waveform by reading an existing .wav file that updates as the music is playing in real time. For example, if the song is played, I want the waveform to be continuously generated as the song progresses. So far I was able to read data from a .wav file and generate a STATIC waveform using javafx for roughly the first 2500 samples of the song. I compared the waveform to what it looks like in audacity and it matches it pretty well. Here's what it looks like:
However, I'm confused on how I would make the waveform generate not just statically for a particular window, but generate a constantly updated waveform through the duration of the entire song. I was looking into Realtime charts and using ScheduledExecutorService but I can't seem to make the line graph draw a new point faster than once every second without it causing issues. Is there another way I should go about doing this? I've spent a lot of time on this and don't want to give up now. I attached my code below, give it a look. Thanks.
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.shape.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.geometry.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.scene.text.Text;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.Group;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.TargetDataLine;
import java.net.*;
import java.io.*;
import java.nio.*;
public class Practice2 extends Application {
public static AudioInputStream audioInputStream;
#Override
public void start(Stage primaryStage) {
int channelCounter = 2; //used to jump between left and right channel in the loop
try {
File file = new File("testData/record.wav");
byte[] data = new byte[(int)file.length()];
BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
in.read(data);
in.close();
File leftChannelFile = new File("audioData/leftChannelAmpData.txt");
File rightChannelFile = new File("audioData/rightChannelAmpData.txt");
//Defining the x axis
NumberAxis xAxis = new NumberAxis(0, 2490, 1);
xAxis.setLabel("Samples");
//Defining the y axis
NumberAxis yAxis = new NumberAxis (-675, 675, 1);
yAxis.setLabel("Amplitude");
//Creating the line chart
LineChart linechart = new LineChart(xAxis, yAxis);
linechart.setPrefHeight(1350); //picked 1350 and 2500 because it is slightly less than the height and width of my monitor. Will definitely change these values in the future
linechart.setPrefWidth(2500);
linechart.setMaxHeight(1350);
linechart.setMaxWidth(2500);
//Prepare XYChart.Series objects by setting data
XYChart.Series series = new XYChart.Series();
series.setName("Amplitude from sample");
for (int i=44; i < data.length - 1; i+=2) { //start at the 44th byte of the .wav file. This skips the header info and reads the raw data
if (channelCounter % 2 == 0) { //This represents the loop for the left channel
channelCounter++;
short tempAmp = calculateAmpValue(data[i], data[i+1]); // Since the amplitude value for each sample is represented by 2 bytes instead of 1, this function takes the next 2 bytes and gives an integer representation for it (-32728 to 32727)
if (i < 2491)
series.getData().add(new XYChart.Data(i-43, tempAmp / 50)); //divide by 50 so the amplitude values can fit within the range of my graph.
}
else if (channelCounter % 2 == 1) { //this represents the loop for the right channel. ***I'M ONLY BUILDING A WAVEFORM FOR THE LEFT CHANNEL AT THE MOMENT. ***
channelCounter++;
short tempAmp = calculateAmpValue(data[i], data[i+1]);
}
}
//Setting the data to Line chart
linechart.getData().add(series);
Group root = new Group(linechart);
Scene scene = new Scene(root, 2500, 1350);
primaryStage.setTitle("Generated Waveform");
primaryStage.setScene(scene);
primaryStage.show();
}
catch (FileNotFoundException ex){ex.printStackTrace();}
catch (IOException ex){ex.printStackTrace();}
}
public static short calculateAmpValue(byte byte1, byte byte2) {
ByteBuffer bb = ByteBuffer.allocate(2);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.put(byte1);
bb.put(byte2);
short ampVal = bb.getShort(0);
return ampVal;
}
public static void main(String[] args) {
Application.launch(args);
}
}
If I were looking to draw a real-time chart with JavaFX, I'd probably assume I'd be using AnimationTimer for the drawing. It runs at 60 fps. So whatever is being drawn would reflect changes made in that time period. If the audio is 44100 fps, that's 735 audio data points per animation-cycle period. I'm not clear what you are attempting, though. Are you trying to write the equivalent of an oscilloscope?
I have a curious question about the graph LineChart JavaFX.
I have this graph:
dots forming a "jump" on the X axis (as shown by the two red points I scored) and therefore JavaFX draws me the line between these two points. How do I remove that line between each "jump"?
I post the code:
public class ControllerIndividua {
public static void plotIndividuaFull(String path, Stage stage, String name) {
final NumberAxis xAxisIntensity = new NumberAxis(); //Dichiarazione asseX
final NumberAxis yAxisIntensity = new NumberAxis();//Dichiarazione asseY
DetectionS1.countS1();
//Dichiarazione del tipo di grafico
final LineChart<Number, Number> lineChartIntensity = new LineChart<Number, Number>(xAxisIntensity,yAxisIntensity);
ArrayList<Double> extractedData; //Lista dei valori dello dell' intensità
ArrayList<Double> extractedTime; //Lista del tempo
ArrayList<Double> extractedS1; //Lista del tempo
ArrayList<Double> extractedS1Time; //Lista del tempo
//Gestione e settaggio del grafico
lineChartIntensity.getData().clear();
try {
//Popolamento delle liste
extractedTime = IntensityExtractor.pointsTime();
extractedData = IntensityExtractor.pointsIntensity();
extractedS1 = DetectionS1.S1pitch();
extractedS1Time = DetectionS1.pointsS1Time();
XYChart.Series<Number, Number> series = new XYChart.Series<Number, Number>();
XYChart.Series<Number, Number> seriesS1 = new XYChart.Series<Number, Number>(); //Creazione seconda serie
series.setName("Intensità di:\t" + name.toUpperCase());
for (int j = 0; j < extractedS1.size(); j++) {
seriesS1.getData().add(new XYChart.Data<Number, Number>(extractedS1Time.get(j), extractedS1.get(j)));
lineChartIntensity.getStyleClass().add("CSSintensity");
}
//Creazione finestra e stampa del grafico
Scene scene = new Scene(lineChartIntensity, 1000, 600);
lineChartIntensity.getData().addAll(series,seriesS1);
scene.getStylesheets().add("application/application.css");
stage.setScene(scene);
stage.show();
} catch (java.lang.Exception e) {
e.printStackTrace();
}
}
}
Someone also has a little idea on how I could do?
Thank you all in advance.
This is an old question with an accepted answer but I came across it and was curious. I wanted to know if it was possible to put a gap in a LineChart (at least without having to create a custom chart implementation). It turns out that there is. The solution is kind of hacky and brittle. It involves getting the Path by using XYChart.Series.getNode() and manipulating the list of PathElements. The following code gives an example:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
LineChart<Number, Number> chart = new LineChart<>(new NumberAxis(), new NumberAxis());
chart.getXAxis().setLabel("X");
chart.getYAxis().setLabel("Y");
chart.setLegendVisible(false);
chart.getData().add(new XYChart.Series<>());
for (int x = 0; x <= 10; x++) {
chart.getData().get(0).getData().add(new XYChart.Data<>(x, Math.pow(x, 2)));
}
/*
* Had to wrap the call in a Platform.runLater otherwise the Path was
* redrawn after the modifications are made.
*/
primaryStage.setOnShown(we -> Platform.runLater(() -> {
Path path = (Path) chart.getData().get(0).getNode();
LineTo lineTo = (LineTo) path.getElements().get(8);
path.getElements().set(8, new MoveTo(lineTo.getX(), lineTo.getY()));
}));
primaryStage.setScene(new Scene(new StackPane(chart), 500, 300));
primaryStage.setTitle("LineChart Gap");
primaryStage.show();
}
}
This code results in the following:
This is possible because the ObservableList of PathElements seems to be a MoveTo followed by a bunch of LineTos. I simply picked a LineTo and replaced it with a MoveTo to the same coordinates. I haven't quite figured out which index of LineTo matches with which XYChart.Data, however, and picked 8 for the example randomly.
There are a couple of issues with this solution. The first and obvious one is that this relies on the internal implementation of LineChart. The second is where the real brittleness comes from though. Any change to the data, either axes' value ranges, the chart's width or height, or pretty much anything that causes the chart to redraw itself will cause the Path to be recomputed and redrawn. This means if you use this solution you'll have to reapply the modification every time the chart redraws itself.
I made a GapLineChart that automatically sets a MoveTo like Slaw pointed it whenever it sees a Double.NaN. You can find it here https://gist.github.com/sirolf2009/ae8a7897b57dcf902b4ed747b05641f9. Check the first comment for an example
I checked it. It is not possible to change only one part of this line. Because this is one long Path and you can't change one element of Path.
I think the only way is add each "jump" in different series.
I was in need of adapting LineChart so that it drew line segments, so each pair of data points created a single line segment. It's pretty straight-forward to do by extending LineChart and overriding the layoutPlotChildren function, although I had to delete an animation y-scaling variable as it was private in the super class, but I'm not animating the graph so it was fine. Anyway, here is the Kotlin version (which you can easily adapt to Java, or just copy the Java LineChart layoutPlotChildren code and adapt). It's easy to adapt to your circumstances, just change the contents of the for loop at the very end.
import javafx.scene.chart.Axis
import javafx.scene.chart.LineChart
import javafx.scene.shape.LineTo
import javafx.scene.shape.MoveTo
import javafx.scene.shape.Path
import java.util.*
import kotlin.collections.ArrayList
class GapLineChart<X, Y>(xAxis: Axis<X>?, yAxis: Axis<Y>?) : LineChart<X, Y>(xAxis, yAxis) {
public override fun layoutPlotChildren() {
val constructedPath = ArrayList<LineTo>(data.size)
for (seriesIndex in 0 until data.size) {
val series = data[seriesIndex]
if (series.node is Path) {
val seriesLine = (series.node as Path).elements
val it = getDisplayedDataIterator(series)
seriesLine.clear()
constructedPath.clear()
var parity = true
while (it.hasNext()) {
val item = it.next()
val x = xAxis.getDisplayPosition(item.xValue)
val y = yAxis.getDisplayPosition(yAxis.toRealValue(yAxis.toNumericValue(item.yValue)))
if (java.lang.Double.isNaN(x) || java.lang.Double.isNaN(y)) {
continue
}
if(parity)
constructedPath.add(LineTo(x, y))
val symbol = item.node
if (symbol != null) {
val w = symbol.prefWidth(-1.0)
val h = symbol.prefHeight(-1.0)
symbol.resizeRelocate(x - w / 2, y - h / 2, w, h)
}
}
when (axisSortingPolicy) {
SortingPolicy.X_AXIS -> constructedPath.sortWith(Comparator { e1, e2 -> java.lang.Double.compare(e1.x, e2.x) })
SortingPolicy.Y_AXIS -> constructedPath.sortWith(Comparator { e1, e2 -> java.lang.Double.compare(e1.y, e2.y) })
}
if (!constructedPath.isEmpty()) {
for (i in 0 until constructedPath.size-1 step 2) {
seriesLine.add(MoveTo(constructedPath[i].x, constructedPath[i].y))
seriesLine.add(LineTo(constructedPath[i+1].x, constructedPath[i+1].y))
}
}
}
}
}
}
Is there a way to determine the first and last visible row of a listview? In other words I'm looking for two indexes into an array that populates a listview which represent the top and the bottom row of the 'display window'.
You could get the VirtualFlow of the ListView which has methods for getting the first and last rows.
Example:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.scene.Scene;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import com.sun.javafx.scene.control.skin.VirtualFlow;
public class ListViewSample extends Application {
#Override
public void start(Stage stage) {
VBox box = new VBox();
ListView<Integer> list = new ListView<>();
ObservableList<Integer> items = FXCollections.observableArrayList();
for( int i=0; i < 100; i++) {
items.add(i);
}
list.setItems(items);
box.getChildren().add(list);
VBox.setVgrow(list, Priority.ALWAYS);
Scene scene = new Scene(box, 200, 200);
stage.setScene(scene);
stage.show();
VirtualFlow flow = (VirtualFlow) list.lookup( ".virtual-flow");
flow.addEventFilter(Event.ANY, event -> {
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
IndexedCell last = flow.getLastVisibleCellWithinViewPort();
System.out.println( list.getItems().get( first.getIndex()) + " - " + list.getItems().get( last.getIndex()) );
});
}
public static void main(String[] args) {
launch(args);
}
}
You see the fully visible first and last items in the console.
ps: I leave the no data check and event handling to you
Alternate version without css lookup:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import com.sun.javafx.scene.control.skin.VirtualFlow;
public class ListViewSample extends Application {
ListView<String> list = new ListView<String>();
#Override
public void start(Stage stage) {
VBox box = new VBox();
ListView<Integer> list = new ListView<>();
ObservableList<Integer> items = FXCollections.observableArrayList();
for( int i=0; i < 100; i++) {
items.add(i);
}
list.setItems(items);
box.getChildren().add(list);
VBox.setVgrow(list, Priority.ALWAYS);
Scene scene = new Scene(box, 200, 200);
stage.setScene(scene);
stage.show();
VirtualFlow virtualFlow = null;
for( Node node: list.getChildrenUnmodifiable()) {
if( node instanceof VirtualFlow) {
virtualFlow = (VirtualFlow) node;
}
}
final VirtualFlow flow = virtualFlow;
flow.addEventFilter(Event.ANY, event -> {
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
IndexedCell last = flow.getLastVisibleCellWithinViewPort();
System.out.println( list.getItems().get( first.getIndex()) + " - " + list.getItems().get( last.getIndex()) );
});
}
public static void main(String[] args) {
launch(args);
}
}
UPDATE
VirtualFlow is available only after the ListView has been rendered, because it uses Layout parameters which are not available until after the ListView is visible on the stage. So I had to make sure that I got the VirtualFlow when it was certain that the ListView had been rendered. Since I was manipulating the list with various methods I call this method at the end of each method:
private VirtualFlow flow;
private void updateListView(int centreIndex) {
if (flow == null)
flow = (VirtualFlow) myListView.lookup(".virtual-flow");
if (flow != null){
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
IndexedCell last = flow.getLastVisibleCellWithinViewPort();
System.out.println(first.getIndex() + " - " + last.getIndex());
}
// Now the list can be selectively 'redrawn' using the scollTo() method,
// and using the .getSelectionModel().select(centreIndex) to set the
// desired cell
}
It's bit of a hack, but it works. Using layout parameters does have a drawback though that needs to be considered. If the height of the ListView is only 1 pixel less than the total height of all rows, n number of rows will be visible, but the flow will report n-1 rows which will appear to be a discrepancy at first. Hence keeping a fixed layout height is imperative. At least now by using scrollTo(..) I have control over the position of the selected item in the list (I want to keep it centred in the list display when an item is dragged through the list). This solution leaves me feeling uneasy, but it seems to be the only 'simple' way.
Just a note on the odd-looking logic. It seems that getting the flow takes time, while the program keeps executing. The second (flow != null) is necessary to avoid a NullPointerException.
UPDATE 2
My hack turns out not to work. The whole hack is dependent on timing. Rendering is done on a different thread and as soon as I changed the order of instantiation of classes in my app, I got a NullPointerException again. I turned to the Java doc:
"JavaFX is not thread safe and all JavaFX manipulation should be run on the JavaFX processing thread. If you allow a JavaFX application to interact with a thread other than the main processing thread, unpredictable errors will occur"
And they do! So forget the above - it does not work and will make you scratch your head (and more!) trying to debug it ;-)