I am working with the Javafx GUI but i also require the same level of functionality from the command line. I am wondering what the best way to make a main class which has functionality for both command line and Javafx at the same time so you can do one thing on the GUI and then do the next thing on command line. Command line would also update the GUI display.
(Really, this question is off-topic, as it is too broad. It was interesting enough, though, for me to try a proof of concept of the approach that seemed natural to me, so I answered it anyway.)
You essentially need two things here:
Use an MVC approach, with the model containing the data. You can share the same model instance with both the command line interface and the UI, so both update the same data. The UI, as usual, will observe the model and update if the data change.
Launch the CLI from the JavaFX application's start() method, running it in a background thread so that it doesn't block the UI. You just need to make sure there that the model makes updates on the correct (i.e. the FX Application) thread.
Here's a simple example, which just computes the total of a list of integers. Here's the model, which stores the list and the total. It has methods to add a new value, or clear the list. Note how those methods execute their changes on the UI thread:
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class AddingModel {
private final ObservableList<Integer> values = FXCollections.observableArrayList();
private final ReadOnlyIntegerWrapper total = new ReadOnlyIntegerWrapper();
public AddingModel() {
total.bind(Bindings.createIntegerBinding(() ->
values.stream().collect(Collectors.summingInt(Integer::intValue)),
values));
}
private void ensureFXThread(Runnable action) {
if (Platform.isFxApplicationThread()) {
action.run();
} else {
Platform.runLater(action);
}
}
public void clear() {
ensureFXThread(values::clear);
}
public void addValue(int value) {
ensureFXThread(() -> values.add(value));
}
public final ReadOnlyIntegerProperty totalProperty() {
return this.total.getReadOnlyProperty();
}
public final int getTotal() {
return this.totalProperty().get();
}
public ObservableList<Integer> getValues() {
return values ;
}
}
Here's the UI code. First a view, in FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="AddingController">
<top>
<HBox spacing="5">
<TextField fx:id="valueField" onAction="#addValue"/>
<Button text="Clear" onAction="#clearValues"/>
</HBox>
</top>
<center>
<ListView fx:id="values"/>
</center>
<bottom>
<Label fx:id="sum"/>
</bottom>
</BorderPane>
and a controller, which observes and updates the model:
import java.util.function.UnaryOperator;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
public class AddingController {
private final AddingModel model ;
#FXML
private TextField valueField ;
#FXML
private ListView<Integer> values ;
#FXML
private Label sum ;
public AddingController(AddingModel model) {
this.model = model ;
}
#FXML
private void initialize() {
values.setItems(model.getValues());
sum.textProperty().bind(model.totalProperty().asString("Total = %,d"));
// Allow only integer values in the text field:
UnaryOperator<TextFormatter.Change> filter = c -> {
if (c.getControlNewText().matches("-?[0-9]*")) {
return c;
} else {
return null ;
}
};
valueField.setTextFormatter(new TextFormatter<>(filter));
}
#FXML
private void addValue() {
String text = valueField.getText();
if (! text.isEmpty()) {
int value = Integer.parseInt(text);
model.addValue(value);
valueField.clear();
}
}
#FXML
private void clearValues() {
model.clear();
}
}
Now a simple command line interpreter, which reads from the command line and references the model. It supports either integer entry (add value to the model), or the commands total, show, or clear:
import java.util.List;
import java.util.Scanner;
import java.util.regex.Pattern;
public class AddingCLI {
private final AddingModel model ;
private final Pattern intPattern = Pattern.compile("-?[0-9]+");
public AddingCLI(AddingModel model) {
this.model = model ;
}
public void processCommandLine() {
try (Scanner in = new Scanner(System.in)) {
while (true) {
String input = in.next().trim().toLowerCase();
if (intPattern.matcher(input).matches()) {
int value = Integer.parseInt(input);
model.addValue(value);
} else if ("show".equals(input)) {
outputValues();
} else if ("clear".equals(input)) {
model.clear();
System.out.println("Values cleared");
} else if ("total".equals(input)) {
System.out.println("Total = "+model.getTotal());
}
}
}
}
private void outputValues() {
List<Integer> values = model.getValues();
if (values.isEmpty()) {
System.out.println("No values");
} else {
values.forEach(System.out::println);
}
}
}
Finally, the JavaFX application which assembles all these. Note that the same model instance is passed to both the CLI and the UI controller, so both are updating the same data. You can enter some values in the text field, then type "show" in the command line, and you'll see the values. Type "clear" in the command line, and the values will be removed from the UI, etc.
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AddingApp extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
AddingModel model = new AddingModel();
AddingController controller = new AddingController(model);
FXMLLoader loader = new FXMLLoader(AddingController.class.getResource("ValueTotaler.fxml"));
loader.setControllerFactory(type -> {
if (type == AddingController.class) {
return controller ;
} else {
throw new IllegalArgumentException("Unexpected controller type: "+type);
}
});
Parent root = loader.load();
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
AddingCLI cli = new AddingCLI(model);
Thread cliThread = new Thread(cli::processCommandLine);
cliThread.setDaemon(true);
cliThread.start();
}
public static void main(String[] args) {
launch(args);
}
}
You can, of course, just create the UI without the CLI, or create the CLI without the UI; both are independent of each other (they just both depend on the model).
I think this is borderline to be too broad. One part of that is: your requirements are unclear. Do you intend to you use the command line like:
java -jar whatever.jar -command A
java -jar whatever.jar -command B
java -jar whatever.jar -command C
So - you invoke java repeatedly, and whatever.jar basically is a client that takes to some "server" that does the real work, or do you envision
java -jar whatever.jar
> Type your command:
> A
... ran command A
> Type your command:
Obviously, that makes a huge difference here.
But in the end, it also tells us where a solution is: by de-coupling these clients from the actual execution.
Meaning: you should do two things
define the functionality aka services that some server has to provide
then you can look into ways of creating different clients that make use of these services
Avoid baking all of these different aspects into one single main() method!
Everything on the GUI is Event based. This means that methods get called when you press a button or interact with a JavaFX Window in another way like selecting an item on a list.
I suggest keeping your internal logic and GUI logic seperated.
When clicking on a button you call a handleButton(ActionEvent actionEvent) method that is linked to the button. This method should call a method in one of your other classes that actually contains the logic.
You can get user input through the command line with a scanner:
public String getUserInput() {
Scanner scan = new Scanner(System.in);
String s = scan.next();
return s
}
You can now check this user input string and connect the corresponding method with a switch(s) statement.
I'm not sure WHEN you want to get this input through the command line, but I suggest adding a developer button in your Stage that calls getUserInput() when pressed.
Related
I am working on coding a modular soundboard in Java and JavaFX using FXML. One of key aspects is the ability to load an arbitrary class that implements the Plugin abstract class at runtime.
The plugin abstract class looks like this:
import javafx.scene.control.Tab;
import java.io.File;
import java.io.FileReader;
import java.net.URLClassLoader;
import java.util.Properties;
public abstract class Plugin {
protected Tab mainTab;
protected Tab settingsTab;
protected Properties propertyFile = new Properties();
protected File fileName;
protected URLClassLoader loader;
public Plugin(){
mainTab = new Tab();
}
public Plugin(String tabName){
mainTab = new Tab(tabName);
settingsTab = new Tab(tabName);
}
public abstract void save();
public Tab getMainTab(){
return mainTab;
}
public Tab getSettingsTab(){
return settingsTab;
}
public void initPropertyFile(String filename){
try{
File file = new File(System.getenv("APPDATA")+"\\Testing\\"+filename+".properties");
if(file.exists())
propertyFile.load(new FileReader(file));
else{
file.createNewFile();
propertyFile.load(new FileReader(file));
}
}catch(Exception e){
e.printStackTrace();
}
}
public void setClassLoader(URLClassLoader loader){
this.loader = loader;
}
public abstract void initController();
public Properties getPropertyFile(){
return propertyFile;
}
}
I use a URLClassLoader to load any classes found in a specified JAR file, and then I instantiate any instances of Plugin and collect them inside a list. Where I am having an issue is creating an FXML controller to use for these classes that I instantiate at runtime. Because both these classes and the controllers are loaded at runtime using a custom URLClassLoader I am unable to just specify a FXML controller inside my FXML files. If I try to, I get an error saying that the class I am using for a controller could not be found. If I don't specify a controller inside the FXML file, it is able to load the UI components, but I can't specify any actions inside it. I can set a controller using the FXMLLoader.setController() method, but when I load a controller like this though, none of the buttons I am using have any actions. I've tried using the initialize() method with FXML annotations, but this method isn't automatically called by the FXMLLoader. If I call initialize myself then it seems like the FXML hasn't been injected yet, but the UI components specified by the FXML file appear with the formatting specified by the FXML file on my screen.
import java.net.URL;
public class Plugin3 extends Plugin {
FXMLLoader mainTabLoader;
URL testURL;
public Plugin3(){
//Calls Plugin Constructor to initialize mainTab and settingsTab, and set the UI names of the tabs
super("Plugin3");
//Try creating FXMLLoader to setup the UI of the plugin
try {
mainTabLoader = new FXMLLoader();
//mainTabLoader.setController(this);
Plugin3 thisObj = mainTabLoader.getController();
testURL = getClass().getResource("testFXML.fxml");
//Load the FXML file
} catch (Exception e) {
e.printStackTrace();
}
}
#Override
public void save() {
}
//Attempts to set the controller of the FXMLLoader mainTabLoader and call the initialize method of that controller
#Override
public void initController() {
try {
String classPath = "com.example.plugindevelopment.Plugin3Controller";
mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
getMainTab().setContent(mainTabLoader.load(testURL));
mainTabLoader.getController().getClass().getDeclaredMethod("initialize").invoke(mainTabLoader.getController());
}catch(Exception e){
e.printStackTrace();
}
}
}
This is the controller:
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.VBox;
public class Plugin3Controller
{
#FXML
VBox root;
#FXML
Label label;
#FXML
Slider slider;
#FXML
Button button1;
protected void onButtonClick(){
System.out.println("Hello");
}
#FXML
public void initialize(){
System.out.println("Initializing Plugin3Controller");
button1 = new Button("Hello");
button1.setOnAction(event -> onButtonClick());
}
}
and this is the FXML file:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
<children>
<VBox fx:id="root" prefHeight="200.0" prefWidth="100.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<Label fx:id="label" prefHeight="137.2" text="text" />
<Slider fx:id="slider">
<VBox.margin>
<Insets top="100.0" />
</VBox.margin>
<padding>
<Insets top="50.0" />
</padding></Slider>
<Button fx:id="button1" mnemonicParsing="false" text="Click Me" />
</children></VBox>
</children>
</AnchorPane>
I would appreciate any insight anyone can provide, thank you.
Because both these classes and the controllers are loaded at runtime using a custom URLClassLoader I am unable to just specify a FXML controller inside my FXML files. If I try to, I get an error saying that the class I am using for a controller could not be found.
Note that you can set the Class Loader to be used by the FXMLLoader.
The solution you attempt in which you set the controller manually has a number of errors. Most importantly:
String classPath = "com.example.plugindevelopment.Plugin3Controller";
mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
getMainTab().setContent(mainTabLoader.load(testURL));
Here mainTabLoader.load(testURL) invokes the static method FXMLLoader.load(URL). Since this is a static method, it doesn't reference the FXMLLoader instance mainTabLoader at all, and in particular won't be aware of the controller instance.
You should use
String classPath = "com.example.plugindevelopment.Plugin3Controller";
mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
mainTabLoader.setLocation(testURL);
getMainTab().setContent(mainTabLoader.load());
Note the call to load() has no arguments, invoking the instance method FXMLLoader.load().
Once the FXMLLoader is properly referencing a controller instance, it will invoke initialize() automatically. You should remove the line
mainTabLoader.getController().getClass().getDeclaredMethod("initialize").invoke(mainTabLoader.getController());
You also have an error in your controller implementation (I'm guessing this may have been a desperate attempt to fix the problems caused by the error above).
It is always a mistake to instantiate objects and assign them to #FXML-annotated fields, as you do with
button1 = new Button("Hello");
Remove this line. It causes the event handler to be set on a button that is not displayed in the UI, instead of on the button that is declared in the FXML file.
In all you should have
public class Plugin3Controller
{
#FXML
VBox root;
#FXML
Label label;
#FXML
Slider slider;
#FXML
Button button1;
protected void onButtonClick(){
System.out.println("Hello");
}
#FXML
public void initialize(){
System.out.println("Initializing Plugin3Controller");
button1.setOnAction(event -> onButtonClick());
}
}
and
public class Plugin3 extends Plugin {
FXMLLoader mainTabLoader;
URL testURL;
public Plugin3(){
//Calls Plugin Constructor to initialize mainTab and settingsTab, and set the UI names of the tabs
super("Plugin3");
//Try creating FXMLLoader to setup the UI of the plugin
mainTabLoader = new FXMLLoader();
//mainTabLoader.setController(this);
// This line is patently nonsense: remove it:
// Plugin3 thisObj = mainTabLoader.getController();
testURL = getClass().getResource("testFXML.fxml");
//Load the FXML file
}
#Override
public void save() {
}
//Attempts to set the controller of the FXMLLoader mainTabLoader and call the initialize method of that controller
#Override
public void initController() {
try {
String classPath = "com.example.plugindevelopment.Plugin3Controller";
mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
mailTabLoader.setLocation(testURL);
getMainTab().setContent(mainTabLoader.load());
}catch(Exception e){
e.printStackTrace();
}
}
}
This question already has answers here:
How to reference javafx fxml files in resource folder?
(3 answers)
How do I determine the correct path for FXML files, CSS files, Images, and other resources needed by my JavaFX Application?
(1 answer)
Javafx - Can application class be the controller class
(2 answers)
Closed 1 year ago.
Let's say I have three files:
Controller.java
Creator.java
Scene.fxml
Scene.fxml's controller is set to Controller.java. Controller.java calls a method from Creator.java which creates a new scene using FXMLLoader.load( ) method, then passes this scene to newly created stage and returns this stage. Controller.java calls .show() method on that returned stage. Everything is great so far, the window did pop up, but the issue is that I can not access any node from Scene.fxml in Controller.java. I naturally have "fx:id" stuff in Scene.fxml and I've also created all those nodes in Controller.java with the exact name and #FXML annotation. Anyway, they are all set to NULL.
I assume that the issue might be connected with FXMLLoader.load() method being called from s class that is not the class controller. So the question is: am I right? i
If I am, is there any way to actually make it work the way I want?
If the explanation is not good enough I will create a minimal reproducible example.
EDIT:
Controller.java:
package Issue;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.stage.Stage;
public class Controller extends Application {
#FXML
private Label label;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
stage = Creator.createStage(stage);
stage.show();
System.out.println("Is label null?");
if(label == null){
System.out.println("yes");
} else {
System.out.println("no");
}
}
}
Creator.java:
package Issue;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Creator {
public static Stage createStage(Stage stage) {
Parent root = null;
String pathToProject = System.getProperty("user.dir");
Path path = Paths.get(pathToProject, "src", "main", "java", "Issue", "Scene.fxml");
try{
URL url = new URL("file", "", path.toString());
root = FXMLLoader.load(url);
} catch (IOException | NullPointerException e) {
System.out.println("wrong url to fxml");
}
stage.setScene(new Scene(root, 400, 400));
return stage;
}
}
Scene.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="Issue.Controller"
prefHeight="400.0" prefWidth="600.0">
<center>
<Label fx:id="label" text="hey stackoverflow"/>
</center>
</BorderPane>
module-info.java:
module Main {
requires javafx.controls;
requires javafx.fxml;
opens Issue to javafx.fxml;
exports Issue;
}
I've created standard Maven projects and created "Issue" package with those files in src/main/java path. module-info.java is in src/main/java.
I'm developing an IntelliJ plugin that has its own tool window. The plugin should invoke IntelliJ's built-in rename refactor function to rename a variable when the user presses a key. When I run my example the following exception is thrown when I press a key to invoke the rename refactor function:
2020-05-16 23:03:17,741 [ 41062] ERROR - llij.ide.plugins.PluginManager - EventQueue.isDispatchThread()=false Toolkit.getEventQueue()=com.intellij.ide.IdeEventQueue#574ed46a
Current thread: Thread[JavaFX Application Thread,6,Idea Thread Group] 648578026
Write thread (volatile): Thread[AWT-EventQueue-0,6,Idea Thread Group] 807407917com.intellij.openapi.diagnostic.Attachment#339b1167
com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments: EventQueue.isDispatchThread()=false Toolkit.getEventQueue()=com.intellij.ide.IdeEventQueue#574ed46a
Current thread: Thread[JavaFX Application Thread,6,Idea Thread Group] 648578026
Write thread (volatile): Thread[AWT-EventQueue-0,6,Idea Thread Group] 807407917com.intellij.openapi.diagnostic.Attachment#339b1167
at com.intellij.openapi.application.impl.ApplicationImpl.assertIsWriteThread(ApplicationImpl.java:1068)
at com.intellij.openapi.application.impl.ApplicationImpl.startWrite(ApplicationImpl.java:1154)
at com.intellij.openapi.application.impl.ApplicationImpl.runWriteAction(ApplicationImpl.java:974)
at MyToolWindowFactory.handle(MyToolWindowFactory.java:55)
at MyToolWindowFactory.handle(MyToolWindowFactory.java:17)
I thought that calling the setName function as a lambda inside ApplicationManager.getApplication().runWriteAction would work, but apparently not. How can I get it to work?
Here is the complete code I used.
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.psi.*;
import javafx.embed.swing.JFXPanel;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
public class MyToolWindowFactory implements ToolWindowFactory, EventHandler<KeyEvent> {
PsiField[] variables;
#Override
public void createToolWindowContent(#NotNull Project project, #NotNull ToolWindow toolWindow) {
final JFXPanel fxPanel = new JFXPanel();
JComponent component = toolWindow.getComponent();
ApplicationManager.getApplication().invokeLater(() -> {
PsiClass currentClass = getCurrentClass(project);
variables = currentClass.getFields();
Scene scene = new Scene(new VBox(), 400, 250);
scene.setOnKeyPressed(this);
fxPanel.setScene(scene);
fxPanel.requestFocus();
});
component.getParent().add(fxPanel);
}
private PsiClass getCurrentClass(Project project) {
// Get the currently selected file.
FileEditorManager manager = FileEditorManager.getInstance(project);
VirtualFile[] files = manager.getSelectedFiles();
VirtualFile currentFile = files[0];
// Get the PsiClass for the currently selected file.
PsiFile psiFile = PsiManager.getInstance(project).findFile(currentFile);
PsiJavaFile psiJavaFile = (PsiJavaFile)psiFile;
final PsiClass[] classes = psiJavaFile.getClasses();
return classes[0];
}
#Override
public void handle(KeyEvent event) {
ApplicationManager.getApplication().runWriteAction(() -> {
variables[0].setName("newVariableName");
});
}
}
One way to get it to work is to call the rename function from the WriteCommandAction.runWriteCommandAction method. In addition calling just setName only renames the variable declaration. To rename all references to the variable I first searched for all the variable's references with the ReferencesSearch.search method, and then called handleElementRename on each reference.
public void handle(KeyEvent event) {
Runnable r = ()->
{
for (PsiReference reference : ReferencesSearch.search(variables[0])) {
reference.handleElementRename(variableName);
}
variables[0].setName(variableName);
}
WriteCommandAction.runWriteCommandAction(project, r);
}
Got the hint to use WriteCommandAction from here:
https://intellij-support.jetbrains.com/hc/en-us/community/posts/206754235
I have a JavaFX in which the user can select files to be processed. Now I want to automate it so that you can run the application from the command line and pass those files as a parameter. I tried to do this:
java -jar CTester.jar -cl file.txt
public static void main(String[] args)
{
if (Arrays.asList(args).contains("-cl"))
{
foo();
}
else
{
launch(args);
}
}
The main is executed and the argument is correct, but this still creates the GUI.
From the docs:
The entry point for JavaFX applications is the Application class. The
JavaFX runtime does the following, in order, whenever an application
is launched:
Constructs an instance of the specified Application class
Calls the init() method
Calls the start(javafx.stage.Stage) method
Waits for the application to finish, which happens when either of the following
occur:
the application calls Platform.exit()
the last window has been closed and the implicitExit attribute on Platform is true
Calls the stop() method
So if I cannot use the main method, how can I create this "alterantive" flow? I thought about creating a normal java application as a wrapper but that seems a little bit overkill for such a simple task. Is there a more elegant way of doing this?
Simply exit the application after calling your foo() method:
Platform.exit();
Here is a quick sample application to demonstrate:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class CLSample extends Application {
public static void main(String[] args) {
if (Arrays.asList(args).contains("-cl")) {
commandLine();
Platform.exit();
} else {
launch(args);
}
}
public static void commandLine() {
System.out.println("Running only command line version...");
}
#Override
public void start(Stage primaryStage) {
// Simple Interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
root.getChildren().add(new Label("GUI Loaded!"));
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("CLSample Sample");
primaryStage.show();
}
}
If you pass -cl, then only the commandLine() method gets called.
Pretty much, I'm trying to write a simple program that lets the user choose a file. Unfortunately, JFileChooser through Swing is a little outdated, so I am trying to use JavaFX FileChooser for this. The goal is to run FileGetter as a thread, transfer the file data to the Main Class, and continue from there.
Main Class:
package application;
import java.io.File;
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(new FileGetter());
FileGetter fg = new FileGetter();
t1.start();
boolean isReady = false;
while(isReady == false){
isReady = FileGetter.getIsReady();
}
File file = FileGetter.getFile();
System.out.println(file.getAbsolutePath());
...
}
}
FileGetter Class:
package application;
import java.io.File;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
public class FileGetter extends Application implements Runnable {
static File file;
static boolean isReady = false;
#Override
public void start(Stage primaryStage) {
try {
FileChooser fc = new FileChooser();
while(file == null){
file = fc.showOpenDialog(primaryStage);
}
isReady = true;
Platform.exit();
} catch(Exception e) {
e.printStackTrace();
}
}
#Override
public void run() {
launch();
}
public static boolean getIsReady(){
return isReady;
}
public static File getFile(){
return file;
}
}
Problem is that the value of isReady in the while loop doesn't update to true when the user picked a file (the reason I have it is to prevent the code in Main from continuing with a File set to null).
Any help, alternative suggestions, or explanations as of why this happens is very much appreciated!
The java memory model does not require variable values to be the same in different threads except under specific conditions.
What is happening here is that the FileGetter thread is updating the value in the own memory that is only accessed from this thread, but your main thread doesn't see the updated value, since it only sees the version of the variable stored in it's own memory that is different from the one of the FileGetter thread. Each of the threads has it's own copy of the field in memory, which is perfectly fine according to the java specification.
To fix this, you can simply add the volatile modifier to isReady:
static volatile boolean isReady = false;
which makes sure the updated value will be visible from your main thread.
Furthermore I recommend reducing the number of FileGetter instances you create. In your code 3 instances are created, but only 1 is used.
Thread t1 = new Thread(() -> Application.launch(FileGetter.class));
t1.start();
...
The easiest way to implement this
Instead of trying to drive the horse with the cart, why not just follow the standard JavaFX lifecycle? In other words, make your Main class a subclass of Application, get the file in the start() method, and then proceed (in a background thread) with the rest of the application?
public class Main extends Application {
#Override
public void init() {
// make sure we don't exit when file chooser is closed...
Platform.setImplicitExit(false);
}
#Override
public void start(Stage primaryStage) {
File file = null ;
FileChooser fc = new FileChooser();
while(file == null){
file = fc.showOpenDialog(primaryStage);
}
final File theFile = file ;
new Thread(() -> runApplication(theFile)).start();
}
private void runApplication(File file) {
// run your application here...
}
}
What is wrong with your code
If you really want the Main class to be separate from the JavaFX Application class (which doesn't really make sense: once you have decided to use a JavaFX FileChooser, you have decided you are writing a JavaFX application, so the startup class should be a subclass of Application), then it gets a bit tricky. There are several issues with your code as it stands, some of which are addressed in other answers. The main issue, as shown in Fabian's answer, is that you are referencing FileGetter.isReady from multiple threads without ensuring liveness. This is exactly the issue addressed in Josh Bloch's Effective Java (Item 66 in the 2nd edition).
Another issue with your code is that you won't be able to use the FileGetter more than once (you can't call launch() more than once), which might not be an issue in your code now, but almost certainly will be at some point with this application as development progresses. The problem is that you have mixed two issues: starting the FX toolkit, and retrieving a File from a FileChooser. The first thing must only be done once; the second should be written to be reusable.
And finally your loop
while(isReady == false){
isReady = FileGetter.getIsReady();
}
is very bad practice: it checks the isReady flag as fast as it possibly can. Under some (fairly unusual) circumstances, it could even prevent the FX Application thread from having any resources to run. This should just block until the file is ready.
How to fix without making Main a JavaFX Application
So, again only if you have a really pressing need to do so, I would first create a class that just has the responsibility of starting the FX toolkit. Something like:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
public class FXStarter extends Application {
private static final AtomicBoolean startRequested = new AtomicBoolean(false);
private static final CountDownLatch latch = new CountDownLatch(1);
#Override
public void init() {
Platform.setImplicitExit(false);
}
#Override
public void start(Stage primaryStage) {
latch.countDown();
}
/** Starts the FX toolkit, if not already started via this method,
** and blocks execution until it is running.
**/
public static void startFXIfNeeded() throws InterruptedException {
if (! startRequested.getAndSet(true)) {
new Thread(Application::launch).start();
}
latch.await();
}
}
Now create a class that gets a file for you. This should ensure the FX toolkit is running, using the previous class. This implementation allows you to call getFile() from any thread:
import java.io.File;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import javafx.application.Platform;
import javafx.stage.FileChooser;
public class FileGetter {
/**
** Retrieves a file from a JavaFX File chooser. This method can
** be called from any thread, and will block until the user chooses
** a file.
**/
public File getFile() throws InterruptedException {
FXStarter.startFXIfNeeded() ;
if (Platform.isFxApplicationThread()) {
return doGetFile();
} else {
FutureTask<File> task = new FutureTask<File>(this::doGetFile);
Platform.runLater(task);
try {
return task.get();
} catch (ExecutionException exc) {
throw new RuntimeException(exc);
}
}
}
private File doGetFile() {
File file = null ;
FileChooser chooser = new FileChooser() ;
while (file == null) {
file = chooser.showOpenDialog(null) ;
}
return file ;
}
}
and finally your Main is just
import java.io.File;
public class Main {
public static void main(String[] args) throws InterruptedException {
File file = new FileGetter().getFile();
// proceed...
}
}
Again, this is pretty complex; I see no reason not to simply use the standard FX Application lifecycle for this, as in the very first code block in the answer.
In this code
while(isReady == false){
isReady = FileGetter.getIsReady();
}
there is nothing that is going to change the state of FileGetter's isReady to true