Reputation: 330
I just discovered JavaFX and I really liked it. I hate java-default GUI, so I immediatly decided to personalize my window. I had numerous tries, but I have one big limitation and one big objective; limitation? I must use MVC pattern. Objective? Make the custom window reusable.
So... This is the point I have now: wstaw.org/m/2016/04/07/resoruces.png
I made a general package application that contains App.java, that will launch the app. Then I make another internal package, containin the "MinimalWindow" logic, with all resoruces I need.
I implemented this FXML code to perform the window:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.control.Label?>
<StackPane fx:id="minimalWindowShadowContainer" id="minimalWindowShadowContainer" stylesheets="@style.css" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" onMousePressed="#updateXY" onMouseDragged="#windowDragging" onMouseReleased="#updateStatus" >
<BorderPane fx:id="minimalWindowContainer" id="minimalWindowContainer">
<!-- This padding will create the dropshadow effect for the window behind -->
<padding>
<Insets top="5" right="5" bottom="5" left="5"/>
</padding>
<!-- "Title Bar" -->
<top>
<HBox id="titleBar" alignment="CENTER" spacing="5" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="30.0" prefWidth="600.0">
<padding>
<Insets top="5" right="5" bottom="5" left="5"/>
</padding>
<ImageView fx:id="logo" fitWidth="20" fitHeight="20"></ImageView>
<Label fx:id="lblTitle" id="title" text="MinimalWindow"></Label>
<Region HBox.hgrow="ALWAYS" prefHeight="30.0" prefWidth="200.0"></Region>
<HBox alignment="CENTER_RIGHT">
<Button id="btnMin" onMouseClicked="#minimizeApp" minHeight="20" minWidth="20" maxHeight="20" maxWidth="20"></Button>
<Button fx:id="btnMax" id="btnMax" onMouseClicked="#maximizeApp" minHeight="20" minWidth="20" maxHeight="20" maxWidth="20"></Button>
<Button id="btnCls" onMouseClicked="#closeApp" minHeight="20" minWidth="20" maxHeight="20" maxWidth="20"></Button>
</HBox>
</HBox>
</top>
<!-- The content of the window will go here -->
<center>
<StackPane fx:id="contentArea" id="contentArea"></StackPane>
</center>
<!-- Footer -->
<bottom>
<HBox id="footer">
<padding>
<Insets top="5" right="5" bottom="5" left="5"/>
</padding>
<Button fx:id="btnResize" id="btnResize" alignment="BOTTOM_RIGHT" onMouseClicked="#updateXY" onMouseEntered="#setMouseCursor" onMouseExited="#resetMouseCursor" onMouseDragged="#resizeWindow" minHeight="10" minWidth="10" maxHeight="10" maxWidth="10"></Button>
</HBox>
</bottom>
</BorderPane>
</StackPane>
I implemented then the controller class:
package application.minimalWindow;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class MinimalWindow extends Application {
@FXML
Label lblTitle;
@FXML
Button btnMax, btnResize;
@FXML
StackPane minimalWindowShadowContainer, minimalWindowContainer,contentArea;
@FXML
Double SHADOW_SPACE;
final private static int MIN_WIDTH = 730, MIN_HEIGHT = 500;
private double actualX, actualY;
private boolean isMovable;
private String source, title;
private Stage mainStage;
//
// Public logic of the class
//
public MinimalWindow() {
//TODO must work...
}
//Show the window
public void show() {
mainStage.show();
}
//
// MIMIZIE | MAXIMIZE | CLOSE
//
//When pressed, will minimize the window to tray
@FXML
private void minimizeApp(MouseEvent e) {
mainStage.setIconified(true);
}
//When pressed, check if it must maximize or restore the window
@FXML
private void maximizeApp(MouseEvent e) {
if (mainStage.isMaximized()) {
setMin();
isMovable = true;
}
else {
setMax();
isMovable = false;
}
}
//When pressed, will kill the window
@FXML
private void closeApp(MouseEvent e) {
mainStage.close();
System.exit(0);
}
//
// WINDOW MOVING
//
//When i must update the XY of the click
@FXML
private void updateXY(MouseEvent e){
actualX = e.getScreenX() - mainStage.getX();
actualY = e.getScreenY() - mainStage.getY();
}
//When pressing and dragging the mouse it will move the window
@FXML
private void windowDragging(MouseEvent e) {
if (isMovable) {
mainStage.setX(e.getScreenX() - actualX);
mainStage.setY(e.getScreenY() - actualY);
}
else {
//setMin();
mainStage.setX(e.getScreenX());
mainStage.setY(e.getScreenY());
}
}
//Update the status of the window from not movable to movable, after "normalize" effect
//from the dragging it when it's maximized
@FXML
private void updateStatus(MouseEvent e) {
if (mainStage.isMaximized() == false) {
isMovable = true;
}
}
//
// WINDOW RESIZING
//
/*onMouseEntered="#setMouseCursor" onMouseExited="#resetMouseCursor" onMouseDragged="#resizeWindow"*/
@FXML
private void setMouseCursor (MouseEvent e) {
minimalWindowContainer.setCursor(Cursor.CROSSHAIR);
}
@FXML
private void resetMouseCursor (MouseEvent e) {
minimalWindowContainer.setCursor(Cursor.DEFAULT);
}
@FXML
private void resizeWindow (MouseEvent e) {
actualX = e.getScreenX() - mainStage.getX() + 13;
actualY = e.getScreenY() - mainStage.getY() + 10;
if (actualX % 5 == 0 || actualY % 5 == 0) {
if (actualX > MIN_WIDTH) {
mainStage.setWidth(actualX);
} else {
mainStage.setWidth(MIN_WIDTH);
}
if (actualY > MIN_HEIGHT) {
mainStage.setHeight(actualY);
} else {
mainStage.setHeight(MIN_HEIGHT);
}
}
}
//
// Internal methods
//
//Will set the window to MAXIMIZE size
private void setMax() {
mainStage.setMaximized(true);
btnResize.setVisible(false);
btnMax.setStyle("-fx-background-image: url('/res/dSquare.png');");
minimalWindowContainer.setPadding(new Insets(0, 0, 0, 0));
}
//Will set the window to NORMAL size
private void setMin() {
mainStage.setMaximized(false);
btnResize.setVisible(true);
btnMax.setStyle("-fx-background-image: url('/res/square.png');");
minimalWindowContainer.setPadding(new Insets(SHADOW_SPACE, SHADOW_SPACE, SHADOW_SPACE, SHADOW_SPACE));
}
@Override
public void start(Stage primaryStage) {
/* //NOT SURE IF DOING RIGHT YA'
try {
//Prepare the resource with the FXML file
FXMLLoader loader = new FXMLLoader(getClass().getResource("/application/minimalWindow/MainWindow.fxml"));
//Load the main stackpane
Parent root = loader.load();
loader.setController(this);
//Prepare the content of the window, with a minWidth/Height
Scene scene = new Scene(root, MIN_WIDTH, MIN_HEIGHT);
//Making the scene transparent
scene.setFill(Color.TRANSPARENT);
//Undecorate the window due its persolalisation
primaryStage.initStyle(StageStyle.TRANSPARENT);
//Set the content of the window
primaryStage.setScene(scene); *
}
catch (Exception e) {
e.printStackTrace();
} */
}
and the CSS for styling:
* {
/* Some general colors */
primaryColor: #f9f9f9;
secondaryColor: derive(primaryColor, -75%);
textColor: white;
closeBtnColor: red;
}
#titleBar, #footer {
-fx-background-color: secondaryColor;
}
#title {
-fx-text-fill: textColor;
}
#contentArea {
-fx-background-color: primaryColor;
}
#minimalWindowShadowContainer {
-fx-background-color: transparent;
-fx-effect: dropshadow( gaussian , black , 5,0,0,0 );
-fx-background-insets: 5;
}
#btnCls, #btnMax, #btnMin, #btnResize {
-fx-background-color: transparent;
-fx-background-radius: 0;
-fx-border-color: transparent;
-fx-border-width: 0;
-fx-background-position: center;
-fx-background-repeat: stretch;
}
#btnMax:hover, #btnMin:hover {
-fx-background-color: derive(secondaryColor, 20%);
}
#btnCls:hover {
-fx-background-color: derive(red, 45%);
}
#btnCls {
-fx-background-image: url('/res/x.png');
}
#btnMax {
-fx-background-image: url('/res/square.png');
}
#btnMin {
-fx-background-image: url('/res/line.png');
}
#btnResize {
-fx-background-image: url('/res/resize.png');
}
In the App.java I should use it like this:
public class App {
public static void main(String[] args) {
//Initialize the minimal window
MinimalWindow mainWindow = new MinimalWindow();
//Show the window, after all
mainWindow.show();
}
}
I post this my solution here becouse on internet I found exactly NOTHING about custom styling in MVC pattern (yes... I need to do it for the exam project).
What are the problems? It must be simple to use and reusable. Trying to make the constructor like this:
public MinimalWindow(String title, String source) {
this.title = title;
this.source = source;
start(mainStage);
}
it gives me errors in parsing XAML file in the 11 row (the first line that define the stackpanel), or giving me an error "Caused by: java.lang.IllegalStateException: Toolkit not initialized". For the first, I don't know what is causing it. For the second, the solution on internet suggest to extend my class from Application and then override the "start" method, but it doesn't worked.
Question time: any solution? Suggestions?
PS: I make work this code in a non-mvc pattern, with a different style, and it worked great: wstaw.org/m/2016/04/07/ezgif.com-crop.gif
Upvotes: 1
Views: 867
Reputation: 330
Ok, I followed all I learned from you and I make almost work all. So, what's I having now it's this:
wstaw.org/m/2016/04/10/project.png
Now, I have FXML of the MinimalWindow:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.GridPane?>
<!-- Container that will do the "shadow" effect -->
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="BorderPane" fx:id="root" id="root" stylesheets="@MinimalWindowStyle.css" onMousePressed="#updateXY" onMouseDragged="#windowDragging" onMouseReleased="#updateStatus">
<center>
<!-- Main content -->
<BorderPane fx:id="mainWindow" id="mainWindow">
<!-- Padding will show the shadow effect under the window -->
<padding>
<Insets top="5" right="5" bottom="5" left="5"></Insets>
</padding>
<!-- Top bar of the window -->
<top>
<HBox id="titleBar" alignment="CENTER" spacing="5" prefHeight="30">
<padding>
<Insets top="5" right="5" bottom="5" left="5"/>
</padding>
<ImageView fx:id="logo" fitWidth="20" fitHeight="20"></ImageView>
<Label fx:id="lblTitle" id="title" text="MinimalWindow"></Label>
<Region HBox.hgrow="ALWAYS" prefHeight="30.0" prefWidth="200.0"></Region>
<HBox alignment="CENTER_RIGHT">
<Button id="btnMin" onMouseClicked="#minimizeApp" minHeight="20" minWidth="20" maxHeight="20" maxWidth="20"></Button>
<Button fx:id="btnMax" id="btnMax" onMouseClicked="#maximizeApp" minHeight="20" minWidth="20" maxHeight="20" maxWidth="20"></Button>
<Button id="btnCls" onMouseClicked="#closeApp" minHeight="20" minWidth="20" maxHeight="20" maxWidth="20"></Button>
</HBox>
</HBox>
</top>
<!-- Window content -->
<center>
<GridPane fx:id="contentArea" id="contentArea"></GridPane>
</center>
<!-- Footer of the window -->
<bottom>
<HBox id="footer" prefHeight="20" alignment="BOTTOM_RIGHT">
<padding>
<Insets top="5" right="5" bottom="5" left="5"/>
</padding>
<Button fx:id="btnResize" id="btnResize" onMouseClicked="#updateXY" onMouseEntered="#setMouseCursor" onMouseExited="#resetMouseCursor" onMouseDragged="#resizeWindow" minHeight="10" minWidth="10" maxHeight="10" maxWidth="10"></Button>
</HBox>
</bottom>
</BorderPane>
</center>
</fx:root>
The style for it:
* {
/* Some general colors */
primaryColor: #f9f9f9;
secondaryColor: derive(primaryColor, -75%);
textColor: white;
closeBtnColor: red;
}
#titleBar, #footer {
-fx-background-color: secondaryColor;
}
#title {
-fx-text-fill: textColor;
}
#contentArea {
-fx-background-color: primaryColor;
}
#root {
-fx-background-color: transparent;
-fx-effect: dropshadow( gaussian , black , 5,0,0,0 );
-fx-background-insets: 5;
}
#btnCls, #btnMax, #btnMin, #btnResize {
-fx-background-color: transparent;
-fx-background-radius: 0;
-fx-border-color: transparent;
-fx-border-width: 0;
-fx-background-position: center;
-fx-background-repeat: stretch;
}
#btnMax:hover, #btnMin:hover {
-fx-background-color: derive(secondaryColor, 20%);
}
#btnCls:hover {
-fx-background-color: derive(red, 45%);
}
#btnCls {
-fx-background-image: url("/resources/x.png");
}
#btnMax {
-fx-background-image: url('/resources/square.png');
}
#btnMin {
-fx-background-image: url('/resources/line.png');
}
#btnResize {
-fx-background-image: url('/resources/resize.png');
}
The controller class for it:
package controller.minimalWindow;
import application.Main;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class MinimalWindowCtrl extends BorderPane {
//Values injected from the FXML
@FXML
private BorderPane root, mainWindow;
@FXML
private Label lblTitle;
@FXML
private Button btnMax, btnResize;
@FXML
private GridPane contentArea;
//Reference to the primaryStage
final private Stage stage;
//References to min/max width/height and the shadow effect
final private int MINWIDTH, MINHEIGHT, SHADOWSPACE = 5;
//Things for the resizing/moving window
private double actualX, actualY;
private boolean isMovable = true;
public MinimalWindowCtrl (Stage stage, int minwidth, int minheight) {
//First, take the reference to the stage
this.stage = stage;
//Taking the references to the window
MINWIDTH = minwidth;
MINHEIGHT = minheight;
//Then load the window, setting the root and controller
FXMLLoader loader = new FXMLLoader(getClass().getResource("../../view/minimalWindow/MinimalWindow.fxml"));
loader.setRoot(this);
loader.setController(this);
//Try to load
try {
loader.load();
}
catch (Exception e) {
e.printStackTrace();
//TODO Show a message error
Main.close();
}
}
public void setTitle(String s) {
lblTitle.setText(s);
}
public void setContent(Node node) {
contentArea.getChildren().add(node);
}
//
// MIMIZIE | MAXIMIZE | CLOSE
//
//When pressed, will minimize the window to tray
@FXML
private void minimizeApp(MouseEvent e) {
stage.setIconified(true);
}
//When pressed, check if it must maximize or restore the window
@FXML
private void maximizeApp(MouseEvent e) {
if (stage.isMaximized()) {
setMin();
isMovable = true;
}
else {
setMax();
isMovable = false;
}
}
//When pressed, will kill the window
@FXML
private void closeApp(MouseEvent e) {
stage.close();
System.exit(0);
}
//
// WINDOW MOVING
//
//When i must update the XY of the click
@FXML
private void updateXY(MouseEvent e){
actualX = e.getScreenX() - stage.getX();
actualY = e.getScreenY() - stage.getY();
}
//When pressing and dragging the mouse it will move the window
@FXML
private void windowDragging(MouseEvent e) {
if (isMovable) {
stage.setX(e.getScreenX() - actualX);
stage.setY(e.getScreenY() - actualY);
}
else {
//setMin();
stage.setX(e.getScreenX());
stage.setY(e.getScreenY());
}
}
//Update the status of the window from not movable to movable, after "normalize" effect
//from the dragging it when it's maximized
@FXML
private void updateStatus(MouseEvent e) {
if (stage.isMaximized() == false) {
isMovable = true;
}
}
//
// WINDOW RESIZING
//
/*onMouseEntered="#setMouseCursor" onMouseExited="#resetMouseCursor" onMouseDragged="#resizeWindow"*/
@FXML
private void setMouseCursor (MouseEvent e) {
mainWindow.setCursor(Cursor.CROSSHAIR);
}
@FXML
private void resetMouseCursor (MouseEvent e) {
mainWindow.setCursor(Cursor.DEFAULT);
}
@FXML
private void resizeWindow (MouseEvent e) {
actualX = e.getScreenX() - stage.getX() + 13;
actualY = e.getScreenY() - stage.getY() + 10;
if (actualX % 5 == 0 || actualY % 5 == 0) {
if (actualX > MINWIDTH) {
stage.setWidth(actualX);
} else {
stage.setWidth(MINWIDTH);
}
if (actualY > MINHEIGHT) {
stage.setHeight(actualY);
} else {
stage.setHeight(MINHEIGHT);
}
}
}
//
// Internal methods
//
//Will set the window to MAXIMIZE size
private void setMax() {
stage.setMaximized(true);
btnResize.setVisible(false);
btnMax.setStyle("-fx-background-image: url('/res/dSquare.png');");
mainWindow.setPadding(new Insets(0, 0, 0, 0));
}
//Will set the window to NORMAL size
private void setMin() {
stage.setMaximized(false);
btnResize.setVisible(true);
btnMax.setStyle("-fx-background-image: url('/res/square.png');");
mainWindow.setPadding(new Insets(SHADOWSPACE, SHADOWSPACE, SHADOWSPACE, SHADOWSPACE));
}
}
And in the Main.java I do:
package application;
import controller.MainWindowCtrl;
import controller.minimalWindow.MinimalWindowCtrl;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.scene.Scene;
import javafx.scene.control.TabPane;
public class Main extends Application {
final private int MINWIDTH = 750, MINHEGIHT = 500;
@Override
public void start(Stage primaryStage) {
try {
//Preparing the model
//TODO the interface of model
Object m = new Object();
//Loading main content
FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/MainWindow.fxml"));
TabPane mainPane = loader.load();
//Setting the model for the controller
((MainWindowCtrl) loader.getController()).setModel(m);
//Creating the style for the custom window
MinimalWindowCtrl minimalWindowCtrl = new MinimalWindowCtrl(primaryStage, MINWIDTH, MINHEGIHT);
minimalWindowCtrl.setContent(mainPane);
//Making new scene
Scene scene = new Scene(minimalWindowCtrl, MINWIDTH, MINHEGIHT);
//Setting the style to the window (undecorating it)
primaryStage.initStyle(StageStyle.TRANSPARENT);
//Setting the scene on the window
primaryStage.setScene(scene);
//Showing the window
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
public static void close() {
System.exit(0);
}
}
It's missing some features, like "I don't know why the icons for buttons is not showed", the shadow is still buggy, but it generally works.
This is the result:
Upvotes: 0
Reputation: 209225
The Application
class represents the whole application. It does not represent a window. Windows in JavaFX are represented by the Stage
class. The Application.start()
method is the entry point (start) for a JavaFX application: you should consider it as the replacement for the main
in a "regular" Java application. The Application
subclass instance is created for you as part of the launch process, which also starts the FX toolkit. In the Oracle JDK, the launch process can be initiated by invoking the Java runtime (e.g. invoking java
from the command line) and specifying an Application
subclass as the class to execute. For environments that don't support direct launch of JavaFX applications, you should include a main
method that invokes Application.launch(args)
, i.e.
public class MyApp extends Application {
@Override
public void start(Stage primaryStage) {
// create objects and set up GUI, etc
}
public static void main(String[] args) {
launch(args);
}
}
Consequently
Application
subclass is inherently not reusable, and you should keep the start(...)
method as minimal as possible (it should basically do nothing but, well, start the application).Application
subclass in any JVMApplication
class as the controller classSo to do what you are trying to do, I think you want to create a separate MinimalWindow
class that is not an Application
subclass. Use the Custom Component pattern described in the FXML documentation to have it load its own FXML and set itself as the controller class. Then you can create a minimal main class, extending Application
whose start
method creates and shows MinimalWindow
.
Upvotes: 2