I have some signal processing data which gets fed at roughly at 50Hz. I need to update a rectangle's opacity based on the signal value in real time. I am trying to develop the UI in JavaFX 8.
For time being I am simulating the signal value using random number generator in JavaFX service in my code.
I am using Platform.runLater to update the UI, however this doesn't update values in real time, I read through similar problems encountered by others and the normal suggestion is that not to call Platform.runLater often but to batch the updates.
In my case if I batch my updates, the frequency at which the opacity changes will not be equal to the signal frequency.
Any thoughts on how to achieve this?
public class FlickerController
{
#FXML
private Rectangle leftBox;
#FXML
private Rectangle rightBox;
#FXML
private ColorPicker leftPrimary;
#FXML
private ColorPicker leftSecondary;
#FXML
private ColorPicker rightPrimary;
#FXML
private ColorPicker rightSecondary;
#FXML
private Slider leftFrequency;
#FXML
private Slider rightFrequency;
#FXML
private Button startButton;
#FXML
private Label leftfreqlabel;
#FXML
private Label rightfreqlabel;
#FXML
private Label rightBrightness;
#FXML
private Label leftBrightness;
private boolean running = false;
DoubleProperty leftopacity = new SimpleDoubleProperty(1);
DoubleProperty rightopacity = new SimpleDoubleProperty(1);
private FlickerThread ftLeft;
private FlickerThread ftRight;
public void initialize()
{
leftopacity.addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable,
Number oldValue, Number newValue)
{
Platform.runLater(new Runnable()
{
#Override
public void run()
{
double brightness = leftopacity.doubleValue();
leftBrightness.setText(""+brightness);
leftBox.opacityProperty().set(brightness);
}
});
}
});
rightopacity.addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable,
Number oldValue, Number newValue)
{
Platform.runLater(new Runnable()
{
#Override
public void run()
{
double brightness = rightopacity.doubleValue();
rightBrightness.setText(""+brightness);
rightBox.opacityProperty().set(brightness);
}
});
}
});
startButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event)
{
if(running)
{
synchronized(this)
{
running=false;
}
startButton.setText("Start");
}
else
{
running=true;
ftLeft = new FlickerThread((int)leftFrequency.getValue(),leftopacity);
ftRight = new FlickerThread((int)rightFrequency.getValue(), rightopacity);
try
{
ftLeft.start();
ftRight.start();
}
catch(Throwable t)
{
t.printStackTrace();
}
startButton.setText("Stop");
}
}
});
leftFrequency.valueProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable,
Number oldValue, Number newValue)
{
leftfreqlabel.setText(newValue.intValue()+"");
}
});
rightFrequency.valueProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable,
Number oldValue, Number newValue)
{
rightfreqlabel.setText(newValue.intValue()+"");
}
});
}
class FlickerThread extends Service<Void>
{
private long sleeptime;
DoubleProperty localval = new SimpleDoubleProperty(1) ;
public FlickerThread(int freq, DoubleProperty valtoBind)
{
this.sleeptime = (1/freq)*1000;
valtoBind.bind(localval);
}
#Override
protected Task <Void>createTask()
{
return new Task<Void>() {
#Override
protected Void call() throws Exception
{
while(running)
{
double val = Math.random();
System.out.println(val);
localval.setValue(val);
Thread.sleep(sleeptime);
}
return null;
}
};
}
}
}
class FlickerThread extends Thread
{
private long sleeptime;
final AtomicReference<Double> counter = new AtomicReference<>(new Double(-1.0));
private Label label;
private Rectangle myrect;
public FlickerThread(int freq, Label label,Rectangle rect)
{
this.sleeptime = (long) ((1.0/freq)*1000.0);
System.out.println("Sleep time is "+sleeptime);
this.label = label;
this.myrect = rect;
}
#Override
public void run() {
double count = 1.0 ;
while (running) {
count = Math.random();
if (counter.getAndSet(count) == -1) {
updateUI(counter, label,myrect);
try
{
Thread.sleep(sleeptime);
} catch (InterruptedException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
private void updateUI(final AtomicReference<Double> counter,
final Label label, final Rectangle myrect) {
Platform.runLater(new Runnable() {
#Override
public void run() {
double val = counter.getAndSet(-1.0);
final String msg = String.format("Brt: %,f", val);
label.setText(msg);
myrect.opacityProperty().set(val);
}
});
}
You have a calculation error in your code.
Consider:
1/100*1000=0
But:
1.0/100*1000=10.0
i.e. you need to use floating point arithmetic, not integer arithmetic.
There are numerous other issues with your code as pointed out in my previous comment, so this answer is more of a code review and suggested approach than anything else.
You can batch updates to runLater as in James's answer to Throttling javafx gui updates. But for an update rate of 100 hertz max, it isn't going to make a lot of difference performance-wise as JavaFX generally operates on a 60 hertz pulse cycle, unless you really overload it (which you aren't really doing in your example). So the savings you get by throttling updates will be pretty minimal.
Here is a sample you can try out (it uses James's input throttling technique):
import javafx.application.*;
import javafx.beans.property.DoubleProperty;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
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 java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
public class InputApp extends Application {
private final ToggleButton controlButton = new ToggleButton("Start");
private final Rectangle box = new Rectangle(100, 100, Color.BLUE);
private final Label brightness = new Label();
private final Label frequencyLabel = new Label();
private final Slider frequency = new Slider(1, 100, 10);
private InputTask task;
#Override
public void start(Stage stage) throws Exception {
// initialize bindings.
brightness.textProperty().bind(
box.opacityProperty().asString("%.2f")
);
frequencyLabel.textProperty().bind(
frequency.valueProperty().asString("%.0f")
);
frequency.valueChangingProperty().addListener((observable, oldValue, newValue) -> {
if (controlButton.isSelected()) {
controlButton.fire();
}
});
// start and stop the input task.
controlButton.selectedProperty().addListener((observable, wasSelected, isSelected) -> {
if (isSelected) {
task = new InputTask(
(int) frequency.getValue(),
box.opacityProperty()
);
Thread inputThread = new Thread(task, "input-task");
inputThread.setDaemon(true);
inputThread.start();
controlButton.setText("Stop");
} else {
if (task != null) {
task.cancel();
}
controlButton.setText("Start");
}
});
// create the layout
VBox layout = new VBox(
10,
frequency,
new HBox(5, new Label("Frequency: " ), frequencyLabel, new Label("Hz"),
controlButton,
box,
new HBox(5, new Label("Brightness: " ), brightness)
);
layout.setPadding(new Insets(10));
// display the scene
stage.setScene(new Scene(layout));
stage.show();
}
// simulates accepting random input from an input feed at a given frequency.
class InputTask extends Task<Void> {
private final DoubleProperty changeableProperty;
private final long sleeptime;
final AtomicLong counter = new AtomicLong(-1);
final Random random = new Random(42);
public InputTask(int inputFrequency, DoubleProperty changeableProperty) {
this.changeableProperty = changeableProperty;
this.sleeptime = (long) ((1.0 / inputFrequency) * 1_000);
}
#Override
protected Void call() throws InterruptedException {
long count = 0 ;
while (!Thread.interrupted()) {
count++;
double newValue = random.nextDouble(); // input simulation
if (counter.getAndSet(count) == -1) {
Platform.runLater(() -> {
changeableProperty.setValue(newValue);
counter.getAndSet(-1);
});
}
Thread.sleep(sleeptime);
}
return null;
}
}
public static void main(String[] args) {
System.out.println(1.0/100*1000);
}
}
Related
I'm building a UI for a Simulator running in background. Since this Simulator may not hold for a long time, it of course has to be in a separate thread from the JavaFx Thread. I want to start, pause, resume and stop/terminate the Simulator when the corresponding button is clicked.
The service class that advances the simulator looks like this:
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class SimulatorService extends Service<Void> {
private Simulator simulator;
private long cycleLengthMS = 1000;
private final AtomicBoolean simulatorStopped = new AtomicBoolean(false);
public SimulatorService(Simulator simulator){
this.simulator = simulator;
}
#Override
protected Task<Void> createTask() {
return new Task<>() {
#Override
protected Void call() {
System.out.println("Requested start of Simulator");
int state;
do {
// advance
state = simulator.nextStep();
try {
TimeUnit.MILLISECONDS.sleep(cycleLengthMS);
if(simulatorStopped.get()){
super.cancel();
}
} catch (InterruptedException e) {
return null;
}
}
while (state == 0 && !simulatorStopped.get());
return null;
}
};
}
#Override
public void start(){
if(getState().equals(State.READY)){
simulatorStopped.set(false);
super.start();
}
else if(getState().equals(State.CANCELLED)){
simulatorStopped.set(false);
super.restart();
}
}
#Override
public boolean cancel(){
if(simulatorStopped.get()){
simulatorStopped.set(true);
return false;
} else{
simulatorStopped.set(true);
return true; //if value changed
}
}
}
The Simulator starts the Service if a button on the GUI is pressed:
import javafx.application.Platform;
import java.util.concurrent.atomic.AtomicInteger;
public class Simulator {
Model model;
private final SimulatorService simulatorService;
public Simulator(Model model){
this.model = model;
simulatorService = new SimulatorService(this);
}
public int nextStep(){
final AtomicInteger res = new AtomicInteger(0);
Platform.runLater(new Thread(()-> {
res.set(model.nextStep());
}));
return res.get();
}
public boolean stopSimulationService() throws IllegalStateException{
return simulatorService.cancel();
}
public void startSimulationService() throws IllegalStateException{
simulatorService.start();
}
}
Parts of the window are redrawn if a observed value in the model changes:
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class Model {
private final IntegerProperty observedValue = new SimpleIntegerProperty(0);
public int nextStep() {
observedValue.set(observedValue.get() + 1);
return observedValue.get() > 500000 ? 1 : 0;
}
public int getObservedValue() {
return observedValue.get();
}
public IntegerProperty observedValueProperty() {
return observedValue;
}
public void setObservedValue(int observedValue) {
this.observedValue.set(observedValue);
}
}
The redraw happens in another class:
import javafx.beans.value.ChangeListener;
public class ViewController {
private View view;
private Simulator simulator;
private Model model;
public ViewController(Simulator simulator) {
this.simulator = simulator;
this.view = new View();
setModel(simulator.model);
view.nextStep.setOnMouseClicked(click -> {
simulator.nextStep();
});
view.startSim.setOnMouseClicked(click -> {
simulator.startSimulationService();
});
view.stopSim.setOnMouseClicked(click ->{
simulator.stopSimulationService();
});
}
public View getView() {
return view;
}
private final ChangeListener<Number> observedValueListener = (observableValue, oldInt, newInt) -> {
view.updateView(newInt.intValue());
};
public void setModel(Model m) {
this.model = m;
m.observedValueProperty().addListener(observedValueListener);
}
}
The corresponding view:
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
public class View extends BorderPane {
Button nextStep = new Button("next step");
Button startSim = new Button("start");
Button stopSim = new Button("stop");
GridPane buttons = new GridPane();
Text num = new Text();
public View(){
buttons.add(nextStep,0,0);
buttons.add(startSim,0,1);
buttons.add(stopSim,0,2);
buttons.setAlignment(Pos.BOTTOM_LEFT);
setCenter(buttons);
setTop(num);
}
public void updateView(int num){
this.num.setText("" + num);
}
}
Main:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
ViewController c = new ViewController(new Simulator(new Model()));
Scene scene = new Scene(c.getView(),200,200);
stage.setScene(scene);
stage.show();
}
}
hello everyone, I am kind of new using android studio and I am working on my school project. I need to use RecycleVie wand I tried making it but without success.
I use a Object class caled Task whcih have 3 propeties to be shown on the layout but I don't know where is my mistake. the rows which shown as problems are in bold. I will be glad if anyone can help me!
my Object class:
public class Task {
private String material;
private String day;
private String month;
public Task (String material,String day,String month)
{
this.material = material;
this.day = day;
this.month = month;
}
public String getMaterial() {
return material;
}
public void setMaterial(String material) {
this.material = material;
}
public String getDay() {
return day;
}
public void setDay(String day) {
this.day = day;
}
public String getMonth() {
return month;
}
public void setMonth(String month) {
this.month = month;
}
}
the Adapter Code:
public class HomeRecyclerViewAdapter extends RecyclerView.Adapter<HomeRecyclerViewAdapter.ViewHolder> {
private Context mCtx;
private List<Task> tList;
// data is passed into the constructor
public HomeRecyclerViewAdapter(Context mCtx, List<Task> tList) {
this.mCtx = mCtx;
this.tList = tList;
}
// stores and recycles views as they are scrolled off screen
public class ViewHolder extends RecyclerView.ViewHolder {
TextView tvText, tvDateDay, tvDateMonth;
public ViewHolder(View itemView) {
super(itemView);
tvText = itemView.findViewById(R.id.tvText);
tvDateDay = itemView.findViewById(R.id.tvDateDay);
tvDateMonth = itemView.findViewById(R.id.tvDateMonth);
}
}
// inflates the row layout from xml when needed
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//inflating and returning our view holder
LayoutInflater inflater = LayoutInflater.from(mCtx);
View view = inflater.inflate(R.layout.home_recyclerview_row, null);
return new ViewHolder(view);
}
// binds the data to the TextView in each row
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Task task = tList.get(position);
**holder.tvText.setText(task.getMaterial());**
}
// allows clicks events to be caught
#Override
public int getItemCount() {
return tList.size();
}
}
and the main code:
public class HomeScreen_activity extends AppCompatActivity implements View.OnClickListener {
List<Task> tList;
RecyclerView homercy;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.home_screen_layout);
homercy = (RecyclerView) findViewById(R.id.homercy);
homercy.setHasFixedSize(true);
homercy.setLayoutManager(new LinearLayoutManager(this));
// set up the RecyclerView
RecyclerView recyclerView = findViewById(R.id.homercy);
tList = new ArrayList<Task>();
Task t1 = new Task("test","12","05");
tList.add(t1);
**HomeRecyclerViewAdapter adapter = new HomeRecyclerViewAdapter(this,tList);**
recyclerView.setAdapter(adapter);
}
Maybe in onCreateViewHolder(), you must do this:
// inflates the row layout from xml when needed
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.home_recyclerview_row, parent, false);
return new ViewHolder(view);
}
I'm using a TextField to display the path of a directory the user has opened in my application.
Currently, if the path can't fit inside the TextField, upon focusing away/clicking away from this control, it looks like as if the path has become truncated:
I want the behaviour of TextField set such that when I focus away from it, the path shown inside automatically scrolls to the right and the user is able to see the directory they've opened. I.e. something like this:
How can I achieve this? I've tried adapting the answer given from here
as follows in initialize() method in my FXML Controller class:
// Controller class fields
#FXML TextField txtMoisParentDirectory;
private String moisParentDirectory;
// ...
txtMoisParentDirectory.textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> observable, String oldStr, String newStr) {
moisParentDirectory = newStr;
txtMoisParentDirectory.selectPositionCaret(moisParentDirectory.length());
txtMoisParentDirectory.deselect();
}
});
However it doesn't work.
Your problem is based on two events, the length of the text entered and the loss of focus, so to solve it I used the properties textProperty() and focusedProperty() and here is the result :
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class Launcher extends Application{
private Pane root = new Pane();
private Scene scene;
private TextField tf = new TextField();
private TextField tft = new TextField();
private int location = 0;
#Override
public void start(Stage stage) throws Exception {
scrollChange();
tft.setLayoutX(300);
root.getChildren().addAll(tft,tf);
scene = new Scene(root,400,400);
stage.setScene(scene);
stage.show();
}
private void scrollChange(){
tf.textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
location = tf.getText().length();
}
});
tf.focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if(!newValue){
Platform.runLater( new Runnable() {
#Override
public void run() {
tf.positionCaret(location);
}
});
}
}
});
}
public static void main(String[] args) {
launch(args);
}
}
And concerning the Platform.runLater I added it following this answer Here I don't know why it does not work without it, good luck !
tf.textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
int location = tf.getText().length();
Platform.runLater(() -> {
tf.positionCaret(location);
});
}
});
this is also work
Since the other answers didn't work for me here is a solution that should do the trick:
private TextField txtField;
// Both ChangeListeners just call moveCaretToEnd(), we need them both because of differing data types we are listening to
private final ChangeListener<Number> caretChangeListener = (observable, oldValue, newValue) -> moveCaretToEnd();
private final ChangeListener<String> textChangeListener = (observable, oldValue, newValue) -> moveCaretToEnd();
// This method moves the caret to the end of the text
private void moveCaretToEnd() {
Platform.runLater(() -> {
txtField.deselect();
txtField.end();
});
}
public void initialize() {
// Immediatly add the listeners on initialization (or once you created the TextField if you are not using FXML)
txtField.caretPositionProperty().addListener(caretChangeListener);
txtField.textProperty().addListener(textChangeListener);
txtField.focusedProperty().addListener((observable, oldValue, isFocused) -> {
if (isFocused) {
// once the TextField has been focused remove the listeners to enable normal editing of the text
txtField.caretPositionProperty().removeListener(caretChangeListener);
txtField.textProperty().removeListener(textChangeListener);
} else {
// when the focus is lost apply the listeners again
moveCaretToEnd();
txtField.caretPositionProperty().addListener(caretChangeListener);
txtField.textProperty().addListener(textChangeListener);
}
});
}
I am trying to run an Infinite loop in my JavaFX app.
An infinite while loop is present in my code in the Kulta.java file.
This loop actually freezes my app.
While the same thing works when I port the app to normal javax.swing.
Now since java.lang.Thread doesn't work for javafx, I came accross javafx.concurrent.Task,
which is not working as intended. As one of the main features of multithreading, i.e. running an infinite loop in a GUI app, is not served properly, please help me with the solution.
This is my code:
Urania.java
import java.awt.Toolkit;
import java.awt.Dimension;
import javax.swing.SwingUtilities;
import static javax.swing.WindowConstants.EXIT_ON_CLOSE;
public class Urania {
public static final Dimension DIMENSION = Toolkit.getDefaultToolkit().getScreenSize();
public static void main(String[] args) {
SwingUtilities.invokeLater(
new Runnable() {
#Override
public void run() {
Kulta kulta = new Kulta();
kulta.setTitle("Abha K Pauri");
kulta.setSize(DIMENSION.width/2, DIMENSION.height/2);
kulta.setLocationRelativeTo(null);
kulta.setDefaultCloseOperation(EXIT_ON_CLOSE);
kulta.setVisible(true);
}
}
);
}
}
And here is my JFrame in which I have embedded my JavaFX app.
Kulta.java
import javax.swing.JFrame
import javafx.embed.swing.JFXPanel;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.control.Button;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.concurrent.Task;
public class Kulta extends JFrame {
private JFXPanel fxpanel;
private Scene scene;
private BorderPane borderpane;
private Button button;
public static final String INVOKE = "INVOKE";
public static final String INTERRUPT = "INTERRUPT";
public static final String[] COLORS = new String[]{"yellow", "pink", "green", "blue", "orange"};
public Kulta() {
fxpanel = new JFXPanel();
add(fxpanel);
Platform.runLater(
new Runnable() {
#Override
public void run() {
Kulta.this.setScene();
Kulta.this.setButton();
Kulta.this.setListener();
}
}
);
}
private void setScene() {
borderpane = new BorderPane();
scene = new Scene(borderpane);
fxpanel.setScene(scene);
}
private void setButton() {
button = new Button(INVOKE);
borderpane.setTop(button);
}
private void setListener() {
Event event = new Event();
button.setOnAction(event);
}
private class Event implements EventHandler<ActionEvent> {
#Override
public void handle(ActionEvent event) {
boolean flag = true;
Task<Void> onInvoke = new Task<Void>() {
#Override
public Void call() {
int count = 0;
flag = true;
button.setText(INTERRUPT);
/* This loop freezes the app. */
while(flag) {
borderpane.setStyle("-fx-color: "+COLORS[count]+";");
count++;
if(count == COLORS.length)
count = 0;
}
return null;
}
};
Task<Void> onInterrupt = new Task<Void>() {
#Override
public Void call() {
button.setText(INVOKE);
if(flag)
flag = false; // This will stop the onInvoke thread
return null;
}
};
Task<Void> change = new Task<Void>() {
#Override
public Void call() {
if(button.getText().equals(INVOKE))
onInvoke().run();
else if(button.getText().equals(INTERRUPT))
onInterrupt().run();
}
};
change.run();
}
}
}
How should I write the loop in order to not let the app freeze.
Any code, solution, link or any help in any form will help a lot.
Thanks in advance.
I want to develop a client app for website .
I want the app to reside in system tray when minimised.
I dont know how to accomplish this task .
Is their any example for this type of operation.
The key here is to set the implicit exit to false Platform.setImplicitExit(false);
Also is important to show and hide the stage in a new thread.
Platform.runLater(new Runnable() {
#Override
public void run() {
stage.show();
}
});
Platform.runLater(new Runnable() {
#Override
public void run() {
stage.hide();
}
});
Next, the whole code:
import java.awt.AWTException;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.URL;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javax.imageio.ImageIO;
/**
*
* #author alvaro
*/
public class TrayTest extends Application {
private boolean firstTime;
private TrayIcon trayIcon;
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
createTrayIcon(stage);
firstTime = true;
Platform.setImplicitExit(false);
Scene scene = new Scene(new Group(), 800, 600);
stage.setScene(scene);
stage.show();
}
public void createTrayIcon(final Stage stage) {
if (SystemTray.isSupported()) {
// get the SystemTray instance
SystemTray tray = SystemTray.getSystemTray();
// load an image
java.awt.Image image = null;
try {
URL url = new URL("http://www.digitalphotoartistry.com/rose1.jpg");
image = ImageIO.read(url);
} catch (IOException ex) {
System.out.println(ex);
}
stage.setOnCloseRequest(new EventHandler<WindowEvent>() {
#Override
public void handle(WindowEvent t) {
hide(stage);
}
});
// create a action listener to listen for default action executed on the tray icon
final ActionListener closeListener = new ActionListener() {
#Override
public void actionPerformed(java.awt.event.ActionEvent e) {
System.exit(0);
}
};
ActionListener showListener = new ActionListener() {
#Override
public void actionPerformed(java.awt.event.ActionEvent e) {
Platform.runLater(new Runnable() {
#Override
public void run() {
stage.show();
}
});
}
};
// create a popup menu
PopupMenu popup = new PopupMenu();
MenuItem showItem = new MenuItem("Show");
showItem.addActionListener(showListener);
popup.add(showItem);
MenuItem closeItem = new MenuItem("Close");
closeItem.addActionListener(closeListener);
popup.add(closeItem);
/// ... add other items
// construct a TrayIcon
trayIcon = new TrayIcon(image, "Title", popup);
// set the TrayIcon properties
trayIcon.addActionListener(showListener);
// ...
// add the tray image
try {
tray.add(trayIcon);
} catch (AWTException e) {
System.err.println(e);
}
// ...
}
}
public void showProgramIsMinimizedMsg() {
if (firstTime) {
trayIcon.displayMessage("Some message.",
"Some other message.",
TrayIcon.MessageType.INFO);
firstTime = false;
}
}
private void hide(final Stage stage) {
Platform.runLater(new Runnable() {
#Override
public void run() {
if (SystemTray.isSupported()) {
stage.hide();
showProgramIsMinimizedMsg();
} else {
System.exit(0);
}
}
});
}
}
As far as I know it will be possible in JFX 8. Right now the best solution is to embed your application into AWT and hide the AWT window itself.