Scrollbar - the block increment seems variable not fixed - java
In the example below the block increment of the scrollbar is set to 100. However it appears that the actual value will be a value between 1 - 100 depending on where you click on the track bar.
When running the example try clicking roughly in the middle of the track several times to scroll down. When I do this I get output like:
ScrollBar Max: 400.0
LayoutY: -100.0
LayoutY: -200.0
LayoutY: -300.0
LayoutY: -375.0 // this varies depending on the click location
I am expecting the scroll units to change by 100 no matter where I click on the track. (ie, only a single label should be visible at a time).
Is this a common scrolling feature? If so, is there a way to turn it off?
Here is the code:
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Orientation;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class ScrollBarSSCCE extends Application
{
#Override
public void start(Stage stage)
{
int labelWidth = 300;
int labelHeight = 100;
String[] styles =
{
"-fx-background-color: #336699;",
"-fx-background-color: #996633;",
"-fx-background-color: #ff0000;",
"-fx-background-color: #00ff00;",
"-fx-background-color: #0000ff;"
};
VBox vb = new VBox();
for (int i = 0; i < styles.length; i++)
{
Label label = new Label();
label.setPrefWidth( labelWidth );
label.setPrefHeight( labelHeight );
label.setStyle( styles[i] );
vb.getChildren().add( label );
}
ScrollBar sc = new ScrollBar();
sc.setOrientation(Orientation.VERTICAL);
sc.setLayoutX(labelWidth);
sc.setMin(0);
sc.setMax( (styles.length - 1) * labelHeight);
System.out.println("ScrollBar Max: " + sc.getMax());
sc.setPrefHeight( labelHeight );
sc.setUnitIncrement( labelHeight / 2 );
sc.setBlockIncrement( labelHeight );
sc.valueProperty().addListener(
(ObservableValue<? extends Number> ov, Number old_val, Number new_val) ->
{
double y = -new_val.doubleValue();
System.out.println("LayoutY: " + y);
vb.setLayoutY( y );
});
Group root = new Group();
root.getChildren().addAll(vb, sc);
Scene scene = new Scene(root, labelWidth+20, labelHeight);
stage.setScene(scene);
stage.setTitle("ScrollBar SSCCE");
stage.show();
}
public static void main(String[] args)
{
launch(args);
}
}
i cant give a good explanation
System.out.println("block increment: " +sc.getBlockIncrement()); is always the same, and when i tested it, it happens on both sides going down or going up, i think it has to do with the ScrollBar.adjustValue(position); so if i am to provide some sort of solution i will say adjust the Value when you most need it.
//in your value listener you can add these codes
if(new_val.doubleValue() > old_val.doubleValue()){ //going down
if(sc.getMax()-sc.getBlockIncrement() < Math.abs(y)){
sc.adjustValue(1);
}
}else{//going up
if(old_val.doubleValue() == sc.getBlockIncrement()){
sc.adjustValue(0);
}
}
i think with this you might want to set sc.setUnitIncrement(labelHeight/2); the same as sc.setBlockIncrement(labelHeight); as that code might ruing the flow of UnitIncrement
hope it hepls :)
The ScrollBar.adjustValue(position) method is responsible for this behaviour.
It basically has three steps:
determine a "position value" by using the relative location of the mouse pressed
determine the "new value" by using the block increment value (this is the value I want)
Determine whether to use the "position value" or the "new value".
Since I always want the "new value" I just removed step 3 from the adjustValue(...) method.
Here is the code for the custom ScrollBar:
ScrollBar sc = new ScrollBar()
{
#Override
public void adjustValue(double position)
{
// figure out the "value" associated with the specified position
double posValue = ((getMax() - getMin()) * Utils.clamp(0, position, 1)) + getMin();
double newValue;
if (Double.compare(posValue, getValue()) != 0)
{
if (posValue > getValue())
{
newValue = getValue() + getBlockIncrement();
}
else
{
newValue = getValue() - getBlockIncrement();
}
setValue( Utils.clamp(getMin(), newValue, getMax()) );
}
}
};
Related
JavaFx TextArea Fixed Content Row Count
I have a fixed TextArea with 50 lines. If the content reaches 50th line, the 1st line should remove and the 51st line should add to maintain the fixed row count. This behavior is same as application console which hide previous inputs after some point. Currently I am clearing the TextArea after reaches 50th lines using counter. public static void updateTextAreaTest(String text) { lineCount++; Platform.runLater(() -> { if (lineCount <= 50) { txtAreaTest.appendText(text + "\n"); } else { txtAreaTest.setText(""); lineCount = 0; } }); } I need to hide the first line while adding new line without affecting the performance of the application which has so many threads running. Edit: The TextArea is not editable. TextArea will update automatically without user interaction.
As suggested in the comments, a ListView might be a preferred option for this. However, if you want to use a text area, you can achieve this with by setting a TextFormatter on the text area. The filter for the TextFormatter can check the number of lines in the proposed new text for the text area, and if that contains more than the allowed number of lines, modify the change to drop the first lines. Note that this solution allows for inserting text with multiple lines in a single action. import java.util.regex.Matcher; import java.util.regex.Pattern; import javafx.animation.Animation; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.TextArea; import javafx.scene.control.TextFormatter; import javafx.scene.layout.BorderPane; import javafx.stage.Stage; import javafx.util.Duration; /** * JavaFX App */ public class App extends Application { private int lineNumber ; private final int MAX_LINES = 50 ; private TextArea createConsole() { TextArea appConsole = new TextArea(); appConsole.setWrapText(false); appConsole.setEditable(false); Pattern newline = Pattern.compile("\n"); appConsole.setTextFormatter(new TextFormatter<String>(change -> { String newText = change.getControlNewText(); // count lines in proposed new text: Matcher matcher = newline.matcher(newText); int lines = 1 ; while (matcher.find()) lines++; // if there aren't too many lines just return the changed unmodified: if (lines <= MAX_LINES) return change ; // drop first (lines - 50) lines and replace all text // (there's no other way AFAIK to drop text at the beginning // and replace it at the end): int linesToDrop = lines - MAX_LINES ; int index = 0 ; for (int i = 0 ; i < linesToDrop ; i++) { index = newText.indexOf('\n', index) ; } change.setRange(0, change.getControlText().length()); change.setText(newText.substring(index+1)); return change ; })); return appConsole; } #Override public void start(Stage stage) { TextArea appConsole = createConsole(); // Fill with 45 lines to start: appConsole.appendText("Line 1"); for (lineNumber = 2 ; lineNumber <= 45 ; lineNumber++) { appConsole.appendText("\nLine "+lineNumber); } // add a new line every 2 seconds: Timeline demo = new Timeline( new KeyFrame(Duration.seconds(2), e -> appConsole.appendText("\nLine "+(lineNumber++)) ) ); demo.setCycleCount(Animation.INDEFINITE); demo.play(); stage.setScene(new Scene(new BorderPane(appConsole))); stage.show(); } public static void main(String[] args) { launch(); } } One nice thing about this solution is that it is a "fire and forget" configuration of the text area. You create the text area, set the formatter on it, which automatically provides the functionality of never having more than 50 lines of text, with lines at the beginning being dropped if needed, and then the rest of your code can just call TextArea methods (such as appendText()) as needed.
Rectangle size binding to Scene size - JavaFX
I'm trying to write a simple JavaFX application that contains a graphical representation of an NxM matrix (default 100 x 100). I would like to bind the matrix size to the Scene size, so when I resize the app window, the matrix follows it and keeps the aspect ratio. The binding works seamlessly for small matrices (for example 10x10), but when the matrix gets bigger (for example 50x50) and rectangles get smaller so they can fit my screen, the binding process becomes discontinuous. The matrix sometimes even gets bigger than window size (can't see all cells/rectangles) and matrix size seems like it's switching between integer values. What I did is created a StackPane as a root Node inside the Scene and a GridPane as a child Node to the StackPane. I filled the GridPane with an NxM Rectangle matrix. The code follows: public class GridPaneExample extends Application { public static void main(String[] args) { launch(args); } #Override public void start(Stage primaryStage) throws Exception { int n = 100, m = 100; // matrix size double rectw = 5, recth = 5; // size of each rectangle StackPane root = new StackPane(); Scene scene = new Scene(root, (m + 2) * rectw, (n + 2) * recth, Color.DARKOLIVEGREEN); // creating a scene with // a frame around matrix primaryStage.setScene(scene); primaryStage.sizeToScene(); GridPane gp = new GridPane(); root.getChildren().add(gp); Rectangle[][] rects = new Rectangle[n][m]; for (int i = 0; i < n; ++i) for (int j = 0; j < m; ++j) { rects[i][j] = new Rectangle(); rects[i][j].setWidth(rectw); rects[i][j].setHeight(recth); rects[i][j].setFill(Color.ANTIQUEWHITE); rects[i][j].setStrokeType(StrokeType.INSIDE); rects[i][j].setStrokeWidth(0.2); rects[i][j].setStroke(Color.GREY); rects[i][j].widthProperty().bind(gp.widthProperty().divide(m)); rects[i][j].heightProperty().bind(gp.heightProperty().divide(n)); GridPane.setRowIndex(rects[i][j], i); GridPane.setColumnIndex(rects[i][j], j); gp.getChildren().add(rects[i][j]); } rects[0][0].setFill(Color.RED); rects[0][m - 1].setFill(Color.RED); rects[n - 1][0].setFill(Color.RED); rects[n - 1][m - 1].setFill(Color.RED); StackPane.setAlignment(gp, Pos.TOP_CENTER); gp.minHeightProperty().bind(scene.heightProperty().subtract(2 * recth)); gp.minWidthProperty().bind(scene.widthProperty().subtract(2 * rectw)); gp.maxHeightProperty().bind(scene.heightProperty().subtract(2 * recth)); gp.maxWidthProperty().bind(scene.widthProperty().subtract(2 * rectw)); root.setLayoutY(recth); primaryStage.show(); primaryStage.setMinHeight(primaryStage.getHeight()); primaryStage.setMinWidth(primaryStage.getWidth()); } } Here are some screenshots of the program showing different sized matrices. A 10 x 10 matrix app when started (rectangle size is 20x20): https://i.imgur.com/I2vMLSD.png A 10 x 10 matrix app when stretched: https://i.imgur.com/9rR3e0g.png A 100 x 100 matrix app when started (rectangle size is 5x5): https://i.imgur.com/1KCKg6W.png A 100 x 100 matrix app when stretched a bit, see how the matrix gets out of bounds: https://i.imgur.com/pG8DxXA.png So, is there a way to make this binding experience smooth and consistent, maybe by using something else other than Rectangles? Edit: A weird thing happens. When I remove the rectangle binding lines from code and launch the app with 100x100 matrix, the matrix looks as it should. The program prints out the size of the GridPane and red rectangles. They are 500x500 and 5x5 respectively, as expected. However, when I include rectangle binding (as shown in the code), the matrix gets out of bounds, as in third photo attached, but the program still prints out the same dimensions of GridPane and rectangles.
As commented earlier, this problem is fixable by calling Node#setSnapToPixel(false) on the GridPane. Explanation This problem occurs when Nodes become so small that a single pixel becomes too big for accurate display. JavaFX uses a 'pixel snapping' feature by default to ensure a 'clear/clean' look. This would be mainly vissible with borders getting 'fussy/blurry' when not used. In the backend JavaFX has defined the methods snapSpace, snapSize and snapPosition, which correspond respectively to Math.round, Math.ceil and Math.round. This means that if a Node has a width of 2.5 with a 1 pixel border, it should have the size of (1 + 2.5 + 1) = 4.5, wich ceiled wil result in 5.0. If you have 100 elements getting a 0.5 width bonus it will result in 50 pixels overflow! Setting 'snapToPixel' to false means it wil ignore the mechanism above, and use mixed colors per pixel. If you would look closely this 'blurr' effect is noticable on borders. But it is worth the tradeoff for the perfect layout IMO.
Here is a sample app I wrote a long time ago which does a similar kind of thing. It uses resizable nodes within a GridPane with a listener on the layoutBoundsProperty to pick the size of the resizable nodes. Not sure if it is really what you are interested in (the approach you have in your question, with some fixes or tweaks, is probably fine for what you wish to accomplish), but it might be worth taking a look at for an alternate approach if needed. Relevant code is for the swatch node in the ColorChooser constructor from the sample code below (sorry it's a bit long, it was written for a different purpose but seems close enough that it might be worth posting in its entirety): import javafx.application.Application; import javafx.beans.property.*; import javafx.event.*; import javafx.geometry.*; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; import javafx.stage.StageStyle; /** * Sample application for using the color chooser */ public class ColorChooserSample extends Application { public void start(final Stage stage) throws Exception { // initialize the stage. stage.setTitle("Color Chooser"); stage.initStyle(StageStyle.UTILITY); // create a new color chooser sized to the stage. final String[][] smallPalette = { {"aliceblue", "#f0f8ff"}, {"antiquewhite", "#faebd7"}, {"aqua", "#00ffff"}, {"aquamarine", "#7fffd4"}, {"azure", "#f0ffff"}, {"beige", "#f5f5dc"}, {"bisque", "#ffe4c4"}, {"black", "#000000"}, {"blanchedalmond", "#ffebcd"}, {"blue", "#0000ff"}, {"blueviolet", "#8a2be2"}, {"brown", "#a52a2a"}, {"burlywood", "#deb887"}, {"cadetblue", "#5f9ea0"}, {"chartreuse", "#7fff00"}, {"chocolate", "#d2691e"}, {"coral", "#ff7f50"}, {"cornflowerblue", "#6495ed"}, {"cornsilk", "#fff8dc"}, {"crimson", "#dc143c"}, {"cyan", "#00ffff"}, {"darkblue", "#00008b"}, {"darkcyan", "#008b8b"}, {"darkgoldenrod", "#b8860b"}, }; final ColorChooser colorChooser = new ColorChooser(smallPalette); // to use the full web palette, just use the default constructor. // final ColorChooser colorChooser = new ColorChooser(); final Scene scene = new Scene(colorChooser, 600, 500); // show the stage. stage.setScene(scene); stage.show(); // monitor the color chooser's chosen color and respond to it. colorChooser.chosenColorProperty().addListener((observableValue, oldColor, newColor) -> System.out.println("Chose: " + colorChooser.getChosenColorName() + " " + colorChooser.getChosenColor()) ); } public static void main(String[] args) { launch(args); } } /** * A Color Chooser Component - allows the user to select a color from a palette. */ class ColorChooser extends VBox { private final double GOLDEN_RATIO = 1.618; private final double MIN_TILE_SIZE = 5; private final double nColumns; private final double nRows; /** * The color the user has selected or the default initial color (the first color in the palette) */ private final ReadOnlyObjectWrapper<Color> chosenColor = new ReadOnlyObjectWrapper<Color>(); public Color getChosenColor() { return chosenColor.get(); } public ReadOnlyObjectProperty<Color> chosenColorProperty() { return chosenColor.getReadOnlyProperty(); } /** * Friendly name for the chosen color */ private final ReadOnlyObjectWrapper<String> chosenColorName = new ReadOnlyObjectWrapper<String>(); public String getChosenColorName() { return chosenColorName.get(); } /** * Preferred size for a web palette tile */ private DoubleProperty prefTileSize = new SimpleDoubleProperty(MIN_TILE_SIZE); /** * A palette of colors from http://docs.oracle.com/javafx/2.0/api/javafx/scene/doc-files/cssref.html#typecolor */ private static final String[][] webPalette = { {"aliceblue", "#f0f8ff"}, {"antiquewhite", "#faebd7"}, {"aqua", "#00ffff"}, {"aquamarine", "#7fffd4"}, {"azure", "#f0ffff"}, {"beige", "#f5f5dc"}, {"bisque", "#ffe4c4"}, {"black", "#000000"}, {"blanchedalmond", "#ffebcd"}, {"blue", "#0000ff"}, {"blueviolet", "#8a2be2"}, {"brown", "#a52a2a"}, {"burlywood", "#deb887"}, {"cadetblue", "#5f9ea0"}, {"chartreuse", "#7fff00"}, {"chocolate", "#d2691e"}, {"coral", "#ff7f50"}, {"cornflowerblue", "#6495ed"}, {"cornsilk", "#fff8dc"}, {"crimson", "#dc143c"}, {"cyan", "#00ffff"}, {"darkblue", "#00008b"}, {"darkcyan", "#008b8b"}, {"darkgoldenrod", "#b8860b"}, {"darkgray", "#a9a9a9"}, {"darkgreen", "#006400"}, {"darkgrey", "#a9a9a9"}, {"darkkhaki", "#bdb76b"}, {"darkmagenta", "#8b008b"}, {"darkolivegreen", "#556b2f"}, {"darkorange", "#ff8c00"}, {"darkorchid", "#9932cc"}, {"darkred", "#8b0000"}, {"darksalmon", "#e9967a"}, {"darkseagreen", "#8fbc8f"}, {"darkslateblue", "#483d8b"}, {"darkslategray", "#2f4f4f"}, {"darkslategrey", "#2f4f4f"}, {"darkturquoise", "#00ced1"}, {"darkviolet", "#9400d3"}, {"deeppink", "#ff1493"}, {"deepskyblue", "#00bfff"}, {"dimgray", "#696969"}, {"dimgrey", "#696969"}, {"dodgerblue", "#1e90ff"}, {"firebrick", "#b22222"}, {"floralwhite", "#fffaf0"}, {"forestgreen", "#228b22"}, {"fuchsia", "#ff00ff"}, {"gainsboro", "#dcdcdc"}, {"ghostwhite", "#f8f8ff"}, {"gold", "#ffd700"}, {"goldenrod", "#daa520"}, {"gray", "#808080"}, {"green", "#008000"}, {"greenyellow", "#adff2f"}, {"grey", "#808080"}, {"honeydew", "#f0fff0"}, {"hotpink", "#ff69b4"}, {"indianred", "#cd5c5c"}, {"indigo", "#4b0082"}, {"ivory", "#fffff0"}, {"khaki", "#f0e68c"}, {"lavender", "#e6e6fa"}, {"lavenderblush", "#fff0f5"}, {"lawngreen", "#7cfc00"}, {"lemonchiffon", "#fffacd"}, {"lightblue", "#add8e6"}, {"lightcoral", "#f08080"}, {"lightcyan", "#e0ffff"}, {"lightgoldenrodyellow", "#fafad2"}, {"lightgray", "#d3d3d3"}, {"lightgreen", "#90ee90"}, {"lightgrey", "#d3d3d3"}, {"lightpink", "#ffb6c1"}, {"lightsalmon", "#ffa07a"}, {"lightseagreen", "#20b2aa"}, {"lightskyblue", "#87cefa"}, {"lightslategray", "#778899"}, {"lightslategrey", "#778899"}, {"lightsteelblue", "#b0c4de"}, {"lightyellow", "#ffffe0"}, {"lime", "#00ff00"}, {"limegreen", "#32cd32"}, {"linen", "#faf0e6"}, {"magenta", "#ff00ff"}, {"maroon", "#800000"}, {"mediumaquamarine", "#66cdaa"}, {"mediumblue", "#0000cd"}, {"mediumorchid", "#ba55d3"}, {"mediumpurple", "#9370db"}, {"mediumseagreen", "#3cb371"}, {"mediumslateblue", "#7b68ee"}, {"mediumspringgreen", "#00fa9a"}, {"mediumturquoise", "#48d1cc"}, {"mediumvioletred", "#c71585"}, {"midnightblue", "#191970"}, {"mintcream", "#f5fffa"}, {"mistyrose", "#ffe4e1"}, {"moccasin", "#ffe4b5"}, {"navajowhite", "#ffdead"}, {"navy", "#000080"}, {"oldlace", "#fdf5e6"}, {"olive", "#808000"}, {"olivedrab", "#6b8e23"}, {"orange", "#ffa500"}, {"orangered", "#ff4500"}, {"orchid", "#da70d6"}, {"palegoldenrod", "#eee8aa"}, {"palegreen", "#98fb98"}, {"paleturquoise", "#afeeee"}, {"palevioletred", "#db7093"}, {"papayawhip", "#ffefd5"}, {"peachpuff", "#ffdab9"}, {"peru", "#cd853f"}, {"pink", "#ffc0cb"}, {"plum", "#dda0dd"}, {"powderblue", "#b0e0e6"}, {"purple", "#800080"}, {"red", "#ff0000"}, {"rosybrown", "#bc8f8f"}, {"royalblue", "#4169e1"}, {"saddlebrown", "#8b4513"}, {"salmon", "#fa8072"}, {"sandybrown", "#f4a460"}, {"seagreen", "#2e8b57"}, {"seashell", "#fff5ee"}, {"sienna", "#a0522d"}, {"silver", "#c0c0c0"}, {"skyblue", "#87ceeb"}, {"slateblue", "#6a5acd"}, {"slategray", "#708090"}, {"slategrey", "#708090"}, {"snow", "#fffafa"}, {"springgreen", "#00ff7f"}, {"steelblue", "#4682b4"}, {"tan", "#d2b48c"}, {"teal", "#008080"}, {"thistle", "#d8bfd8"}, {"tomato", "#ff6347"}, {"turquoise", "#40e0d0"}, {"violet", "#ee82ee"}, {"wheat", "#f5deb3"}, {"white", "#ffffff"}, {"whitesmoke", "#f5f5f5"}, {"yellow", "#ffff00"}, {"yellowgreen", "#9acd32"} }; public ColorChooser() { this(webPalette); } public ColorChooser(String[][] colors) { super(); // create a pane for showing info on the chosen color. final HBox colorInfo = new HBox(); final Label selectedColorName = new Label(); HBox.setMargin(selectedColorName, new Insets(2, 0, 2, 10)); colorInfo.getChildren().addAll(selectedColorName); chosenColorName.addListener((observableValue, oldName, newName) -> { if (newName != null) { colorInfo.setStyle("-fx-background-color: " + newName + ";"); selectedColorName.setText(newName); chosenColor.set(Color.web(newName)); } }); // create a color swatch. final GridPane swatch = new GridPane(); swatch.setSnapToPixel(false); // calculate the number of columns and rows based on the number of colors and a golden ratio for layout. nColumns = Math.floor(Math.sqrt(colors.length) * 2 / GOLDEN_RATIO); nRows = Math.ceil(colors.length / nColumns); // create a bunch of button controls for color selection. int i = 0; for (String[] namedColor : colors) { final String colorName = namedColor[0]; final String colorHex = namedColor[1]; // create a button for choosing a color. final Button colorChoice = new Button(); colorChoice.setUserData(colorName); // position the button in the grid. GridPane.setRowIndex(colorChoice, i / (int) nColumns); GridPane.setColumnIndex(colorChoice, i % (int) nColumns); colorChoice.setMinSize(MIN_TILE_SIZE, MIN_TILE_SIZE); colorChoice.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); // add a mouseover tooltip to display more info on the colour being examined. // todo it would be nice to be able to have the tooltip appear immediately on mouseover, but there is no easy way to do this, (file jira feature request?) final Tooltip tooltip = new Tooltip(colorName); tooltip.setStyle("-fx-font-size: 14"); tooltip.setContentDisplay(ContentDisplay.BOTTOM); final Rectangle graphic = new Rectangle(30, 30, Color.web(colorHex)); graphic.widthProperty().bind(prefTileSize.multiply(1.5)); graphic.heightProperty().bind(prefTileSize.multiply(1.5)); tooltip.setGraphic(graphic); colorChoice.setTooltip(tooltip); // color the button appropriately and change it's hover functionality (doing some of this in a css sheet would be better). final String backgroundStyle = "-fx-background-color: " + colorHex + "; -fx-background-insets: 0; -fx-background-radius: 0;"; colorChoice.setStyle(backgroundStyle); colorChoice.setOnMouseEntered(mouseEvent -> { final String borderStyle = "-fx-border-color: ladder(" + colorHex + ", whitesmoke 49%, darkslategrey 50%); -fx-border-width: 2;"; colorChoice.setStyle(backgroundStyle + borderStyle); }); colorChoice.setOnMouseExited(mouseEvent -> { final String borderStyle = "-fx-border-width: 0; -fx-border-insets: 2;"; colorChoice.setStyle(backgroundStyle + borderStyle); }); // choose the color when the button is clicked. colorChoice.setOnAction(new EventHandler<ActionEvent>() { #Override public void handle(ActionEvent actionEvent) { chosenColorName.set((String) colorChoice.getUserData()); } }); // add the color choice to the swatch selection. swatch.getChildren().add(colorChoice); i++; } // select the first color in the chooser. ((Button) swatch.getChildren().get(0)).fire(); // layout the color picker. getChildren().addAll(swatch, colorInfo); VBox.setVgrow(swatch, Priority.ALWAYS); setStyle("-fx-background-color: black; -fx-font-size: 16;"); swatch.layoutBoundsProperty().addListener((observableValue, oldBounds, newBounds) -> { prefTileSize.set(Math.max(MIN_TILE_SIZE, Math.min(newBounds.getWidth() / nColumns, newBounds.getHeight() / nRows))); for (Node child : swatch.getChildrenUnmodifiable()) { Control tile = (Control) child; final double margin = prefTileSize.get() / 10; tile.setPrefSize(prefTileSize.get() - 2 * margin, prefTileSize.get() - 2 * margin); GridPane.setMargin(child, new Insets(margin)); } }); } }
Javafx setscale problems when enlarging group by dragging rectangle in the group
I am having an issue with scaling a group containing shapes, and dragging the shapes around. In the example below, I have a group containing 2 rectangles, after scaling the group, if I drag one of the rectangles, the other will go negative the distance of the other, i.e., if I drag the rectangle r2 down, the rectangle r will go up, etc I am wondering if this is some sort of bug, or what the issue is here? The code below is a working example of what my issue is. NOTE: I ran a test on the bounds, and it only changes when the dragged rectangle moves outside the current bounds. if moving to the bottom right the new bounds will be something similar to BoundingBox [minX:250.0, minY:250.0, minZ:0.0, width:301.7606201171875, height:338.6553955078125, depth:0.0, maxX:551.7606201171875, maxY:588.6553955078125, maxZ:0.0] Even though the other rectangle moves outside of the minX and minY bounds. import javafx.scene.shape.Rectangle; import javafx.application.Application; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.stage.Stage; public class SampleScale extends Application { #Override public void start(Stage primaryStage) { Group g = new Group(); Rectangle r = new Rectangle(250,250,10,10); Rectangle r2 = new Rectangle(260,260,10,10); r2.setFill(Color.RED); r2.setOnMouseDragged(event -> { r2.setTranslateX(r2.getTranslateX() + (event.getX() -r2.getX())); r2.setTranslateY(r2.getTranslateY() + (event.getY() -r2.getY())); g.autosize(); event.consume(); }); Pane p = new Pane(); p.setPrefSize(1000, 1000); g.getChildren().addAll(r,r2); p.getChildren().addAll(g);// comment out to test bottom code; //default works //g.setScaleX(1); //g.setScaleY(1); //causes other node to move negative distance of the other object // i.e., if r2 is dragged down, r will move up, etc. g.setScaleX(2); g.setScaleY(2); //------------------------- //this works if not placed inside a group // p.getChildren().addAll(r,r2); // r.setScaleX(2); // r.setScaleY(2); // r2.setScaleX(2); // r2.setScaleY(2); Scene scene = new Scene(p, 1300, 1250); primaryStage.setTitle("Hello World!"); primaryStage.setScene(scene); primaryStage.show(); } /** * #param args the command line arguments */ public static void main(String[] args) { launch(args); } }
The scaleX and scaleY properties use the center of a node as pivot point for scaling. Since moving r2 resizes the Group, the other children also move. You could apply a Scale transform with pivot point (0, 0) instead: // g.setScaleX(2); // g.setScaleY(2); g.getTransforms().add(new Scale(2, 2, 0, 0));
Access cell of a column using a given index?
I have the index (as integer) of a cell in a TableColumn of a TableView. Using the index of the cell, how can I access the cell object at this index? I am missing something like tableColumn.getCells() which would then allow me to access the cell with the given index: tableColumn.getCells().get(index) Any hints on this? Thanks! Update: As per request, a use case for this: Imagine a music playlist. For some reasons I am using a TableView for that and the TableView is as big as possible, meaning even if there are just 5 music tracks in the playlist and there is space for 10 rows, then 10 rows (5 filled, 5 empty) are shown. I then implemented drag & drop for re-arranging items. Now imagine a user wants to place the second entry to the end of the playlist. He drags the second entry, but not directly over the last used table row, but on e.g. row 8. In that case I want to give a visual feedback that the music track is placed under the last used row (row 5, but the mouse is over row/cell 8!), so I need to modify row/cell 5, while the mouse is over row 8. Knowing which row is the last filled one is quite easy - it's tableview.getItems().size()-1, but getting a reference to row/cell 5 is a tricky thing / my problem here.
This API doesn't exist for a reason. For better performance FX TableView manipulates cells by itself. Cells are created and destroyed depending on currently visible part of the TableView and other parameters. Even more: cell objects can be reused to represent data from other cells under some circumstances. So return value of tableColumn.getCells().get(index) is not determinate, thus there is no API to receive it. All cell related logic should be implemented in the CellFactory callback. Can you describe what kind of logic you want to implement by accessing cells?
I would set a fixed cell size and just take a guess using the mouse coordinates. I just added it to an existing example and I'm sure it's not quite right but it shows the idea. import javafx.application.Application; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.Scene; import javafx.scene.chart.PieChart; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.TextFieldTableCell; import javafx.scene.input.MouseEvent; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.util.converter.NumberStringConverter; public class ChangePie extends Application { #Override public void start(Stage primaryStage) { ObservableList<PieChart.Data> pieChartData = FXCollections.observableArrayList( new PieChart.Data("Grapefruit", 13), new PieChart.Data("Oranges", 25), new PieChart.Data("Plums", 10), new PieChart.Data("Pears", 22), new PieChart.Data("Apples", 30)); final PieChart chart = new PieChart(pieChartData); chart.setTitle("Imported Fruits"); final TableView<PieChart.Data> tv = new TableView(pieChartData); tv.setEditable(true); tv.setFixedCellSize(40); tv.addEventHandler(MouseEvent.ANY, (MouseEvent evt) -> { double y = 0; int idx1 = 0, idx2 = 0; if (evt.getEventType() == MouseEvent.MOUSE_PRESSED) { y = evt.getY(); idx1 = tv.getSelectionModel().getSelectedIndex(); } else if (evt.getEventType() == MouseEvent.MOUSE_RELEASED) { idx2 = (int) Math.round((evt.getY() - y) / 40); System.out.println("insert at " + idx2); if (idx2 > 0) { chart.setData(null); PieChart.Data removed = pieChartData.remove(idx1); pieChartData.add(Math.min(idx2, pieChartData.size() - 1), removed); } } }); TableColumn tc1 = new TableColumn("Name"); tc1.setPrefWidth(100); tc1.setCellValueFactory(new PropertyValueFactory("name")); tc1.setCellFactory(TextFieldTableCell.forTableColumn()); TableColumn tc2 = new TableColumn("Data"); tc2.setPrefWidth(100); tc2.setCellValueFactory(new PropertyValueFactory("pieValue")); tc2.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter())); tv.getColumns().addAll(tc1, tc2); Scene scene = new Scene(new VBox(chart, tv)); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
JavaFX Image (PNG) transparency crispness being lost when rendering
I am loading and displaying a transparent PNG on a gradient background with the following result: If I open the same file in Paint.NET, add the background it looks like that: Somehow, JavaFX makes the image loose crispness, I fear this may be an issue for ALL images in my app and its just that its most visible in this particular case. Here is extracted code showing how I load this particular image: ImageView imgDownload = new ImageView(this.getClass().getResource("/img/docstore/document_downloaded_btn.png").toExternalForm()); imgDownload.setFitWidth(59); imgDownload.setFitHeight(32); GridPane.setHalignment(imgDownload, HPos.CENTER); grid_item.add(imgDownload, 3, 0); For reference, here is a link to the original image. I'm looking for an answer highlighting a possible reason this is happening
Update JavaFX images in Java 8 are now rendered crisply in all cases. The original issue described in the question has been addressed. The bug Dreen filed regarding this behaviour, RT-28765 Incorrect subpixel placement for an Image, was closed as a duplicate of RT-19282 [StackPane] unwanted blur with ImageView. RT-19282 was closed as fixed for Java 8. I tested Java 8 build 108 on Windows 7. The sample application in this answer now displays correctly (no fuzzy image sometimes offset by half a pixel in the x or y direction). This is an excellent question and very curious behaviour. I put together a sample program which offers a possible explanation and a workaround. The output of this program after running it and dragging the border around a little to resize it, is as below. The fuzzy cloud on the left is a standard ImageView placed in a GridPane. The crisp cloud on the right is an ImageView wrapped in my workaround fix class (CenteredRegion), placed in the same GridPane. While the above image is displayed, the output of the program is: Layout SnapToPixel: true ... fuzzy: New Bounds: BoundingBox [minX:14.5, minY:12.5, minZ:0.0, width:59.0, height:32.0, depth:0.0, maxX:73.5, maxY:44.5, maxZ:0.0] fuzzy: xDisplacement: 0.5, yDisplacement: 0.5 crisp: New Bounds: BoundingBox [minX:84.0, minY:13.0, minZ:0.0, width:59.0, height:32.0, depth:0.0, maxX:143.0, maxY:45.0, maxZ:0.0] crisp: xDisplacement: 0.0, yDisplacement: 0.0 As you can see, the fuzzy image has not been pixel aligned and is offset by half a pixel in the x and y direction, causing it to look fuzzy. This is despite the grid region having it's snapToPixel setting as true. As the stage containing the layout is dynamically resized, the fuzzy image will alternate between being pixel aligned and not being pixel aligned (depending on the oddness or evenness of the width and height of the scene). This creates an an annoying shimmering effect as you resize the stage borders, due to the fuzzy image continuously alternating between being fuzzy and clear. The behaviour of the fuzzy image would seem to be a bug when snapToPixel is set to true on the parent container, so I'd advise filing a bug against the JavaFX runtime project and linking back to this stack overflow question in the bug and placing a link to the created bug in a comment. The crisp version remains crisp because it is housed in a custom region implementation which ensures pixel alignment. Test system was win7 + jdk8b77 + ATI HD4600 graphics card. import javafx.application.Application; import static javafx.application.Application.launch; import javafx.beans.value.*; import javafx.geometry.*; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.image.*; import javafx.scene.layout.*; import static javafx.scene.layout.Region.USE_PREF_SIZE; import javafx.stage.Stage; public class TransparentPngSample extends Application { public static final String IMAGE_LOC = "http://i.imgur.com/byY8whh.png"; #Override public void start(Stage stage) { Pane layout = createSceneContent(); stage.setScene(new Scene(layout)); stage.show(); System.out.println("Layout SnapToPixel: " + layout.snapToPixelProperty().get()); } private Pane createSceneContent() { final Image cloudImage = new Image(IMAGE_LOC); final ImageView fuzzyCloud = new ImageView(cloudImage); final CenteredRegion crispCloud = new CenteredRegion(new ImageView(cloudImage)); GridPane layout = new GridPane(); layout.setHgap(10); layout.setVgap(10); layout.addRow(0, fuzzyCloud, crispCloud); layout.setAlignment(Pos.CENTER); layout.setStyle("-fx-padding: 10px; -fx-background-color: slategrey;"); fuzzyCloud.boundsInParentProperty().addListener(new BoundsReporter("fuzzy")); crispCloud.boundsInParentProperty().addListener(new BoundsReporter("crisp")); return layout; } class CenteredRegion extends Region { private Node content; CenteredRegion(Node content) { this.content = content; getChildren().add(content); } #Override protected void layoutChildren() { content.relocate( Math.round(getWidth() / 2 - content.prefWidth(USE_PREF_SIZE) / 2), Math.round(getHeight() / 2 - content.prefHeight(USE_PREF_SIZE) / 2) ); System.out.println("crisp content relocated to: " + getLayoutX() + "," + getLayoutY() ); } public Node getContent() { return content; } } class BoundsReporter implements ChangeListener<Bounds> { final String logPrefix; BoundsReporter(String logPrefix) { this.logPrefix = logPrefix; } #Override public void changed(ObservableValue<? extends Bounds> ov, Bounds oldBounds, Bounds newBounds) { System.out.println(logPrefix + ": " + "New Bounds: " + newBounds ); double xDisplacement = newBounds.getMinX() - Math.floor(newBounds.getMinX()); double yDisplacement = newBounds.getMinY() - Math.floor(newBounds.getMinY()); System.out.println(logPrefix + ": " + "xDisplacement: " + xDisplacement + ", " + "yDisplacement: " + yDisplacement ); } } public static void main(String[] args) { launch(args); } }