JavaFX: How to make a Node partially mouse transparent? - event-handling

Simplified Problem:
Make one Node "A" that is on top of another Node "B" to be half transparent to MouseEvents, so the Events will reach the underlying Node "B". Both Nodes are of equal size but Node "A" has a half transparent background image so one half of Node "B" is visible.
Real Problem:
I have a menu of tabs. Each tab can be dragged to expand the corresponding menu layer. Therefore each tab layer is a Pane with a partially transparent background (basically a polygon) of which the transparent part should be also transparent to MouseEvents.
The illustration (which I can't post yet, see link: Illustration of tabs, the dark green line is the border of the green Pane) shows the basic principle: just imagine only the tabs are visible and the layer itself can be pulled to the right to view it's content.
So the question is, how do I make a region of a Node transparent to MouseEvents without making the whole Node transparent?
Thank you for your help!
Update:
To clarify the simple problem here is the corresponding code:
//Create parent group
Group root = new Group();
//Create linear gradient, so one side is transparent
Stop[] stops = new Stop[] { new Stop(0, Color.rgb(0, 255, 0, 0.0)), new Stop(1, Color.rgb(0, 255, 0, 1.0))};
LinearGradient lg1 = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops);
//Create the rectangles
Rectangle A = new Rectangle(100, 50, lg1);
Rectangle B = new Rectangle(100,50, Color.RED);
//Add eventHandlers
A.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
System.out.println("Clicked A");
}
});
B.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
System.out.println("Clicked B");
}
});
root.getChildren().addAll(B, A);
//Add to Scene..
Hope this helps.

Consider the pickOnBounds property, it may help in your situation, but it is not clear to me without seeing your code attempt which fails for the simplified problem.
node.setPickOnBounds(true)
If pickOnBounds is true, then picking is computed by intersecting with the bounds of this node, else picking is computed by intersecting with the geometric shape of this node.
The code below demonstrates how this may be used by creating a square overlaid by an ImageView for an Image which contains tranparent pixels. If pickOnBounds is set to true for the ImageView, then, even if you click on the transparent pixels in the image, the ImageView will receive the mouseClick event. If pickOnBounds is set to false for the ImageView, then, even if you click on the transparent pixels in the image, the ImageView will not process the click and the click event will be received by the node behind the image.
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class PickOnBoundsDemo extends Application {
public static void main(String[] args) { Application.launch(args); }
#Override public void start(Stage stage) {
final Rectangle back = new Rectangle(128, 128);
back.setFill(Color.FORESTGREEN);
final ImageView front = new ImageView("http://icons.iconarchive.com/icons/aha-soft/free-large-boss/128/Wizard-icon.png");
// icon: Linkware (Backlink to http://www.aha-soft.com required)
final StackPane pickArea = new StackPane();
pickArea.getChildren().addAll(
back,
front
);
final ToggleButton pickTypeSelection = new ToggleButton("Pick On Bounds");
final Label pickResult = new Label();
Bindings.bindBidirectional(front.pickOnBoundsProperty(), pickTypeSelection.selectedProperty());
front.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent t) {
pickResult.setText("Front clicked");
}
});
back.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent t) {
pickResult.setText("Back clicked");
}
});
VBox layout = new VBox(10);
layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 10;");
layout.getChildren().setAll(
pickArea,
new Label("Click inside the above area to test mouse picking."),
pickTypeSelection,
pickResult
);
layout.setAlignment(Pos.CENTER);
stage.setScene(new Scene(layout));
stage.show();
}
}

Related

Javafx equally spanned toggle button

