StdAudio class makes my JFrame not display - java

I'm trying to write a program that plays musical chords. I'd like to add a window that shows a progress bar displaying the time that the chords play for and how much they have completed. To play the chords, I've been using a slightly modified version of the StdAudio class. So far, I have the following code to be run when I ask a chord to play.
public static void playNotes(double[] frequencies, double duration, double amplitude)
{
PlayAudioGUI g = new PlayAudioGUI(duration);
g.run();
amp = amplitude;
ArrayList<double[]> chord = new ArrayList<double[]>();
for(double freq : frequencies) {
double[] note = StdAudio.tone(freq, duration);
chord.add(note);
}
double[] chordCombined = new double[chord.get(0).length];
for (int i = 0; i < chordCombined.length; i++) {
for (double[] note : chord) {
chordCombined[i] += note[i];
}
chordCombined[i] /= chord.size();
}
StdAudio.play(chordCombined);
}
I've never attempted multithreading before, so I don't know what I'm doing wrong. When I run the code, It shows an empty window while it plays the chord, then afterwards displays the window properly. I'd like for it to display the window at the same time as playing the audio.
Here is my code for the window's class.
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JProgressBar;
import javax.swing.Timer;
public class PlayAudioGUI implements Runnable {
private JFrame window;
private JProgressBar prog;
private double duration;
private Timer t;
class TimerListener implements ActionListener {
// This runs every few milliseconds, depending on the delay set below
public void actionPerformed(ActionEvent event) {
prog.setValue(prog.getValue() + 1);
// Stop the timer and hide the window when the progress bar
// completes
if (prog.getValue() == prog.getMaximum()) {
t.stop();
window.setVisible(false);
}
}
}
public PlayAudioGUI(double duration) {
this.window = new JFrame("Playing audio...");
this.duration = duration;
}
#Override
public void run() {
// Setting up gridbag layout. I will add more components later.
Container pane = this.window.getContentPane();
pane.setLayout(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.gridx = 0;
c.gridy = 0;
c.insets = new Insets(30, 30, 30, 30);
// Display the approximate duration
String clippedDuration;
if (Double.toString(duration).length() > 5) {
clippedDuration = Double.toString(duration).substring(0, 4);
} else {
clippedDuration = Double.toString(duration);
}
String message = "Playing audio for " + clippedDuration + " seconds";
pane.add(new JLabel(message), c);
// Make a progressbar
c.gridy = 1;
this.prog = new JProgressBar();
this.prog.setMinimum(0);
this.prog.setMaximum(250);
pane.add(this.prog, c);
// More window management stuff
this.window.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
this.window.pack();
this.window.setVisible(true);
// Set up the timer
ActionListener listener = new TimerListener();
final int DELAY = (int) (4 * this.duration); // This works, I did the
// math :)
t = new Timer(DELAY, listener);
t.start();
}
}
Thanks for your help.

Suggestions:
The new dependent dialog window should be just that, a dialog such as a JDialog, not a new JFrame which is creating a whole separate application.
You know that you should be doing your sound creation in a background thread, and your blank screen is being caused by just this problem, and yet I see no thread creation in your code -- why?
Myself, I'd not use a Swing Timer, and poll data, but rather do all within a SwingWorker, within the SwingWorker's doInBackground method I'd update its progress state, and I'd add a PropertyChangeListener to the SwingWorker and monitor this state.
As an aside, you will almost never want to create a Runnable class and then call its run() method. If you're creating the Runnable to allow it to run in a background thread, then you'd likely place it into a Thread and then call start() on the Thread. Since your code above should run on the Swing event thread, then if it is not being called from this thread, it should be queued on to it via SwingUtilities.invokeLater(myRunnable);
I'm not sure how you can get a progress value from your StdAudio library. If there's a way, then use it to set the SwingWorker's progress state via its setProgress(...) method. If not, then you could guess, I suppose or you may be better off using an indeterminate progress bar. I believe JProgressBar has a method called setIndeterminate(true) that would work for this.

Related

How to append text to a JTextArea using data from a separate thread

I have a Scheduler class with a thread in charge of creating Process objects and I want to take the Process object as they are created and display the useful information to a JTextArea. However, when the Scheduler class creates the Process the JTextArea remains blank. How can i notify or update the JTextArea everytime a new Process is created? There is also an ArrayBlockingQueue that stores every Process until the CPU class executes it.
I have tried setting up Event listeners to try to capture when a process has been created.
public class Main {
public static void main(String[] args) {
Scheduler scheduler = new Scheduler();
scheduler.createProcesses();
SwingUtilities.invokeLater(new Runnable(){
public void run(){
JFrame frame = new MainFrame();
frame.setVisible(true);
frame.setSize(500,500);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
});
}
}
Main creates the Scheduler object and then calls createProcess(). Then it calls the SwingUtilities runnable thread.
import java.awt.BorderLayout;
import java.awt.Container;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.lang.Math;
import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
public class Scheduler {
private static final int MAX_QUEUE_SIZE = 1001;
private CPU cpu;
private MainFrame frame;
ArrayBlockingQueue<Process> readyQueue;
int time = 0;
int pid = 1000;
public Scheduler()
{
readyQueue = new ArrayBlockingQueue<Process>(MAX_QUEUE_SIZE, true);
this.cpu = new CPU(this);
frame = new MainFrame();
}//end of constructor
public void createProcesses() //populate ready queue
{
new Thread(new Runnable() {
#Override
public void run() {
// Create 1002 processes
Scheduler.this.cpu.start();
while(pid < 2002) {
Random rand = new Random();
int meanRunTime = 10;
int sd = 2;
// Random number following a Normal distribution
int runTime = (int) Math.round(rand.nextGaussian()) * sd + meanRunTime;
int meanDelayTime = 5;
sd = 1;
int arrivalDelayTime = (int) Math.round(rand.nextGaussian()) * sd + meanDelayTime;
//System.out.println(Scheduler.this.time);
try {
// Wait for process to arrive
Thread.sleep(arrivalDelayTime);
Scheduler.this.time += arrivalDelayTime;
} catch (InterruptedException e) {
System.out.println("Queue waiting for arival interrupted");
}
Process p = new Process(Scheduler.this.pid, Process.WAITING, (time), runTime); //constructs Process
System.out.println(p.toString());
frame.setProcess(p); //This is where I am attempting to pass the process to the frame however this does not seem to work
Scheduler.this.pid++;
try {
Scheduler.this.readyQueue.put(p);
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
}).start();
}//end of create process
This is the scheduler class. Basically when it creates Process p i need it to tell the GUI about the newly created process so that it can be added to processTextArea
import java.awt.BorderLayout;
import java.awt.Container;
import java.util.concurrent.ArrayBlockingQueue;
import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.JTextField;
public final class MainFrame extends JFrame{
private Process process;
public MainFrame(){
//Layout of Frame
setLayout(new BorderLayout());
//Creation of Components that will go into the Frame
JTextArea processTextArea = new JTextArea("Awaiting Completed Processes");
while(process != null){
processTextArea.setText(process.toString());
process = null;
}
//Adds Compnents to the content frame
Container c = getContentPane();
c.add(processTextArea, BorderLayout.EAST);
}
public void setProcess(Process p){
this.process = p;
}
The MainFrame is the GUI class. At the moment the setProcess call made in the Scheduler class does give the MainFrame class a process object but only once. How can this be updated everytime a new Process is created?
I Wish to have the GUI fill up the processTextArea as new Process's are being created. What happens at the moment is The GUI frame pops up however nothing is being added to the processTextArea.
This is the scheduler class. Basically when it creates Process p i need it to tell the GUI about the newly created process so that it can be added to processTextArea
I think the MainFrame object in Main and the MainFrame in Scheduler are two different reference? You should solve this first.
I Wish to have the GUI fill up the processTextArea as new Process's are being created. What happens at the moment is The GUI frame pops up however nothing is being added to the processTextArea.
Extract processTextArea to become the member of MainFrame, and to create a method like:
public void onProcessComplete(Process P) {
synchronized (processTextArea) {
processTextArea.append(process.toString());
}
}
Whenever a Process is completed, invoke mainFrame.onProcessComplete(this). This should meet your needs.

How could I add a refreshing timer to a JLabel using swing components

I am creating a Maze game and want a timer to be displayed on the gameStage.
I have tried using java.util but it requires me to get rid of my swing timer.How could i add a refreshing timer to game
This code is used to make the game frame which contains the button pane and the gameStage.
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
/**
* This class Holds the game pane that has the moving player. It also contains
* the GamePane
*
* #author 602052004
*
*/
public class GamePane extends JPanel implements ActionListener, KeyListener {// *change
// GamePane
// to
// GamePane
// This is were the game screen is made and the player is created.
static final long serialVersionUID = 1L;
JLabel player = new JLabel();
JLabel finish = new JLabel();
JFrame gameFrame;
int playerSpeed = 4;
int FPS = 40;
// This array holds my JLabels for the walls.I used it so that i can have a
// for loop with an index for the labels.
JLabel[] walls = new JLabel[3];
{
walls[0] = new JLabel();
walls[1] = new JLabel();
walls[2] = new JLabel();
}
private final Set<Integer> keys = new HashSet<>();
// The keys set holds the keys being pressed
public static void main(String[] args) {
// Open the GUI window
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
// Create a new object and
// run its go() method
new GamePane().go();
}
});
}
GamePane() {
// Run the parent class constructor
super();
// Allow the panel to get focus
setFocusable(true);
// Don't let keys change the focus
}
/**
* This method creates the gameFrame and sets its layout to a cardlayout.It
* then proceeds the set up the GameFrame.The gameFrame contains the button
* pane and the gameStage
*
* The walls are an array and are used to create an index which is then used
* for the collisions.I set up the walls location here
*/
protected void go() {
setLayout(new CardLayout());
// Setup the window
gameFrame = new JFrame();
// Add this panel to the window
gameFrame.setLayout(new CardLayout());
gameFrame.add(this, "main");
gameFrame.setContentPane(this);
// Set's the window properties
gameFrame.setTitle("main");
gameFrame.setSize(800, 600);
gameFrame.setResizable(false);
gameFrame.setLocationRelativeTo(null);
gameFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
gameFrame.setVisible(true);
gameFrame.add(new ButtonPane(gameFrame), "buttons");
// Creates the new JPanel that will hold the game.
JPanel gamestage = new JPanel();
gamestage.setBackground(Color.darkGray);
gameFrame.add(gamestage, "game");
gamestage.setLayout(null);
// *Move the setup of the player and the timer under the walls
// Get a sample of collisions going so that i can do it over the weekend
// Setup the movable box
player.setBounds(25, 25, 20, 20);
player.setVisible(true);
player.setBackground(Color.red);
// Opaque makes the background visible
player.setOpaque(true);
// Setup the key listener
addKeyListener(this);
// Null layout allows moving objects!!!
gamestage.add(player);
// Set the timer
Timer tm = new Timer(1000 / FPS, this);
tm.start();
walls[0].setBounds(10, 15, 10, 480);// left height
walls[0].setVisible(true);
walls[0].setBackground(Color.white);
walls[0].setOpaque(true);
gamestage.add(walls[0]);
walls[1].setBounds(10, 10, 490, 10);// top width
walls[1].setVisible(true);
walls[1].setBackground(Color.white);
walls[1].setOpaque(true);
gamestage.add(walls[1]);
// wall3.setBounds(x, y, width, height);
walls[2].setBounds(10, 100, 100, 10);
walls[2].setVisible(true);
walls[2].setBackground(Color.white);
walls[2].setOpaque(true);
gamestage.add(walls[2]);
finish.setBounds(30, 455, 20, 20); // *make the game change to the main
// screen when finished
// Add a timer
finish.setVisible(true);
finish.setBackground(Color.LIGHT_GRAY);
finish.setOpaque(true);
gamestage.add(finish);
}
/**
* Check if two JLabel objects are touching
*
* #param a
* The first JLabel
* #param b
* The second JLabel
* #return true if the JLabels are touching
*/
public boolean areColliding(JLabel a, JLabel b) {
return a.getBounds().intersects(b.getBounds());
}
/**
* this method makes the player move. It takes the players speed and
* subtracts or adds the player speed to the current position of the player.
* It also figures out were the player is at currently aswell.
*
* #param arg0
*/
#Override
public void actionPerformed(ActionEvent arg0) {
// Move up if W is pressed
if (keys.contains(KeyEvent.VK_W)) {
player.setLocation(player.getX(), player.getY() - playerSpeed);
}
// Move right if D is pressed
if (keys.contains(KeyEvent.VK_D)) {
player.setLocation(player.getX() + playerSpeed, player.getY());
}
// Move down if S is pressed
if (keys.contains(KeyEvent.VK_S)) {
player.setLocation(player.getX(), player.getY() + playerSpeed);
}
// Move left if A is pressed
if (keys.contains(KeyEvent.VK_A)) {
player.setLocation(player.getX() - playerSpeed, player.getY());
}
for (int i = 0; i < walls.length; i++) {
// I created a for loop instead
// of a do loop because the for
// loop would have been a lot
// simpler to manage
if (areColliding(walls[i], player)) { // Reposition the target
int newX = (int) (25);
int newY = (int) (25);
player.setLocation(newX, newY);
}
}
if (areColliding(finish, player)) {
// Reposition the target
int newX = 25;
int newY = 25;
player.setLocation(newX, newY);
CardLayout layout = (CardLayout) gameFrame.getContentPane()
.getLayout();
layout.show(gameFrame.getContentPane(), "buttons");
}
}
#Override
public void keyPressed(KeyEvent e) {
// Add the key to the list
// of pressed keys
if (!keys.contains(e.getKeyCode())) {
keys.add(e.getKeyCode());
}
}
#Override
public void keyReleased(KeyEvent e) {
// Remove the key from the
// list of pressed keys
keys.remove((Integer) e.getKeyCode());
}
#Override
public void keyTyped(KeyEvent e) {
}
}
this code shows the game pane when the buttob is pressed
/**
* This pane contains the button and sets up the button pane
*/
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class ButtonPane extends JPanel {
private JButton startBTN;// Calls the JButton
JFrame game;
public ButtonPane(JFrame g) {
game = g;
setLayout(new GridBagLayout());
setBackground(Color.gray);// Sets the menu stages color blue
startBTN = new JButton("Game");// Creates a new button
add(startBTN);// Adds the button on the startStage
startBTN.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (game.getContentPane().getLayout() instanceof CardLayout) {
CardLayout layout = (CardLayout) getParent().getLayout();
layout.show(game.getContentPane(), "game");
}
}
});
}
}
Okay, so you have a Timer already, which is ticking away at 1000/FPS times a second, cool. All you really need is away to calculate the difference between two points in time, which, amazingly, is very simple.
Start by defining a "start time"
private Instant startTime;
This will be null till you need it. When you want to start the timer, use startTime = Instant.now();
When startTime != null, you want to calculate the difference between it and now...
Duration runningTime = Duration.between(startTime, Instant.now());
This now tells you how long the timer has been running for.
Next, we need to make some decisions, like what to do when the timer runs out, but for that, we actually need to know how lone the timer should run for...
private Duration timeOutDuration = Duration.ofSeconds(5);
This just sets up a timeout of 5 seconds, you can use what ever range you want.
This then allows us to calculate the remaining time of the timer...
Duration timeRemainig = timeOutDuration.minus(runningTime);
and then to make decisions about what to do...
if (timeRemainig.isNegative() || timeRemainig.isZero()) {
// Time has run out...
// startTime = null; // stop the timer
} else {
// Update the UI
}
The date/time API introduced in Java 8 is incredibly powerful and flexible (and a lot of fun, when you get your head around it)
A solution might start looking something like...
private Duration timeOutDuration = Duration.ofSeconds(5);
private Instant startTime; // Set this when you're ready to start the timer
#Override
public void actionPerformed(ActionEvent arg0) {
if (startTime != null) {
Duration runningTime = Duration.between(startTime, Instant.now());
Duration timeRemainig = timeOutDuration.minus(runningTime);
if (timeRemainig.isNegative() || timeRemainig.isZero()) {
// Time has run out...
// startTime = null; // stop the timer
} else {
// Update the UI
}
}
Formatting a Duration for output generally looks something like...
long hours = timeRemainig.toHours();
long mins = timeRemainig.minusHours(hours).toMinutes();
// Or if you're lucky enough to be using Java 9+
//String formatted = String.format("%dhrs %02dmins", duration.toHours(), duration.toMinutesPart());
String formatted = String.format("%dhrs %02dmins", hours, mins);
or simular, depending on how you want it formatted
Why use this approach instead of some "counter"
Simple, it's (super) accurate. Timer only guarantees a "at least" interval, that is, it will delay no less then the value apply, this means that it's possible to introduce a "drag" over time, where a counter would fall out of sync. Sure, over a short period of time, it's probably not a big deal, but since there is a (super easy) better way to do it, why not make use of it.
The solution is also super flexible, applied to a broad spectrum of similar issues. I use the above concept as part of time based animations, which generally produce a far superior, overall, result.

Sprite animation timing java swing

I tried making a sprite animation in JAVA using Swing. The code below is an array of ImageIcon which I'm going to iterate through in order to show the several different images (only 2 for testing purposes).
But I don't know how to properly time each iteration. I mean, when I compile the code the label I'm working on, only displays the last image of the array (obviously), but I want it to display the first one for some ms and the other one after.
I made some research and saw some suggestions regarding Time class, but I don't really know how to implement it in these circunstances. I also tried to use sleep, which works fine in C++, but only came up with thread.sleep which doesn't work in this case.
The animation is supposed to simulate a playing card being turned around (for the Monopoly game).
Can anyone give me some input on the matter?
import javax.swing.ImageIcon;
import javax.swing.JLabel;
public class Sprite2 {
private ImageIcon[] sprites;
private ImageIcon a = new ImageIcon("C:/Users/Guilherme/Desktop/G/FEUP/2º Ano/2nd semester/Eclipse repos/Sprite/src/images/ball.jpg");
private ImageIcon b = new ImageIcon("C:/Users/Guilherme/Desktop/G/FEUP/2º Ano/2nd semester/Eclipse repos/Sprite/src/images/transferir.jpg");
public Sprite2() {
sprites = new ImageIcon[] {a, b};
}
public void render(JLabel lbl) {
for (int i = 0; i < sprites.length; i++) {
lbl.setIcon(sprites[i]);
//sleep(1000); - Looking for a similar Java function which is able to delay each iteration and make it look like a gif
}
}
You can use Swing timer, see How to Use Swing Timers for more details.
You should not use sleep() on Event Dispatch Thread as it will stop the thread from processing painting and other UI related events and the UI will become frozen. For more details see The Event Dispatch Thread tutorial.
I figured it out using timer.
For some reason, the first frame wouldn't show up in the first animation, hence sprites[0] and [1] are the same image.
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Sprite extends JPanel implements ActionListener{
private ImageIcon[] sprites = new ImageIcon[4];
private Timer time;
private int delay = 500, currentFrame = 0;
private ImageIcon a0 = new ImageIcon("C:/Users/Guilherme/Desktop/G/FEUP/2º Ano/2nd semester/Eclipse repos/Sprite/src/images/ball.jpg");
private ImageIcon a1 = new ImageIcon("C:/Users/Guilherme/Desktop/G/FEUP/2º Ano/2nd semester/Eclipse repos/Sprite/src/images/transferir.jpg");
private ImageIcon a2 = new ImageIcon("C:/Users/Guilherme/Desktop/G/FEUP/2º Ano/2nd semester/Eclipse repos/Sprite/src/images/smurf_sprite.png");
public Sprite() {
sprites[0] = a0;
sprites[1] = a0;
sprites[2] = a1;
sprites[3] = a2;
time = new Timer(delay, this);
time.start();
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
sprites[currentFrame].paintIcon(this, g, 0, 0);
if (currentFrame == sprites.length-1) {
time.stop();
}
else currentFrame++;
}
public void actionPerformed(ActionEvent e) {
repaint();
}
public static void main(String[] arg) {
JFrame f = new JFrame();
Sprite s = new Sprite();
f.add(s);
f.setVisible(true);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setSize(500,400);
}
}

Keeping constant track of time in java

So I want to make a program that constantly keeps track of time while other functions of the code can run. For my code specifically, I have 4 JButtons and 4 JLabels. Once I click a JButton, I want the corresponding JLabel to update its text every couple of seconds (let's say 5 seconds). But while the code running, I want to be able to select a different button at any given time to update its corresponding JLabel and stop the one before. I've tried a few things, but every time I run into two problems:
The JLabel doesn't update every 5 seconds.
I can't click a different JButton while the another JButton is clicked.
They both have to do with the fact that my code goes through an infinite loop once a JButton is clicked, but I'm not sure how I can go around doing this.
I'm new to coding so my knowledge of the terminology is quite limited. Any help will be appreciated.
Here's the code for what happens when the button is clicked. (I changed the code a few times here and there to try to fix on my own, but couldn't so currently it just gives me a stackoverflow error.) :
package maingame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import javax.swing.JButton;
import javax.swing.JLabel;
public class StatButtonListener implements ActionListener
{
private JButton _button;
private ArrayList<JLabel> _jlList;
private static String _clicked;
public StatButtonListener(JButton button, ArrayList<JLabel> jlList)
{
_button = button;
_jlList = jlList;
}
#Override
public void actionPerformed(ActionEvent arg0)
{
_clicked = _button.getText();
Long value = null;
JLabel valueLabel = null;
// to stop an infinite loop
for(int i=0; i < 8; i++)
{
if(_button.getText().equals(_jlList.get(i).getText()))
{
valueLabel = _jlList.get(i+1);
value = Long.parseLong(valueLabel.getText());
}
}
double startTime = System.currentTimeMillis()/1000;
updateStat(value, valueLabel, startTime);
}
public void updateStat(Long value, JLabel valueLabel, double startTime)
{
if(!(value == null || valueLabel == null))
{
if(_clicked.equals(_button.getText()))
{
double endTime=System.currentTimeMillis()/1000;
if((endTime-startTime) >= 1)
{
startTime = System.currentTimeMillis()/1000;
value = value + 3;
valueLabel.setText(value.toString());
}
updateStat(value, valueLabel, startTime);
}
}
}
}
You can do this using the aforementioned Swing-Timers:
http://docs.oracle.com/javase/tutorial/uiswing/misc/timer.html
This invokes actionPerformed() once its done. You can then update whatever you want to update and restart() it.
Or you can do this using a new Thread:
http://de.wikibooks.org/wiki/Java_Standard:_Threads
This runs completely on its own and does not block the main program - and might need a handle of the objects you want to change.

Dynamically Update Tooltip Currently Displayed

I'm trying to got a tooltip which displays the current progress of a task. So I want that the tooltip text change while the tooltip is displayed. But, when I call setToolTipText() the displayed text remains the same until I exit the mouse from the tooltip component and enter again. And call setToolTipText(null) before doesn't change anything.
Indeed it does not update itself, even when resetting the tooltip to null between calls.
So far, the only trick I found was to simulate a mouse-move event and forward it on the TooltipManager. It makes him think that the mouse has moved and that the tooltip must be relocated. Not pretty, but quite efficient.
Have a look at this demo code which displays a progress in % from 0 to 100:
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.ToolTipManager;
public class TestTooltips {
protected static void initUI() {
JFrame frame = new JFrame("test");
final JLabel label = new JLabel("Label text");
frame.add(label);
frame.pack();
frame.setVisible(true);
Timer t = new Timer(1000, new ActionListener() {
int progress = 0;
#Override
public void actionPerformed(ActionEvent e) {
if (progress > 100) {
progress = 0;
}
label.setToolTipText("Progress: " + progress + " %");
Point locationOnScreen = MouseInfo.getPointerInfo().getLocation();
Point locationOnComponent = new Point(locationOnScreen);
SwingUtilities.convertPointFromScreen(locationOnComponent, label);
if (label.contains(locationOnComponent)) {
ToolTipManager.sharedInstance().mouseMoved(
new MouseEvent(label, -1, System.currentTimeMillis(), 0, locationOnComponent.x, locationOnComponent.y,
locationOnScreen.x, locationOnScreen.y, 0, false, 0));
}
progress++;
}
});
t.start();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
initUI();
}
});
}
}
Here's a simplified version of Guillaume Polet's answer which is self-contained in a single method. This code assumes one has called component.setToolTip("..."); previously. This code does not show how to periodically update the tooltip to show the progress.
public static void showToolTip(JComponent component)
{
ToolTipManager manager;
MouseEvent event;
Point point;
String message;
JComponent component;
long time;
manager = ToolTipManager.sharedInstance();
time = System.currentTimeMillis() - manager.getInitialDelay() + 1; // So that the tooltip will trigger immediately
point = component.getLocationOnScreen();
event = new MouseEvent(component, -1, time, 0, 0, 0, point.x, point.y, 1, false, 0);
ToolTipManager.
sharedInstance().
mouseMoved(event);
}

Categories