I have a GridPane populated with ToggleButtons. First row and column in that GridPane holds Text object labels.
I am unable to center the Text objects with the ToggleButtons so the text appears in the middle, using css.
(This answer shows how to achieve it by using GridPane.setHalignment(node, HPos.CENTER);).
MCVE if needed :
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class GridTest extends Application {
private static final int COLS = 5, ROWS = 3;
#Override public void start(Stage stage) {
Scene scene = new Scene(makeGrid());
scene.getStylesheets().add(getClass().
getResource("GridTest.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
private Pane makeGrid() {
GridPane grid = new GridPane();
for(int rowIndex = 0; rowIndex < ROWS ; rowIndex++) {
Node[] nodes = new Node[COLS];
Node node;
for(int colIndex = 0; colIndex < COLS ; colIndex++) {
if (rowIndex == 0){ //col header;
String txt = (colIndex == 0) ?
" " : String.valueOf(colIndex);
node = new Text(txt);
}else if (colIndex == 0){//row header
node = new Text(String.valueOf(rowIndex));
}else {
node= new ToggleButton();
}
nodes[colIndex]= node;
}
grid.addRow(rowIndex, nodes);
}
return grid;
}
public static void main(String[] args) {
Application.launch(args);
}
}
CSS:
Text{
-fx-text-alignment: center;
}
GridPane {
-fx-hpos:center ;
-fx-hgap: 5;
-fx-vgap: 5;
-fx-padding:10;
}
ToggleButton {
-fx-pref-width:30;
}
Based on the comments posted by # James_D and #fabian and previous answers there are two options to get the text labels centered.
One option, as posted in the question, does not use css. It requires slight modification of the makeGrid:
//see: https://stackoverflow.com/a/35438985/3992939
GridPane.setHalignment(node, HPos.CENTER); //added line
nodes[colIndex]= node;
This solutions does not change the (non resizable) Text. It simple centers it within its GridPane parent column.
The other option involves changing the Text lables to Labels :
private Pane makeGrid() {
GridPane grid = new GridPane();
for(int rowIndex = 0; rowIndex < ROWS ; rowIndex++) {
Node[] nodes = new Node[COLS];
Node node;
for(int colIndex = 0; colIndex < COLS ; colIndex++) {
if (rowIndex == 0){ //col header;
String txt = (colIndex == 0) ? " " : String.valueOf(colIndex);
node = new Label(txt); //** changed
}else if (colIndex == 0){//row header
node = new Label(String.valueOf(rowIndex)); //** changed
}else {
node= new ToggleButton();
}
nodes[colIndex]= node;
}
grid.addRow(rowIndex, nodes);
}
return grid;
}
And using css (or additional code) to set the label's width to maximum and center the text :
GridPane .label{
-fx-alignment: center;
-fx-max-width: Infinity;
}
GridPane {
-fx-hgap: 5;
-fx-vgap: 5;
-fx-padding:10;
}
ToggleButton {
-fx-pref-width:30;
}
Related
I try to write a code that find the label on which one have clicked.
Using an event listener, I got the positions of the event using getX() and getY().
However, I cannot find the adequate methods for the label positions in order to compare them.
Below is my code, and its ouput.
public class Beta extends Application {
final Label[] answerLabel = new Label[4];
#Override
public void start(Stage primaryStage) {
GridPane root = new GridPane();
root.setGridLinesVisible(true);
final int numCols = 7 ;
final int numRows = 12 ;
//final Label[] answerLabel = new Label[4];
for (int i = 0; i < numCols; i++) {
ColumnConstraints colConst = new ColumnConstraints();
colConst.setPercentWidth(100.0 / numCols);
root.getColumnConstraints().add(colConst);
}
for (int i = 0; i < numRows; i++) {
RowConstraints rowConst = new RowConstraints();
rowConst.setPercentHeight(100.0 / numRows);
root.getRowConstraints().add(rowConst);
}
for(int i = 0; i<4; i++){
answerLabel[i] = new Label();
answerLabel[i].setMaxWidth(Double.MAX_VALUE);
answerLabel[i].setMaxHeight(Double.MAX_VALUE);
answerLabel[i].setStyle("-fx-background-color: blue;-fx-font-size: 7pt;-fx-padding: 0;");
answerLabel[i].setPadding(new Insets(10));
answerLabel[i].setCursor(Cursor.HAND);
root.add(answerLabel[i], 3, i +5, 1, 1);
answerLabel[i].setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
answerLabelPressed(e);
}
});
}
primaryStage.setScene(new Scene(root, 900, 500));
primaryStage.show();
}
private void answerLabelPressed(MouseEvent e)
{
int labelSelected;
double px = e.getX();
double py = e.getY();
System.out.println("px = " + px + " py = " + py);
for (labelSelected = 0; labelSelected < 4; labelSelected++)
{
System.out.println("answerLabel[labelSelected].getLayoutX() = " + answerLabel[labelSelected].getLayoutX());
System.out.println("answerLabel[labelSelected].getLayoutY() = " + answerLabel[labelSelected].getLayoutY());
}
}
public static void main(String[] args) {
launch(args);
}
}
px = 42.0 py = 7.0
answerLabel[labelSelected].getLayoutX() = 386.0
view.answerLabel[labelSelected].getLayoutY() = 208.0
answerLabel[labelSelected].getLayoutX() = 386.0
view.answerLabel[labelSelected].getLayoutY() = 250.0
answerLabel[labelSelected].getLayoutX() = 386.0
view.answerLabel[labelSelected].getLayoutY() = 292.0
answerLabel[labelSelected].getLayoutX() = 386.0
view.answerLabel[labelSelected].getLayoutY() = 333.0
Upadate: The main purpose was to find/check the equivalent JavaFX methods of those used in Java Swing.
An alternative and better algorithm beeing as one can read in most popular Java books :
MouseListener ml = new MouseListener() {
public void mouseClicked(MouseEvent e) {
report("mouseClicked", e.paramString());
}
In Java Swing, one should read :
Point p = e.getComponent().getLocation();
System.out.println("px = " + p.getX() + " py = " + p.getY());
for (labelSelected = 0; labelSelected < 4; labelSelected++)
{
System.out.println("answerLabel[labelSelected].getX() = " + answerLabel[labelSelected].getX());
System.out.println("answerLabel[labelSelected].getY() = " + answerLabel[labelSelected].getY());
}
I try to write a code that find the label on which one have clicked.
You create four labels, and you create a listener for each label. Each listener is only registered with one label.
So there is no need to get your hands dirty with the coordinates of the click (the event handling mechanism has already done all of that for you, when it decided to which node to dispatch the event). Just reference the label that was clicked:
public class Beta extends Application {
final Label[] answerLabel = new Label[4];
#Override
public void start(Stage primaryStage) {
GridPane root = new GridPane();
root.setGridLinesVisible(true);
final int numCols = 7 ;
final int numRows = 12 ;
for (int i = 0; i < numCols; i++) {
ColumnConstraints colConst = new ColumnConstraints();
colConst.setPercentWidth(100.0 / numCols);
root.getColumnConstraints().add(colConst);
}
for (int i = 0; i < numRows; i++) {
RowConstraints rowConst = new RowConstraints();
rowConst.setPercentHeight(100.0 / numRows);
root.getRowConstraints().add(rowConst);
}
for(int i = 0; i<4; i++){
answerLabel[i] = new Label();
answerLabel[i].setMaxWidth(Double.MAX_VALUE);
answerLabel[i].setMaxHeight(Double.MAX_VALUE);
answerLabel[i].setStyle("-fx-background-color: blue;-fx-font-size: 7pt;-fx-padding: 0;");
answerLabel[i].setPadding(new Insets(10));
answerLabel[i].setCursor(Cursor.HAND);
root.add(answerLabel[i], 3, i +5, 1, 1);
Label currentLabel = answerLabel[i];
int currentIndex = i ;
answerLabel[i].setOnMouseClicked(event -> {
System.out.println("Clicked on label "+currentIndex);
// just for demo: in real life use external stylesheets
// and pseudoclasses, etc.
for (Label label : answerLabel) {
label.setStyle("-fx-background-color: blue;-fx-font-size: 7pt;-fx-padding: 0;");
}
currentLabel.setStyle("-fx-background-color: gold;-fx-font-size: 7pt;-fx-padding: 0;");
});
}
primaryStage.setScene(new Scene(root, 900, 500));
primaryStage.show();
}
}
It is not necessary to manually compute which node the mouse clicked on. That calculation is already done for you by the framework. That's how the framework knows which event handlers to invoke. If you simply add a unique handler to each node, then when that handler is invoked only that node could be the source. This is demonstrated in #James_D's answer.
However, if you want to manually compute which node was clicked (e.g., for fun or just for learning purposes), then here is a runnable example:
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Border;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
var labels = IntStream.range(0, 12)
.mapToObj(this::createLabel)
.toArray(Label[]::new);
var grid = new GridPane();
grid.setPadding(new Insets(20));
grid.setHgap(20);
grid.setVgap(20);
grid.setOnMouseClicked(e -> {
var clickedOnLabel = getClickedOnLabel(labels, e);
if (clickedOnLabel == null) {
System.out.println("You did not click on a label!");
} else {
System.out.printf("You clicked on a label: '%s'%n", clickedOnLabel.getText());
}
});
int i = 0;
for (int col = 0; col < 4; col++) {
for (int row = 0; row < 3; row++) {
grid.add(labels[i++], col, row);
}
}
primaryStage.setScene(new Scene(grid));
primaryStage.show();
}
private Label getClickedOnLabel(Label[] labels, MouseEvent event) {
for (var label : labels) {
var bounds = label.localToScene(label.getBoundsInLocal());
if (bounds.contains(event.getSceneX(), event.getSceneY())) {
return label;
}
}
return null;
}
private Label createLabel(int n) {
var label = new Label(String.format("Label #%02d", n));
label.setCursor(Cursor.HAND);
label.setPadding(new Insets(5));
label.setFont(Font.font("Monospaced", 15.0));
label.setBorder(Border.stroke(Color.BLACK));
return label;
}
}
The important part, the part which computes which label was clicked on, is here:
private Label getClickedOnLabel(Label[] labels, MouseEvent event) {
for (var label : labels) {
var bounds = label.localToScene(label.getBoundsInLocal());
if (bounds.contains(event.getSceneX(), event.getSceneY())) {
return label;
}
}
return null;
}
It gets the bounds of each Label in the scene's coordinate space, and then tests if the mouse's location—also in the scene's coordinate space—is contained within those bounds. You can use whatever coordinate space you like (e.g., the screen's, the grid pane's, the label's, etc.), as long as you use the same one for both the label's bounds and the mouse's location. Note the mouse's local coordinates (i.e., getX() and getY()) are in the source node's coordinate space. The source node is the node that the currently-being-invoked handler was registered with for the specific event currently being processed (the GridPane in the above example).
But again, for any "real" code, I strongly recommend you use the solution in #James_D's answer.
I decided to create a JavaFX calculator that includes Buttons and Labels and a Pane as my root Parent.
I'm still stuck with the layout, here is a rough sketch of the expected result:
Bear with my poor graphics.
The problem is that I see a blank screen when I run the following code:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.control.Label;
import javafx.scene.control.Button;
import javafx.scene.text.*;
import javafx.scene.paint.Color;
import javafx.geometry.Pos;
/**
* This class is meant to serve as a simple calculator with basic funtions
* to try myself out with javafx
*/
public class CalculatorFX extends Application{
Label question; // This is the label that shows the question
Label answer; // This is the label that shows the answer
Button[] buttons = new Button[19]; // This is the buttons of the calculator
/**
* This is the root parent or container. It is defined out here because
* I hope to know its length and width are crucial for the layout.
*/
Pane root = new Pane();
/**
* This is the length of the Pane
*/
double rootHeight = root.getHeight();
/**
* This is the width of the Pane
*/
double rootWidth = root.getWidth();
public static void main(String[] args){
launch(args);
}
/**
* This start routine sets up the GUI
*/
public void start(Stage stage){
question = new Label("Type your question");
answer = new Label("Answer:");
question.setTextFill(Color.BLACK);
question.setFont( Font.font(null, FontWeight.BOLD, 18) );
question.setStyle("-fx-font: 15pt sans-serif; -fx-padding: 7px; -fx-border-color: darkred; -fx-border-width: 2px; -fx-text-fill: darkred; -fx-background-color: pink; ");
/* Initializing the numerical buttons first using a loop */
// for(int i = 0; i < 10; i++){
// buttons[i] = new Button(i + "");
// }
/* They are configured like this in the way, they would appear on
* the parent node */
buttons[0] = new Button("CLEAR");
buttons[1] = new Button("DEL");
buttons[2] = new Button("CONT");
buttons[3] = new Button("7");
buttons[4] = new Button("8");
buttons[5] = new Button("9");
buttons[6] = new Button("*");
buttons[7] = new Button("4");
buttons[8] = new Button("5");
buttons[9] = new Button("6");
buttons[10] = new Button("/");
buttons[11] = new Button("1");
buttons[12] = new Button("2");
buttons[13] = new Button("3");
buttons[14] = new Button("-");
buttons[15] = new Button("0");
buttons[16] = new Button(".");
buttons[17] = new Button("=");
buttons[18] = new Button("+");
/* Here we set the position of the children */
double unitX = rootWidth/4;
double unitY = rootHeight/9;
double nextX = rootWidth;
double nextY = rootHeight;
for(int lineNum = 6; lineNum >= 0; lineNum--){
nextY = nextY - unitY;
nextX = rootWidth - unitX;
for(int element = 4; element >=0 ; element--){
if(lineNum == 1){// Then we need to fill the lines
if( element == 1)
continue;
//buttons[lineNum + element + 8].relocate(nextX, nextY);
}
if(lineNum == 0){
answer.relocate(0, nextY - unitY);
System.out.println("Relocated answer label");
question.relocate(0,0);
System.out.println("Relocated answer label");
break; // This breaks out of two for loops because
// this is the last lineNum.
}
//buttons[lineNum + element + 8].relocate(nextX, nextY);
nextX = nextX - unitX;
}
}
/* There is also a need to resize the children */
question.setManaged(false);
question.resize(rootWidth, 2*unitY);
System.out.println("Resized label question");
answer.setManaged(false);
answer.resize(rootWidth, 2*unitY);
System.out.println("Resized label answer");
// for(int i = 0; i < buttons.length; i++){
// buttons[i].setManaged(false);
// buttons[i].resize(unitX, unitY);
// }
/* Time to configure them on our root */
root.setPrefWidth(400);
root.setPrefHeight(800);
root.getChildren().addAll(question, answer);
// for(int i = 0; i < buttons.length; i++){
// root.getChildren().add(buttons[i]);
// }
Scene scene = new Scene(root);
scene.getStylesheets().add("myStyle.css");
stage.setScene(scene);
stage.setTitle("CalculatorFX");
stage.show();
} // End of start
} // End of class CalculatorFX
Now, myStyle.css contains:
Button {
-fx-font: bold 16pt "Times New Roman";
-fx-text-fill: darkblue;
}
Label {
-fx-font: 15pt sans-serif;
-fx-padding: 7px;
-fx-border-color: darkred;
-fx-border-width: 2px;
-fx-text-fill: darkred;
-fx-background-color: pink;
}
Help me. Why do I see a blank screen?
To answer your main question:
Yes there is!
In JavaFX, Nodes have visible property:
Specifies whether this Node and any subnodes should be rendered as part of the scene graph.
You control it using the setVisible method.
To help you get started:
As Zephyr and Jamed_D pointed out, you are doing too much work.
I believe there are several correct ways to go about what you are trying to achieve.
A visual editor like SceneBuilder comes to mind. It could help you get a deeper understanding of JavaFX's components. Especially, if building the GUI programmatically is not a must for you.
Here is a simple example to help you get started with one of the ways:
public class App extends Application {
public static void main(String[] args) {
launch();
}
#Override
public void start(Stage stage) {
var label1 = new Label("Type your question: ");
var label2 = new Label("Answer: ");
label1.setPrefHeight(40);
label2.setPrefHeight(40);
label1.setPadding(new Insets(0, 10, 0, 10));
label2.setPadding(new Insets(0, 10, 0, 10));
HBox label1HB = new HBox(label1);
HBox label2HB = new HBox(label2);
VBox vBox = new VBox();
vBox.setFillWidth(true);
vBox.setPrefWidth(Double.MAX_VALUE);
vBox.setPrefHeight(Double.MAX_VALUE);
vBox.getChildren().addAll(label1HB, label2HB);
List<HBox> rows = new ArrayList<>();
for (int row = 0; row < 6; row++) {
HBox hbox = new HBox();
for (int col = 0; col < 4; col++) {
Button button = new Button(row + " " + col);
button.setMaxWidth(Double.MAX_VALUE);
button.setMaxHeight(Double.MAX_VALUE);
HBox.setHgrow(button, Priority.ALWAYS);
hbox.getChildren().add(button);
}
VBox.setVgrow(hbox, Priority.ALWAYS);
rows.add(hbox);
}
vBox.getChildren().addAll(rows);
// Uncomment the following line to see the visible property in action.
// rows.get(2).setVisible(false);
var scene = new Scene(vBox, 400, 800);
stage.setScene(scene);
stage.show();
}
}
I personally like wrapping my nodes within a VBox or an Hbox because it allows me to change the scene's size without worrying about resizing everything again.
You can find many tutorials online explaining how to exploit different JavaFX layouts' properties to achieve better as well as cleaner GUI and code.
It should be mentioned that you are going to have to take care of:
A couple of issues, like what happens when the window is too small to fit your entire content?
Some specifics, like if you want some of your nodes to have a fixed width and height.
etc.
I need to change color of circles in PieChart Legend. I don't know how to get to this property of PieChart. For example I'm able to change color of text in label Legend and I think this is close to the solution.
It shows what I want to change:
#FXML
public PieChart chart;
public ObservableList<PieChart.Data> pieChartData = FXCollections.observableArrayList();
public void chartLoad() {
pieChartData.clear();
List<String> colorList = new ArrayList<>();
for(int i = 0; i < categoryList.getSize(); i++) {
if(categoryList.getByIndex(i).getValue() > 0) {
PieChart.Data data = new PieChart.Data(categoryList.getByIndex(i).getName(),
categoryList.getByIndex(i).getValue());
pieChartData.add(data);
data.getNode().setStyle("-fx-pie-color: " +
categoryList.getByIndex(i).getColor().getName());
colorList.add(categoryList.getByIndex(i).getColor().getName());
}
}
Set<Node> items = chart.lookupAll("Label.chart-legend-item");
int i = 0;
for(Node item : items) {
Label label = (Label) item;
label.setText("sampleText");
label.setStyle("-fx-text-fill: " + colorList.get(i));
System.out.println(label.getChildrenUnmodifiable().toString());
i++;
}
chart.setData(pieChartData);
}
Thank you for your future comments and answers.
Dynamically allocating colors to charts is a bit of a pain. If you have a fixed set of colors, without a predefined mapping from your data to the colors, you can just use an external style sheet, but doing anything else needs (as far as I know) a bit of a hack.
The default modena.css style sheet defines eight constant colors, CHART_COLOR_1 to CHART_COLOR_8. Nodes in a pie chart, including both the "pie slices" and the color swatches in the legend, are assigned a style class from the eight classes default-color0 to default-color7. Each of these style classes by default has -fx-pie-color set to one of the constants. Unfortunately, if the data in the pie chart are changed, these mappings from default-colorx to CHART_COLOR_y change in a way that isn't documented.
So the best approach for your scenario that I can find is:
add the new data to the chart
once all the data are added, for each datum look up the style class that was added to the datum's node
look up all the nodes in the chart that have that style class (this will also give the legend swatches)
update the -fx-pie-color for those nodes to the desired color
The last trap here is that you need to make sure the legend is added to the chart, and that CSS is applied to the chart, so that the lookup works.
public void chartLoad() {
pieChartData.clear();
List<String> colors = new ArrayList<>();
for(int i = 0; i < categoryList.getSize(); i++) {
if(categoryList.getByIndex(i).getValue() > 0) {
PieChart.Data data = new PieChart.Data(categoryList.getByIndex(i).getName(),
categoryList.getByIndex(i).getValue());
pieChartData.add(data);
colors.add(categoryList.getByIndex(i).getColor().getName());
}
}
chart.setData(pieChartData);
chart.requestLayout();
chart.applyCSS();
for (int i = 0 ; i < pieChartData.size() ; i++) {
PieChart.Data d = pieChartData.get(i);
String colorClass = "" ;
for (String cls : d.getNode().getStyleClass()) {
if (cls.startsWith("default-color")) {
colorClass = cls ;
break ;
}
}
for (Node n : chart.lookupAll("."+colorClass)) {
n.setStyle("-fx-pie-color: "+colors.get(i));
}
}
}
Here's a quick, complete demo of this approach:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class PieChartTest extends Application {
private final Random rng = new Random();
#Override
public void start(Stage primaryStage) throws Exception {
PieChart chart = new PieChart();
Button button = new Button("Generate Data");
button.setOnAction(e -> updateChart(chart));
BorderPane root = new BorderPane(chart);
HBox controls = new HBox(button);
controls.setAlignment(Pos.CENTER);
controls.setPadding(new Insets(5));
root.setTop(controls);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void updateChart(PieChart chart) {
chart.getData().clear();
int numValues = 4 + rng.nextInt(10);
List<String> colors = new ArrayList<>();
List<PieChart.Data> data = new ArrayList<>();
for (int i = 0 ; i < numValues ; i++) {
colors.add(getRandomColor());
PieChart.Data d = new PieChart.Data("Item "+i, rng.nextDouble() * 100);
data.add( d );
chart.getData().add(d) ;
}
chart.requestLayout();
chart.applyCss();
for (int i = 0 ; i < data.size() ; i++) {
String colorClass = "" ;
for (String cls : data.get(i).getNode().getStyleClass()) {
if (cls.startsWith("default-color")) {
colorClass = cls ;
break ;
}
}
for (Node n : chart.lookupAll("."+colorClass)) {
n.setStyle("-fx-pie-color: "+colors.get(i));
}
}
}
private String getRandomColor() {
Color color = Color.hsb(rng.nextDouble() * 360, 1, 1);
int r = (int) (255 * color.getRed()) ;
int g = (int) (255 * color.getGreen());
int b = (int) (255 * color.getBlue()) ;
return String.format("#%02x%02x%02x", r, g, b) ;
}
public static void main(String[] args) {
Application.launch(args);
}
}
This really is a bit of a hack, so better solutions are obviously welcome.
Unpleasantly surprised by TextArea CSS font sizes having wacky effects on the sizes of the scroll bars, I'm trying to get control of the sizes myself. Please refer to the following SSCCE. I can easily control the vertical scroll bar, but the horizontal bar is simply ignoring the sizes I'm setting. Am I expecting something unreasonable here, or is this (yet another) bug in JavaFX? Thanks!
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
TextArea textArea = new TextArea();
int lineCount = 100;
int wordCount = 70;
StringBuilder sb = new StringBuilder();
for (int lineNbr = 0; lineNbr < lineCount; lineNbr++) {
for (int wordNbr = 0; wordNbr < wordCount; wordNbr++) {
sb.append("Sample");
}
sb.append(System.getProperty("line.separator"));
}
textArea.setText(sb.toString());
StackPane root = new StackPane();
root.getChildren().add(textArea);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
double prefSize = 50;
ScrollBar vertScrollBar = (ScrollBar)textArea.lookup(".scroll-bar:vertical");
ScrollBar horizScrollBar = (ScrollBar)textArea.lookup(".scroll-bar:horizontal");
vertScrollBar.setPrefWidth(prefSize); // This works great!
horizScrollBar.setPrefHeight(prefSize); // This doesn't do anything!
horizScrollBar.setMinHeight(prefSize); // Nor does this
horizScrollBar.setPrefWidth(prefSize); // Nor this
horizScrollBar.setMinWidth(prefSize); // Nor this
}
}
ScrollBar vertScrollBar = (ScrollBar) textArea.lookup(".scroll-bar:vertical");
ScrollBar horizScrollBar = (ScrollBar) textArea.lookup(".scroll-bar:horizontal");
System.out.println(vertScrollBar + " " + horizScrollBar);
ScrollBar#35ef2d94[styleClass=scroll-bar]
ScrollBar#35ef2d94[styleClass=scroll-bar]
Same object (vertical ScrollBar).
ScrollPane sPane = (ScrollPane)textArea.getChildrenUnmodifiable().get(0);
ScrollBar horizScrollBar = (ScrollBar)sPane.getChildrenUnmodifiable().get(2);
horizScrollBar.setPrefHeight(prefSize); // This does something!
Update: Better way to receive the two ScrollBars.
ScrollBar[] bars = new ScrollBar[2];
textArea.lookupAll(".scroll-bar").toArray(bars);
bars[0].setPrefWidth(prefSize);
bars[1].setPrefHeight(prefSize);
Edit: Explanation for similar problem with lookupAll(...) and pseudo classes here and here.
Lookups are generally pretty fragile (and I don't think they're really intended to be robust); as noted in the links from the other answer they don't appear to support pseudoclasses.
You can of course just use CSS for this:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
TextArea textArea = new TextArea();
int lineCount = 100;
int wordCount = 70;
StringBuilder sb = new StringBuilder();
for (int lineNbr = 0; lineNbr < lineCount; lineNbr++) {
for (int wordNbr = 0; wordNbr < wordCount; wordNbr++) {
sb.append("Sample");
}
sb.append(System.getProperty("line.separator"));
}
textArea.setText(sb.toString());
StackPane root = new StackPane();
root.getChildren().add(textArea);
Scene scene = new Scene(root, 300, 250) ;
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
}
}
with the following in style.css:
.scroll-pane .scroll-bar:horizontal {
-fx-pref-height: 50 ;
}
.scroll-pane .scroll-bar:vertical {
-fx-pref-width: 50 ;
}
This approach, of course, is far more convenient if you have multiple scroll panes in your application and want them all to have the same style of scroll bars.
Problem
You can add an event listener to a node which detects mouse movement over it. This doesn't work if a mouse button was pressed before you moved over the node.
Question
Does anyone know how to detect mouse movement while the button is pressed? So far I've only found a solution by using the MOUSE_DRAGGED event and then instead of using getSource() using getPickResult() and evaluating the PickResult data.
Here's the code including Uluk's solution. The old and new solution are switchable via the useNewVersion (Uluk's version) boolean:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.PickResult;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
boolean useNewVersion= true;
int rows = 10;
int columns = 20;
double width = 1024;
double height = 768;
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = new BorderPane();
// create grid
Grid grid = new Grid( columns, rows, width, height);
MouseGestures mg = new MouseGestures();
// fill grid
for (int row = 0; row < rows; row++) {
for (int column = 0; column < columns; column++) {
Cell cell = new Cell(column, row);
mg.makePaintable(cell);
grid.add(cell, column, row);
}
}
root.setCenter(grid);
// create scene and stage
Scene scene = new Scene(root, width, height);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
private class Grid extends Pane {
int rows;
int columns;
double width;
double height;
Cell[][] cells;
public Grid( int columns, int rows, double width, double height) {
this.columns = columns;
this.rows = rows;
this.width = width;
this.height = height;
cells = new Cell[rows][columns];
}
/**
* Add cell to array and to the UI.
*/
public void add(Cell cell, int column, int row) {
cells[row][column] = cell;
double w = width / columns;
double h = height / rows;
double x = w * column;
double y = h * row;
cell.setLayoutX(x);
cell.setLayoutY(y);
cell.setPrefWidth(w);
cell.setPrefHeight(h);
getChildren().add(cell);
}
}
private class Cell extends StackPane {
int column;
int row;
public Cell(int column, int row) {
this.column = column;
this.row = row;
getStyleClass().add("cell");
Label label = new Label(this.toString());
getChildren().add(label);
}
public void highlight() {
getStyleClass().add("cell-highlight");
}
public void unhighlight() {
getStyleClass().remove("cell-highlight");
}
public String toString() {
return this.column + "/" + this.row;
}
}
public class MouseGestures {
public void makePaintable( Node node) {
if( useNewVersion) {
node.setOnMousePressed( onMousePressedEventHandler);
node.setOnDragDetected( onDragDetectedEventHandler);
node.setOnMouseDragEntered( onMouseDragEnteredEventHandler);
} else {
node.setOnMousePressed( onMousePressedEventHandler);
node.setOnMouseDragged( onMouseDraggedEventHandler);
node.setOnMouseReleased( onMouseReleasedEventHandler);
}
}
/* old version */
EventHandler<MouseEvent> onMousePressedEventHandler = event -> {
Cell cell = (Cell) event.getSource();
if( event.isPrimaryButtonDown()) {
cell.highlight();
} else if( event.isSecondaryButtonDown()) {
cell.unhighlight();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = event -> {
PickResult pickResult = event.getPickResult();
Node node = pickResult.getIntersectedNode();
if( node instanceof Cell) {
Cell cell = (Cell) node;
if( event.isPrimaryButtonDown()) {
cell.highlight();
} else if( event.isSecondaryButtonDown()) {
cell.unhighlight();
}
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = event -> {
};
EventHandler<MouseEvent> onDragDetectedEventHandler = event -> {
Cell cell = (Cell) event.getSource();
cell.startFullDrag();
};
EventHandler<MouseEvent> onMouseDragEnteredEventHandler = event -> {
Cell cell = (Cell) event.getSource();
if( event.isPrimaryButtonDown()) {
cell.highlight();
} else if( event.isSecondaryButtonDown()) {
cell.unhighlight();
}
};
}
}
In the end you should be able to paint via primary mouse button and erase the paint via secondary mouse button:
One solution is to add an event filter to the scene which enables the sourceNode.startFullDrag(). This will work even if you start dragging the mouse outside of your canvas (if you want any space without nodes in your application).
Like this:
scene.addEventFilter(MouseEvent.DRAG_DETECTED , new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
scene.startFullDrag();
}
});
And then you could:
node.setOnMouseDragEntered(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
led.setOn(true);
}
});
The (source) node which handles the initial DRAG_DETECTED event should invoke sourceNode.startFullDrag(), then the target node will able to handle one of MouseDragEvents, for instance MOUSE_DRAG_OVER or MOUSE_DRAG_ENTERED event with respective targetNode.setOn<MouseDragEvent>() method.