I need to make a group of toggle button like the following, with the white background being the selected button, and two buttons take 50% width of the parent container. Two toggle buttons are place inside HBox. The styling
So far I tried, stuck like this.
.viewType .toggle-button {
-fx-padding: 0 2 0 2;
-fx-background-color: #000;
-fx-text-fill: white;
}
.viewType .toggle-button:selected {
-fx-padding: 0;
-fx-background-color: white;
-fx-text-fill: black;
-fx-border-wdith: 2;
-fx-border-color: black;
-fx-border-radius: 4;
}
You can set the buttons maxWidth to max double in java side. This will provide your buttons to same width in HBox. Hope it is useful:
btn1.setMaxWidth(Double.MAX_VALUE);
btn2.setMaxWidth(Double.MAX_VALUE);
You can check the following link for useful information related with sizing and aligning nodes:
Sizing and Aligning Nodes
If you want both buttons to have equal width, use a GridPane instead of a HBox, and use column constraints to make the two columns equal widths:
GridPane grid = new GridPane();
grid.getColumnConstraints().addAll(createCol(), createCol());
ToggleButton toggle1 = new ToggleButton("...");
toggle1.setMaxWidth(Double.MAX_VALUE);
ToggleButton toggle2 = new ToggleButton("...");
toggle2.setMaxWidth(Double.MAX_VALUE);
grid.addRow(0, toggle1, toggle2);
// ...
private ColumnConstraints createCol() {
ColumnConstraints col = new ColumnConstraints();
col.setPercentWidth(50);
col.setFillWidth(true);
return col ;
}
You can further control how big the grid pane is in its parent by configuring the parent (details depend on what type of pane is used for the parent).
SSCCE:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class EqualSizedButtons extends Application {
#Override
public void start(Stage primaryStage) {
GridPane grid = new GridPane();
grid.getStyleClass().add("viewType");
grid.getColumnConstraints().addAll(createCol(), createCol());
ToggleButton toggle1 = new ToggleButton("A");
toggle1.setMaxWidth(Double.MAX_VALUE);
ToggleButton toggle2 = new ToggleButton("This is really big button B");
toggle2.setMaxWidth(Double.MAX_VALUE);
grid.addRow(0, toggle1, toggle2);
new ToggleGroup().getToggles().addAll(toggle1, toggle2);
BorderPane root = new BorderPane();
root.setBottom(grid);
Scene scene = new Scene(root, 600, 600);
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private ColumnConstraints createCol() {
ColumnConstraints col = new ColumnConstraints();
col.setPercentWidth(50);
col.setFillWidth(true);
return col ;
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX8 - How to draw random circles with random x/y centers?

I am trying to draw random circles with random x/y centers, but the result of my code is only one circle at the center of the stage (window).
I use Task class to update my UI every 1 second.
This is my code:
package javafxupdateui;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class JavaFXUpdateUI extends Application {
private Stage window;
private StackPane layout;
private Scene scene;
#Override
public void start(Stage primaryStage) {
window = primaryStage;
window.setTitle("JavaFX - Update UI");
layout = new StackPane();
scene = new Scene(layout, 500, 500);
window.setScene(scene);
window.show();
Thread th = new Thread(task);
th.setDaemon(true);
th.start();
}
Task task = new Task<Void>() {
#Override
protected Void call() throws Exception {
while (true) {
Platform.runLater(new Runnable() {
#Override
public void run() {
drawCircles();
}
});
Thread.sleep(1000);
}
}
};
public void drawCircles() {
Circle circle;
float x = (float)(Math.random()*501);
float y = (float)(Math.random()*501);
circle = new Circle(x, y, 25, Color.RED);
layout.getChildren().add(circle);
scene.setRoot(layout);
window.setScene(scene);
}
public static void main(String[] args) {
launch(args);
}
}
The result of the above code is:
Result GUI
What is going wrong
StackPane is a layout pane, it centers everything by default. As you want to manually place the circles at random locations, you don't want to use a pane which manages the layout for you.
How to fix it
Use a Pane or a Group instead of StackPane. Neither Pane nor Group manage the layout of items for you, so children you add to them at specific locations will remain at those locations.
Aside
You might wish to use a Timeline for your periodic updates rather than a Task with runLater (though the later will still work OK, with a Timeline you don't have to deal with additional complexities of concurrent code).

javafx rounded rectangle corners

When you create a rounded VBox in JavaFX, and the background color for the VBox is black, assume the rounded corners after filling the button is white, How can I fill this region with another color (Assume I want it to be totally transparent).
I assume, the questioneer wants to finally hava a floating round VBox.
This can probably be achieved in many ways, without thinking to much about it, I would rather make use of a clipping Node instead of a pure CSS approach (which should be doable as well).
As he already wrote, you would also need to make the Scenes fill Color.TRANSPARENT (and probably the Stage as well).
My approach looks like this. First the FXML file:
<?import java.lang.*?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<BorderPane xmlns:fx="http://javafx.com/fxml" fx:id="root" fx:controller="application.ClipExampleController" style="-fx-background-color: steelblue;">
<center>
<VBox fx:id="vbox" alignment="CENTER" spacing="5" maxWidth="150">
<Label text="Bla"/>
<TextField promptText="Blub"/>
<Button text="Do it"/>
</VBox>
</center>
</BorderPane>
Then the Controller class:
package application;
import javafx.beans.binding.DoubleBinding;
import javafx.fxml.FXML;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
public class ClipExampleController {
#FXML
private BorderPane root;
#FXML
private VBox vbox;
#FXML
protected void initialize() {
Circle clip = new Circle();
clip.radiusProperty().bind(new DoubleBinding() {
{
bind(vbox.widthProperty());
}
#Override
protected double computeValue() {
return vbox.getWidth() / 2 + 25;
}
});
clip.centerXProperty().bind(new DoubleBinding() {
{
bind(root.widthProperty());
}
#Override
protected double computeValue() {
return root.getWidth() / 2;
}
});
clip.centerYProperty().bind(new DoubleBinding() {
{
bind(root.heightProperty());
}
#Override
protected double computeValue() {
return root.getHeight() / 2;
}
});
root.setClip(clip);
}
}
and finally the glue code - the main Application:
package application;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class Main extends Application {
#Override
public void start(Stage stage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("ClipExample.fxml"));
Parent root = loader.load();
Scene scene = new Scene(root, 300, 300);
scene.setFill(Color.TRANSPARENT);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
So. Assuming, that I understand you correctly, this would be my solution.
assume the rounded corners after filling the button is white, How can I fill this region with another color
Are you talking about the "Stroke?" There is "Fill" (Inside) and "Stroke" (outline, outer-rim, edge, etc)
https://docs.oracle.com/javase/8/javafx/api/javafx/scene/shape/Shape.html
The Shape class provides definitions of common properties for objects that represent some form of geometric shape.
These properties include:
The Paint to be applied to the fillable interior of the shape (see setFill).
The Paint to be applied to stroke the outline of the shape (see setStroke).
https://docs.oracle.com/javase/8/javafx/api/javafx/scene/shape/Shape.html#setStroke-javafx.scene.paint.Paint- (in case you didn't click setStroke above)
public final void setStroke(Paint value)
Sets the value of the property stroke.
Property description:
Defines parameters of a stroke that is drawn around the outline of a Shape using the settings of the specified Paint. The default value is null for all shapes except Line, Polyline, and Path. The default value is Color.BLACK for those shapes.
From here you fill it with a "Paint" Object which is a base class for many different Classes such as "Color"
https://docs.oracle.com/javase/8/javafx/api/javafx/scene/paint/Color.html#TRANSPARENT
public static final Color TRANSPARENT
A fully transparent color with an ARGB value of #00000000.
So... To sum this up, you are going to want to do.
button.setStroke(Color.TRANSPARENT);
If this, of course, is what you're asking, since it's hard to tell...; However, it seems what I have provided is what you ask, but if not I'll try again :).
In order to set the container's background of the VBox to be transparent, then you need to set the fill property of the scene that contains the VBox to TRANSPARENT COLOR, the following piece of code clarifies that:
This is the vbox style:
.vbox
{
-fx-background-color: black;
-fx-background-radius: 300%;
-fx-alignment:center;
}
if you applied the above style to a vbox with width and height = 200 you will get a circle with black background while the rounded corners filled with white. To make this white corners transparent, you need to add this piece of code:
loader.setLocation(MainApp.class.getResource("view/Test.fxml"));
VBox page = (VBox) loader.load();
Stage testStage = new Stage();
Scene scene = new Scene(page);
scene.setFill(Color.TRANSPARENT);
testStage.setScene(scene);

Javafx 8 drawing a line between translated nodes

How do you draw a line between the centers of translated nodes? Given for example the following code snippet:
public class Test extends Application{
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
Circle circle1=new Circle(10, Color.GREEN);
root.getChildren().add(circle1);
Circle circle2=new Circle(10, Color.RED);
root.getChildren().add(circle2);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
circle1.setTranslateX(100);
Line line=new Line(circle1.getCenterX(), circle1.getCenterY(), circle2.getCenterX(), circle2.getCenterY());
root.getChildren().add(line);
}
public static void main(String[] args) {
launch(args);
}
}
Running this application will clearly show a red and a green circle. However, there won't be a line because each of the centers of the circles are at the coordinates (0,0). Nevertheless, the circles do not cover each other because one of the circles is translated. This doesn't work:
Line line=new Line(circle1.getCenterX()+circle1.getTranslateX(), circle1.getCenterY()+circle1.getTranslateY(), circle2.getCenterX()+circle2.getTranslateX(), circle2.getCenterY()+circle2.getTranslateY());
Finally, let's assume that there is an approach to draw a line connecting the centers of the two circles. If, after the line is drawn, I would invoke circle2.setTranslateX(50);, how do I ensure that the endpoint of the line on the side of circle2 moves accordingly?
A StackPane is a managed layout pane, meaning that it manages the positions of its child nodes (by default it centers them); the translation is applied after the StackPane positions the nodes. This is why the circles appear in different locations but the line is not where you expect. Using a Pane instead of a StackPane will make things work as you expect.
To keep the line in the correct position relative to the circles when the circles are repositioned dynamically, bind the startX, startY, endX, and endY properties, instead of just setting them.
import javafx.animation.Animation;
import javafx.animation.ParallelTransition;
import javafx.animation.SequentialTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
import javafx.util.Duration;
public class LineConnectingCircles extends Application{
#Override
public void start(Stage primaryStage) {
Pane root = new Pane();
Circle circle1=new Circle(10, Color.GREEN);
root.getChildren().add(circle1);
Circle circle2=new Circle(10, Color.RED);
root.getChildren().add(circle2);
// move circles so we can see them:
circle1.setTranslateX(100);
circle2.setTranslateY(50);
Line line = new Line();
// bind ends of line:
line.startXProperty().bind(circle1.centerXProperty().add(circle1.translateXProperty()));
line.startYProperty().bind(circle1.centerYProperty().add(circle1.translateYProperty()));
line.endXProperty().bind(circle2.centerXProperty().add(circle2.translateXProperty()));
line.endYProperty().bind(circle2.centerYProperty().add(circle2.translateYProperty()));
root.getChildren().add(line);
// create some animations for the circles to test the line binding:
Button button = new Button("Animate");
TranslateTransition circle1Animation = new TranslateTransition(Duration.seconds(1), circle1);
circle1Animation.setByY(150);
TranslateTransition circle2Animation = new TranslateTransition(Duration.seconds(1), circle2);
circle2Animation.setByX(150);
ParallelTransition animation = new ParallelTransition(circle1Animation, circle2Animation);
animation.setAutoReverse(true);
animation.setCycleCount(2);
button.disableProperty().bind(animation.statusProperty().isEqualTo(Animation.Status.RUNNING));
button.setOnAction(e -> animation.play());
BorderPane.setAlignment(button, Pos.CENTER);
BorderPane.setMargin(button, new Insets(10));
Scene scene = new Scene(new BorderPane(root, null, null, button, null), 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Javafx - Rotate node for display in landscape/portrait orientation on handheld device

The javafx application I am developing will eventually be running on a handheld device with 800x480 resolution. The application will typically be running in portrait mode, but for some features (e.g. displaying charts and tables), it will need to switch to landscape mode to better display the data.
My question is, is there a straight forward way to operate with nodes that are rotated by multiples of 90 degrees?
I can rotate the table by calling setRotate(), although this introduces several new issues:
To resize column widths when rotated, the user has to drag the column dividers left to right (orthogonal to the row of headers),
The table still expands its width/height to the size of its parent, although this doesn't work as well when rotated -90 degrees (or other multiples of 90).
The other constraint is that the chart content is contained in the center of a BorderPane, where the top and bottom of the BorderPane contain toolbars, which prevents rotating the entire scene.
Here is my SSCCE; please correct me if there are any problems with the code below.
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TablePanelTrial extends Application {
BorderPane root = new BorderPane();
private boolean isRotated=false;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(final Stage primaryStage) {
primaryStage.setTitle("Table Panel Trial");
final TablePanel tp = new TablePanel();
Button btnRotate = new Button("Rotate");
btnRotate.setOnAction(new EventHandler() {
#Override
public void handle(ActionEvent event) {
double r = -90;
if(isRotated){
r=0;
isRotated = !isRotated;
}
else{
r=-90;
tp.tv.setMinHeight(200);
isRotated = !isRotated;
}
tp.rotate(r);
}
});
root.setTop(btnRotate);
root.setCenter(tp.getVB());
primaryStage.setScene(new Scene(root, 480, 800));
primaryStage.show();
}
class TablePanel{
private VBox vbox = new VBox();
private TableView tv = new TableView();
private String[] labelVal = {"Column 1", "Element", "Difference", "File Name", "Report Number"};
public TablePanel(){
TableColumn column1 = new TableColumn(labelVal[0]);
TableColumn column2 = new TableColumn(labelVal[1]);
TableColumn column3 = new TableColumn(labelVal[2]);
TableColumn column4 = new TableColumn(labelVal[3]);
TableColumn column5 = new TableColumn(labelVal[4]);
tv.getColumns().addAll(column1, column2, column3, column4,column5);
vbox.getChildren().add(tv);
tv.setPrefHeight(2000);
}
public Pane getVB(){
return vbox;
}
public void rotate(double r){
vbox.setRotate(r);
}
}
}
Android does not perform a rotate function when the device goes to landscape mode, I think.
One has to redraw the layout again to display it.
With JavaFX, what you can do is add a Listener to the scene to listen for a change in width since that's what basically happens.
You can do it like this most probably:
scene.widthProperty().addListener((observable, oldValue, newValue) -> {
....initialize the changes to layout here....
);
});
May not be the best solution but I guess its something.