Progress bar in Swing (Java) for command tools - java

I have several C/C++ command line tools that I'm wrapping with Java.Swing as GUI. The command line tools can take minutes to hours. Progress bar seems like a good idea to keep users sane. I'm also thinking it might be nice to wrap a GUI for the progress bar, instead of just using system out. But how?
I'm thinking the command line tools can write percents to stderr and I can somehow read it in java. Not exactly sure what the mechanics for this would be. I'm also not clear on asynchronous display (learned a bit about invokeLater() ). New to Java, and would appreciate general suggestions as well. Thanks.
--- update ---
Thanks everyone for your suggestions. Here's the resulting code.
private void redirectSystemStreams() {
OutputStream out_stderr = new OutputStream() {
#Override
public void write(final int b) throws IOException {
update(String.valueOf((char) b));
}
#Override
public void write(byte[] b, int off, int len) throws IOException {
update(new String(b, off, len));
}
#Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
};
System.setErr(new PrintStream(out_stderr, true));
}
private void update(final String inputText) {
int value = 20; //parse inputText; make sure your executable calls fflush(stderr) after each fprintf().
jProgressBar.setValue(value);
/* Also one can redirect to a textpane
SwingUtilities.invokeLater(new Runnable() {
public void run() {
//update jTextPane with inputText
}
});
*/
}

That's seems very fragile, better would be to communicate via sockets in a well established protocol or with some sort of RCP ( perhaps Google's protobuf ) or even webservices.
If you still insists you can launch a process in Java with ProcessBuilder that will give you a Process reference of which you can get the InputStream to read the standard output, but again, that seems very fragile to me.
I hope this helps.

For the progress bar part of your problem you can do something like the following. Note that this is just an example to illustrate the point.
Basically, a thread is created to do the work. Presumably this Runner thread will be interacting with your C/C++ code to get its progress. It then calls update on the Progress Bars Dialog class.
import java.awt.BorderLayout;
import java.awt.Dimension;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
public class Main {
private int value;
private Progress pbar;
public static void main(String args[]) {
new Main();
}
public Main() {
pbar = new Progress();
Thread t = new Thread(new Runner());
t.start();
}
class Progress extends JDialog {
JProgressBar pb;
JLabel label;
public Progress() {
super((JFrame) null, "Task In Progress");
pb = new JProgressBar(0, 100);
pb.setPreferredSize(new Dimension(175, 20));
pb.setString("Working");
pb.setStringPainted(true);
pb.setValue(0);
label = new JLabel("Progress: ");
JPanel panel = new JPanel();
panel.add(label);
panel.add(pb);
add(panel, BorderLayout.CENTER);
pack();
setVisible(true);
}
public void update(){
pb.setValue(value);
if(value >= 100){
this.setVisible(false);
this.dispose();
}
}
}
class Runner implements Runnable {
public void run() {
for (int i = 0; i <= 100; i++) {
value++;
pbar.update();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
}
}
}
}

// Create a window
JFrame frame = new JFrame("Progress");
// Creates a progress bar and add it to the window
JProgressBar prog = new JProgressBar();
frame.add(prog);
// Run C/C++ application
try {
Process p = Runtime.getRuntime().exec(new String[]{"filename","arg1","arg2","..."});
// Get InputStream
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
// Update the progress when recieving output from C/C++
new java.util.Timer().schedule(new TimerTask(){
public void run(){
String str = "";
while ((str=br.readLine()!=null) {
prog.setValue(new Integer(str)); // Set Value of Progress Bar
prog.setString(str+"%"); // Set Value to display (in text) on Progress Bar
}
}
},0,100); // Check every 100 milliseconds
// Fit the window to its contents and display it
frame.pack();
frame.setVisible(true);
} catch (Exception e) {
System.out.println("Failed To Launch Program or Failed To Get Input Stream");
}

Related

JAVA - JFrame not looking proper when called from action listener

I have a problem with one of my frames not looking as it should, when it is called upon the press of a button.
The frame looks as if it was rendered improperly, the label text in it is shortened, however when i move the same line of code outside the action listener, it works as it should.
I have a sort of main menu, with two buttons, only the Generate Menu works at the moment, it looks like this:
https://i.imgur.com/k1Ne5v9.png
The code for the action listener:
runMenuButt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Generate Menu pressed");
mF.dispose();
MenuGenerator.generateTheMenu();
}
});
The result looks wrong: https://i.imgur.com/n86y4CD.png
The frame is also unresponsive, clikcing X does not, while it should close the frame and the application.
However changing the code to:
runMenuButt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Generate Menu pressed");
//mF.dispose();
}
});
MenuGenerator.generateTheMenu();
Produces correct look: https://i.imgur.com/TFbkmAO.png
The code for the "Main menu"
public static void openMainMenu() {
Font menuFont = new Font("Courier",Font.BOLD,16);
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
mF.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mF.setSize(465,230);
mF.setLocation(dim.width/2-mF.getSize().width/2, dim.height/2-mF.getSize().height/2);
mF.getContentPane().setBackground(Color.WHITE);
Color blueSteel = new Color(70,107,176);
JPanel p = new JPanel();
p.setSize(600,50);
p.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
p.setLocation((mF.getWidth() - p.getWidth()) /2, 20);
p.setBackground(blueSteel);
JLabel l = new JLabel("Welcome to the menu GENERATORRRR");
l.setFont(menuFont);
l.setForeground(Color.WHITE);
p.add(l, gbc);
JButton runMenuButt = new JButton("Generate Menu");
runMenuButt.setLocation(20 , 90);
JButton manageRecipButt = new JButton("Manage Recipients");
manageRecipButt.setLocation(240 , 90);
menuUtilities.formatButton(runMenuButt);
menuUtilities.formatButton(manageRecipButt);
mF.setResizable(false);
mF.setLayout(null);
mF.add(runMenuButt);
mF.add(manageRecipButt);
mF.add(p);
mF.setVisible(true);
runMenuButt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Generate Menu pressed");
//mF.dispose();
}
});
MenuGenerator.generateTheMenu();
manageRecipButt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(null, "Not supported yet", "Function not yet available",JOptionPane.INFORMATION_MESSAGE);
}
});
//System.out.println(mF.getContentPane().getSize());
}
And the status bar:
public class StatusBar {
private static JLabel statusLabel= new JLabel("Starting");
private static JFrame statusFrame = new JFrame("Generation Status");
public static void createStatusBar() {
Font menuFont = new Font(Font.MONOSPACED,Font.BOLD,20);
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
statusFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
statusFrame.setSize(700,100);
JPanel p = new JPanel();
p.setPreferredSize(new Dimension(100,100));
statusLabel.setFont(menuFont);
p.add(statusLabel);
statusFrame.add(p,BorderLayout.CENTER);
statusFrame.setLocation(dim.width/2-statusFrame.getSize().width/2, dim.height/2-statusFrame.getSize().height/2);
statusFrame.setVisible(true);
}
public static void setStatusBar(String statusText) {
statusLabel.setText(statusText);
statusLabel.paintImmediately(statusLabel.getVisibleRect());
statusLabel.revalidate();
}
public static void closeStatusBar(){
statusFrame.dispose();
}
}
I create the bar with this line:
StatusBar.createStatusBar();
Why does the status bar not render properly when the MenuGenerator.generateTheMenu(); is called from the action listener?
Here is minimal code that reproduces this behavior for anyone who would like to test it: It also uses class for the StatusBar, which is already posted.
public class MinimalClass {
private static JFrame mF = new JFrame("Main Menu");
public static void main(String[] args) {
openMainMenu();
}
public static void openMainMenu() {
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
mF.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mF.setSize(465,230);
mF.setLocation(dim.width/2-mF.getSize().width/2, dim.height/2-mF.getSize().height/2);
mF.getContentPane().setBackground(Color.WHITE);
JButton runMenuButt = new JButton("Generate Menu");
runMenuButt.setLocation(20 , 90);
runMenuButt.setSize(200 , 85);
mF.setResizable(false);
mF.setLayout(null);
mF.add(runMenuButt);
mF.setVisible(true);
runMenuButt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Generate Menu pressed");
mF.dispose();
generateTheMenu();
}
});
}
public static void generateTheMenu() {
System.setProperty("sun.java2d.cmm", "sun.java2d.cmm.kcms.KcmsServiceProvider");
String rawMenuOutput = "";
try {
rawMenuOutput= getMenuInJavaNow();
} catch (Exception e){
System.out.println("Something went terribly wrong");
}
System.out.println(rawMenuOutput);
}
public static String getMenuInJavaNow() throws IOException {
String rawMenuOutput = "Restaurant Menu" ;
rawMenuOutput = rawMenuOutput + "Test line";
String []menuOtpArr = new String [3];
try {
StatusBar.createStatusBar();
TimeUnit.SECONDS.sleep(2);
StatusBar.setStatusBar("Test1");
TimeUnit.SECONDS.sleep(2);
menuOtpArr[0]="Test line";
StatusBar.setStatusBar("Test2");
TimeUnit.SECONDS.sleep(2);
menuOtpArr[1]="Test line";
StatusBar.setStatusBar("Test3");
TimeUnit.SECONDS.sleep(2);
menuOtpArr[2]="Test line";
StatusBar.setStatusBar("Test4");
TimeUnit.SECONDS.sleep(2);
StatusBar.closeStatusBar();
} catch (Exception e) {
}
for (int i=0;i < menuOtpArr.length;i++) {
rawMenuOutput = rawMenuOutput + "\n\n" +menuOtpArr[i];
}
return rawMenuOutput;
}
}
Thank you for your time
statusLabel.paintImmediately(statusLabel.getVisibleRect()); seems to masking a larger issue.
The problem is, Swing is single threaded (and NOT thread safe). This means that when you call TimeUnit.SECONDS.sleep(2); from within getMenuInJavaNow, which is called by generateTheMenu, which is called by the ActionListener, it's been called within the context of the Event Dispatching Thread.
This is putting the EDT to sleep, meaning that it isn't processing layout or paint requests (properly)
Start by having a read of Concurrency in Swing for more details
Now, you have a larger issue, how to solve it. For the answer to that question, we require a lot more context then is currently available.
The getMenuInJavaNow seems to be returning some values, to what end I'm not sure.
"A" solution, would be to use a SwingWorker (see Worker Threads and SwingWorker for more details). It provides the ability to execute long running tasks in the background, but also provides the means for sync updates back to the UI, for example...
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingWorker;
public class MinimalClass {
private static JFrame mF = new JFrame("Main Menu");
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
openMainMenu();
}
});
}
public static void openMainMenu() {
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
mF.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mF.setLocationRelativeTo(null);
mF.getContentPane().setBackground(Color.WHITE);
JButton runMenuButt = new JButton("Generate Menu");
runMenuButt.setMargin(new Insets(25, 25, 25, 25));
JPanel buttons = new JPanel(new GridLayout(0, 2));
buttons.add(runMenuButt);
mF.add(buttons);
mF.pack();
mF.setVisible(true);
runMenuButt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Generate Menu pressed");
mF.dispose();
generateTheMenu();
}
});
}
public static void generateTheMenu() {
System.setProperty("sun.java2d.cmm", "sun.java2d.cmm.kcms.KcmsServiceProvider");
StatusBar.createStatusBar();
SwingWorker<String, String> worker = new SwingWorker<String, String>() {
#Override
protected String doInBackground() throws Exception {
String rawMenuOutput = "Restaurant Menu";
rawMenuOutput = rawMenuOutput + "Test line";
String[] menuOtpArr = new String[3];
try {
TimeUnit.SECONDS.sleep(2);
publish("1234567890123456789012345678901234567890");
TimeUnit.SECONDS.sleep(2);
publish("This is a test");
TimeUnit.SECONDS.sleep(2);
publish("More testing");
TimeUnit.SECONDS.sleep(2);
publish("Still testing");
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
}
for (int i = 0; i < menuOtpArr.length; i++) {
rawMenuOutput = rawMenuOutput + "\n\n" + menuOtpArr[i];
}
return rawMenuOutput;
}
#Override
protected void done() {
StatusBar.closeStatusBar();
}
#Override
protected void process(List<String> chunks) {
StatusBar.setStatusBar(chunks.get(chunks.size() - 1));
}
};
worker.addPropertyChangeListener(new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (worker.getState() == SwingWorker.StateValue.DONE) {
try {
String result = worker.get();
System.out.println(result);
StatusBar.closeStatusBar();
} catch (InterruptedException | ExecutionException ex) {
ex.printStackTrace();
}
}
}
});
worker.execute();
}
public static class StatusBar {
private static JLabel statusLabel = new JLabel("Starting");
private static JFrame statusFrame = new JFrame("Generation Status");
public static void createStatusBar() {
Font menuFont = new Font(Font.MONOSPACED, Font.BOLD, 20);
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
statusFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
statusFrame.setSize(700, 100);
JPanel p = new JPanel();
p.setBackground(Color.RED);
// p.setPreferredSize(new Dimension(100, 100));
statusLabel.setFont(menuFont);
p.add(statusLabel);
statusFrame.add(p, BorderLayout.CENTER);
statusFrame.setLocationRelativeTo(null);
statusFrame.setVisible(true);
}
public static void setStatusBar(String statusText) {
statusLabel.setText(statusText);
}
public static void closeStatusBar() {
statusFrame.dispose();
}
}
}
Observations...
static is not your friend, especially in cases like this. You really, really, really need to learn to live without it.
setLayout(null) is not doing you any favours, especially in the long run. Take the time to go through Laying Out Components Within a Container and start making proper use of layout managers, they might seem "complicated", but they will save you from a lot of hair loss
Avoid using setPreferred/Minimum/MaximumSize where ever possible, you are robing the component of the ability to provide useful rendering hints which may change across platforms and rendering pipelines
Just a quick follow up question, what is the difference between done and addPropertyListener ? Is there any? Isnt it redundant to use both?
The example here is pretty basic, for me I've used done to handle what the SwingWorker "knows" needs to be done, it doesn't however, know what is to be done with the result.
I've used the PropertyChangeListener to deal with that instead - the point - it's an example.
And I also noticed, that I dont need to actually publish, as calling StatusBar.setStatusBar(""); works as well. Is it necessary to use publish?
In a word YES. Swing is NOT thread safe, calling StatusBar.setStatusBar("") directly can lead to some weird and unexpected results. publish pushes the call into the Event Dispatching Thread, making it safe to update the UI from within.
I have the code for generating the String I want to set as the StatusBar Title in another class, not in the generateTheMenu, therefore it is more convenient for me to simply call .setStatusBar. The not minimal code I have is actually something like this
This is where things like interfaces come in really handy. You "string" generating class "could" either return the resulting text OR you could pass a reference to a interface implementation which is used to "display" it. This way, your SwingWorker could act as a consumer for the String and pass it through the publish method.
There are a number of really important concepts to understand.
You want to decouple your code. This makes it easier to change certain parts of the code without affecting the other parts
You want to be able to "code to interface, not implementation". This goes hand in hand with the first comment. Basically, you want to "hide" the implementation details as much as possible - lots of different reasons for it, but it helps keep your code lean, helps make the follow more understandable and stops one part of the code from accessing another it really has no responsibility to do so (is the string generation really responsible for updating the status bar? IMHO - not really)
There is also a swagger of design patterns available to make solving issues easier. I've already mentioned the concept of "produce/consumer", but this is just one
The "Event Dispatching Thread"
The Event Dispatch Thread
Java Event-Dispatching Thread explanation
Swing threading and the event-dispatch thread

Synchronized copying display with jProgressBar

I wanted to monitor the progress of my file getting copied from source to destination. I have used synchronized keyword but somehow it not working as i expect it to be, my logic might be wrong. I will be glad if you help me out.
Here is my Code.
public class Download extends javax.swing.JFrame {
int val=0;
private Timer t;
private ActionListener a;
/* Creates new form Download */
public Download() {
initComponents();
jProgressBar1.setValue(val);
a = new ActionListener() {
#Override
public void actionPerformed(ActionEvent ae) {
if (jProgressBar1.getValue() < val)
jProgressBar1.setValue(jProgressBar1.getValue()+1);
else
t.stop();
}
};
}
public synchronized void copy(String source,String url)
{
try {
val+=25;
t=new Timer(200,a);
t.start();
FileInputStream fs = new FileInputStream(source);
FileOutputStream os = new FileOutputStream(url);
int b;
while ((b = fs.read()) != -1) {
os.write(b);
}
os.close();
fs.close();
} catch (Exception E) {
E.printStackTrace();
}
}
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
String url = null;
int returnValue = chooser.showDialog(null, "Select");
if (returnValue == JFileChooser.APPROVE_OPTION) {
url = chooser.getSelectedFile().getPath();
} else {
dispose();
}
JOptionPane.showMessageDialog(this,"Wait for Completion");
if(CB1.isSelected()==true)
{
File f = new File(getClass().getResource("/PCycle/Ele.pdf").getFile());
String source= f.getAbsolutePath();
copy(source,(url+"\\"+CB1.getText()+".pdf"));
}
if(CB2.isSelected()==true)
{
File f = new File(getClass().getResource("/PCycle/Mech.pdf").getFile());
String source= f.getAbsolutePath();
copy(source,(url+"\\"+CB2.getText()+".pdf"));
}
if(CB3.isSelected()==true)
{
File f = new File(getClass().getResource("/PCycle/Phy.pdf").getFile());
String source= f.getAbsolutePath();
copy(source,(url+"\\"+CB3.getText()+".pdf"));
}
if(CB4.isSelected()==true)
{
File f = new File(getClass().getResource("/PCycle/Civil.pdf").getFile());
String source= f.getAbsolutePath();
copy(source,(url+"\\"+CB4.getText()+".pdf"));
}
JOptionPane.showMessageDialog(this,"Completed");
try {
jProgressBar1.setValue(100);
Thread.sleep(3000);
} catch (InterruptedException ex) {
Logger.getLogger(Download.class.getName()).log(Level.SEVERE, null, ex);
}
System.exit(0);
}
}
Here I tried to implement a logic in such a that, whenever we call "copy" method it will copy the file from one location to another and before that it should run the timer method by which the progress on the jProgressBar is displayed. But unfortunately even after using synchronized it is not displaying the progress for each file.
The problem is you are blocking Swing's Event Dispatching Thread (EDT).
Swing does all drawing when the EDT is not busy responding to events. In this case jButton1ActionPerformed is not returning until all files have been copied. So although a Timer is started during each copy() call, the timers never get a chance to expire, because jButton1ActionPerformed has never returned.
In this case, you want to use a SwingWorker to copy the files in a background thread.
When you want to start copying the files:
start the timer in the main thread
create and start the SwingWorker.
open a model dialog to block further user actions (or otherwise disable the UI)
As the timer expires, your progress bar will advance, and be drawn.
When the SwingWorker is done() (which is executed on the EDT),
stop the timer
dismiss the dialog (or re-enable the UI)
Note: Do not create or access any UI items, or create/start/stop timers, from the background worker thread. These actions must only be performed on the EDT.
Rough example, showing disabling UI element, starting SwingWorker, publishing from the worker to show progress (which file is being download), enabling UI when the worker finishes.
File copy is faked using a 3 seconds sleep.
package progress;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.Timer;
#SuppressWarnings("serial")
public class Download extends JFrame {
public static void main(String[] args) {
SwingUtilities.invokeLater(Download::new);
}
private final JButton downloadBtn = new JButton("Start Download");
private final JProgressBar progressBar = new JProgressBar();
private final Timer timer = new Timer(200, this::timerTick);
Download() {
super("Download Example");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(400, 300);
setLocationByPlatform(true);
downloadBtn.addActionListener(this::startDownload);
add(downloadBtn, BorderLayout.PAGE_START);
progressBar.setStringPainted(true);
add(progressBar, BorderLayout.PAGE_END);
setVisible(true);
}
private void startDownload(ActionEvent evt) {
downloadBtn.setEnabled(false);
timer.start();
DownloadWorker worker = new DownloadWorker("File1", "FileB", "AnotherFile");
worker.execute();
}
private void timerTick(ActionEvent evt) {
progressBar.setValue(progressBar.getValue()+2);
}
private class DownloadWorker extends SwingWorker<Void, String> {
private final String[] files;
DownloadWorker(String ...files) {
this.files = files;
progressBar.setValue(0);
}
#Override
protected Void doInBackground() throws Exception {
for(String file : files) {
publish(file);
// Copy the file
Thread.sleep(3000); // Pretend copy takes a few seconds
}
return null;
}
#Override
protected void process(List<String> chunks) {
String file = chunks.get(chunks.size()-1); // Just last published filename
progressBar.setString("Downloading "+file + " ...");
}
#Override
protected void done() {
progressBar.setString("Complete");
progressBar.setValue(100);
timer.stop();
downloadBtn.setEnabled(true); // Re-enable UI
}
}
}

Why is my download progress bar firing the same event multiple times?

I'm practicing Swing and I coded a download progress bar to download an image when the user presses the "Start download" button. The download works. The problem is that in my Terminal, I can see that the same event (propertyChange) is being fired multiple times, the number of times increasing with every subsequent download. I've debugged my code with checkpoints, but I'm still not sure why this is happening.
To be more specific, in my Terminal I'm seeing something like
...100% completed
...100% completed
...100% completed
...100% completed
...100% completed
...100% completed
...100% completed
when I expect to see "...100% completed" only once. The number of "...100% completed" that is displayed accumulates with every download. I'm not sure if this is affecting the performance of my download, but I'm wondering why it's happening.
ProgressBar.java:
package download_progress_bar;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ProgressBar {
private JFrame frame;
private JPanel gui;
private JButton button;
private JProgressBar progressBar;
public ProgressBar() {
customizeFrame();
createMainPanel();
createProgressBar();
createButton();
addComponentsToFrame();
frame.setVisible(true);
}
private void customizeFrame() {
// Set the look and feel to the cross-platform look and feel
try {
UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
} catch (Exception e) {
System.err.println("Unsupported look and feel.");
e.printStackTrace();
}
frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
}
private void createMainPanel() {
gui = new JPanel();
gui.setLayout(new BorderLayout());
}
private void createProgressBar() {
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(true); // renders a progress string
}
private void createButton() {
button = new JButton("Start download");
}
private void addComponentsToFrame() {
gui.add(progressBar, BorderLayout.CENTER);
gui.add(button, BorderLayout.SOUTH);
frame.add(gui);
frame.pack();
}
// Add passed ActionListener to the button
void addButtonListener(ActionListener listener) {
button.addActionListener(listener);
}
// Get progress bar
public JProgressBar getProgressBar() {
return progressBar;
}
// Enable or disable button
public void turnOnButton(boolean flip) {
button.setEnabled(flip);
}
}
Downloader.java:
package download_progress_bar;
import java.net.*;
import java.io.*;
import java.beans.*;
public class Downloader {
private URL url;
private int percentCompleted;
private PropertyChangeSupport pcs;
public Downloader() {
pcs = new PropertyChangeSupport(this);
}
// Set URL object
public void setURL(String src) throws MalformedURLException {
url = new URL(src);
}
// Add passed PropertyChangeListener to pcs
public void addListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void download() throws IOException {
// Open connection on URL object
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// Check response code (always do this first)
int responseCode = connection.getResponseCode();
System.out.println("response code: " + responseCode);
if (responseCode == HttpURLConnection.HTTP_OK) {
// Open input stream from connection
BufferedInputStream in = new BufferedInputStream(connection.getInputStream());
// Open output stream for file writing
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("cat.jpg"));
int totalBytesRead = 0;
//int percentCompleted = 0;
int i = -1;
while ((i = in.read()) != -1) {
out.write(i);
totalBytesRead++;
int old = percentCompleted;
percentCompleted = (int)(((double)totalBytesRead / (double)connection.getContentLength()) * 100.0);
pcs.firePropertyChange("downloading", old, percentCompleted);
System.out.println(percentCompleted); // makes download a bit slower, comment out for speed
}
// Close streams
out.close();
in.close();
}
}
}
Controller.java:
package download_progress_bar;
import java.util.concurrent.ExecutionException;
import javax.swing.*;
import java.awt.event.*;
import java.util.List;
import java.net.*;
import java.io.*;
import java.beans.*;
public class Controller {
private ProgressBar view;
private Downloader model;
private JProgressBar progressBar;
private SwingWorker<Void, Integer> worker;
public Controller(ProgressBar theView, Downloader theModel) {
view = theView;
model = theModel;
progressBar = view.getProgressBar();
// Add button listener to the "Start Download" button
view.addButtonListener(new ButtonListener());
}
class ButtonListener implements ActionListener {
/**
* Invoked when user clicks the button.
*/
public void actionPerformed(ActionEvent evt) {
view.turnOnButton(false);
progressBar.setIndeterminate(true);
// NOTE: Instances of javax.swing.SwingWorker are not reusable,
// so we create new instances as needed
worker = new Worker();
worker.addPropertyChangeListener(new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals("progress")) {
progressBar.setIndeterminate(false);
progressBar.setValue(worker.getProgress());
}
}
});
worker.execute();
}
}
class Worker extends SwingWorker<Void, Integer> implements PropertyChangeListener {
/*
* Download task. Executed in worker thread.
*/
#Override
protected Void doInBackground() throws MalformedURLException {
model.addListener(this);
try {
String src = "https://lh3.googleusercontent.com/l6JAkhvfxbP61_FWN92j4ulDMXJNH3HT1DR6xrE7MtwW-2AxpZl_WLnBzTpWhCuYkbHihgBQ=s640-h400-e365";
model.setURL(src);
model.download();
} catch (IOException ex) {
System.out.println(ex);
this.cancel(true);
}
return null;
}
/*
* Executed in event dispatching thread
*/
#Override
protected void done() {
try {
if (!isCancelled()) {
get(); // throws an exception if doInBackground throws one
System.out.println("File has been downloaded successfully!");
}
} catch (InterruptedException x) {
x.printStackTrace();
System.out.println("There was an error in downloading the file.");
} catch (ExecutionException x) {
x.printStackTrace();
System.out.println("There was an error in downloading the file.");
}
view.turnOnButton(true);
}
/**
* Invoked in the background thread of Downloader.
*/
#Override
public void propertyChange(PropertyChangeEvent evt) {
this.setProgress((int) evt.getNewValue());
System.out.println("..." + this.getProgress() + "% completed");
}
}
}
Main.java:
package download_progress_bar;
import javax.swing.SwingUtilities;
/**
* Runs the download progress bar application.
*/
public class Main {
public static void main(String[] args) {
// Schedule a job for the event-dispatching thread:
// creating and showing this application's GUI.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// Create view
ProgressBar view = new ProgressBar();
// NOTE: Should model/controller be created outside invokeLater?
// Create model
Downloader model = new Downloader();
// Create controller
Controller controller = new Controller(view, model);
}
});
}
}
EDIT: I've updated my code to reflect the suggested changes. But even after making the changes, the problem persists. I am still seeing multiple invocations of "...100% completed", the number of invocations increasing with every subsequent download. For example, I run the application and press the download button for the first time, I will see
...100% completed
I press the download button again. I see
...100% completed
...100% completed
I press the download button again...
...100% completed
...100% completed
...100% completed
and so on. Why is this happening?
It's possible that, because of the way the percentage is calculated that it will report 100% when there is still some more work to be completed
During my testing I observed...
//...
98
...
99
99
...
100
So a lot of the values were repeated before the code completed.
I noted some issues/oddities in your download code, mostly the fact that you completely ignore the percentCompleted property, so I changed it to something more like...
public void download() throws IOException {
// Open connection on URL object
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// Check response code (always do this first)
int responseCode = connection.getResponseCode();
System.out.println("response code: " + responseCode);
if (responseCode == HttpURLConnection.HTTP_OK) {
// Open input stream from connection
BufferedInputStream in = new BufferedInputStream(connection.getInputStream());
// Open output stream for file writing
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("cat.jpg"));
int totalBytesRead = 0;
//int percentCompleted = 0;
int i = -1;
while ((i = in.read()) != -1) {
out.write(i);
totalBytesRead++;
int old = percentCompleted;
percentCompleted = (int) (((double) totalBytesRead / (double) connection.getContentLength()) * 100.0);
pcs.firePropertyChange("downloading", old, percentCompleted);
System.out.println(percentCompleted); // makes download a bit slower, comment out for speed
}
// Close streams
out.close();
in.close();
}
}
For me, I'd change the code slightly, instead of doing...
#Override
protected void process(List<Integer> chunks) {
int percentCompleted = chunks.get(chunks.size() - 1); // only interested in the last value reported each time
progressBar.setValue(percentCompleted);
if (percentCompleted > 0) {
progressBar.setIndeterminate(false);
progressBar.setString(null);
}
System.out.println("..." + percentCompleted + "% completed");
}
/**
* Invoked when a progress property of "downloading" is received.
*/
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals("downloading")) {
publish((Integer) evt.getNewValue());
}
}
You should take advantage of SwingWorkers inbuilt progress support, for example...
/**
* Invoked when a progress property of "downloading" is received.
*/
#Override
public void propertyChange(PropertyChangeEvent evt) {
setProgress((int)evt.getNewValue());
}
This will mean you will need to attached a PropertyChangeListener to the SwingWorker
/**
* Invoked when user clicks the button.
*/
public void actionPerformed(ActionEvent evt) {
view.turnOnButton(false);
progressBar.setIndeterminate(true);
// NOTE: Instances of javax.swing.SwingWorker are not reusable,
// so we create new instances as needed
worker = new Worker();
worker.addPropertyChangeListener(new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if ("progress".equals(evt.getPropertyName())) {
progressBar.setIndeterminate(false);
progressBar.setValue(worker.getProgress());
}
}
});
worker.execute();
}
The side effect to this is, you know have a means to also be notified when the SwingWorker's state changes to check to see when it is DONE
Updated
Okay, after going over the code, again, I can see that you're adding a new PropertyChangeListener to model EVERY TIME you execute the SwingWorker
/*
* Download task. Executed in worker thread.
*/
#Override
protected Void doInBackground() throws MalformedURLException, InterruptedException {
model.addListener(this); // Add another listener...
try {
String src = "https://lh3.googleusercontent.com/l6JAkhvfxbP61_FWN92j4ulDMXJNH3HT1DR6xrE7MtwW-2AxpZl_WLnBzTpWhCuYkbHihgBQ=s640-h400-e365";
model.setURL(src);
model.download();
} catch (IOException ex) {
System.out.println(ex);
this.cancel(true);
}
return null;
}
Because the model is an instance field of Controller, this is having an accumulative effect.
One solution might be to just add the Downloader as the listener to the model, but that would require you to ensure that any updates you perform to the UI are synced properly.
A better, general, solution would be to add support to remove the listener once the worker completes
public class Downloader {
//...
public void removeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
And then in the SwingWorkers done method, remove the listener...
/*
* Executed in event dispatching thread
*/
#Override
protected void done() {
model.removeListener(this);
As shown here and here, SwingWorker maintains two bound properties: state and progress. Invoking setProgress() ensures that "PropertyChangeListeners are notified asynchronously on the Event Dispatch Thread." Simply add a PropertyChangeListener to your progress bar and call setProgress() in your implementation of doInBackground(), or a method that it calls such as download(). Conveniently, "For performance purposes, all these invocations are coalesced into one invocation with the last invocation argument only."

Java LostFocus and InputVerifier, moving in reverse-tab-order

I have a GUI application that uses an InputVerifier to check the content of text fields before yielding the focus. This is all very normal. Yesterday, however, discovered a problem - it seems to be a bug, but I cannot find any mention of it anywhere. Before I report this as a bug, I thought I would ask: am I missing something obvious here?
Situation:
A set of text fields with InputVerifiers.
Listeners for FocusLost and FocusGained on all controls, so I can see what is happening.
A separate thread uses a DefaultKeyboardFocusManager to report (every 2 seconds) which control has the focus.
I place invalid data in a JTextField in the middle of the form, and try to leave the control.
If I try to move away from this control using the mouse, or using the tab-key, I cannot. The FocusLost event does not fire and the control properly retains the focus.
However, if I try to move away from the control in reverse tab order, using Shift-Tab, sometimes the FocusLost event fires. If this happens, the separate thread reports that no control has the focus, i.e., getFocusOwner() returns null.
Edit: below is a small sample program that shows the problem. The problem has nothing to do with the extra thread - the thread is just there to make the problem more obvious. If there is a race-condition, it is somewhere in Swing.
To see the problem, go to the second text box and empty it. The control should retain the focus, and does so unless you leave it by pressing shift-tab. Unlike the full application, the error seems to occur here 100% of the time. This is true both under OpenJDK 6 and under Oracle Java 7.
This is almost too obvious to be a bug, plus it happens in multiple Java environments. Hence, my suspicion that I am missing something obvious. Anyone?
public class FocusBugDemo extends JFrame {
static JTextField txtOne = new JTextField("Ignore this control");
static JTextField txtTwo = new JTextField("Delete this text, then press shift-tab");
static JLabel lblFocus = new JLabel("");
static KeyboardFocusManager kfm = new DefaultKeyboardFocusManager();
public static void main(String[] args) {
new FocusBugDemo();
Thread t = new Thread() {
#Override
public void run() {
while(true) {
Component c = kfm.getFocusOwner();
String focusInfo = "elsewhere";
if (c == null) { focusInfo = "null";
} else if (c == txtOne) { focusInfo = "txtOne";
} else if (c == txtTwo) { focusInfo = "txtTwo";
}
lblFocus.setText(System.currentTimeMillis() + " - Focus owner " + focusInfo);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
};
t.start();
}
private FocusBugDemo() {
super("Focus bug demo");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setPreferredSize(new Dimension(300,100));
setLayout(new GridLayout(3,1));
NotEmpty validator = new NotEmpty();
txtOne.setInputVerifier(validator);
txtTwo.setInputVerifier(validator);
add(txtOne);
add(txtTwo);
add(lblFocus);
pack();
setVisible(true);
}
private class NotEmpty extends InputVerifier {
#Override
public boolean verify(JComponent input) {
JTextField txtField = (JTextField) input;
return (txtField.getText().length() > 0);
}
}
}
Now reported to Oracle as bug 7167871.
Using your sscce, I am unable to reproduce the effect you describe on Mac OS X, Java 6, which supports #CatalinaIsland's observation. In particular, focus never leaves an empty text field using either tab or shift-tab; focus becomes null only when the frame is deactivated.
I see two threads accessing multiple fields with no synchronization at all. At a minimum, you should use EventQueue.invokeLater() in t to update the GUI, as described in Concurrency in Swing and show below.
The broader question is this: What focus problem are you trying to solve using t?
import java.awt.Component;
import java.awt.DefaultKeyboardFocusManager;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.KeyboardFocusManager;
import javax.swing.InputVerifier;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
public class FocusDemo {
private static final JTextField txtOne =
new JTextField("Ignore this control");
private static final JTextField txtTwo =
new JTextField("Delete this text, then press shift-tab");
private static final JLabel lblFocus = new JLabel("");
public static void main(String[] args) {
new FocusDemo();
Thread t = new Thread() {
#Override
public void run() {
while (true) {
EventQueue.invokeLater(new Runnable() {
KeyboardFocusManager kfm =
new DefaultKeyboardFocusManager();
#Override
public void run() {
Component c = kfm.getFocusOwner();
String focusInfo = "elsewhere";
if (c == null) {
focusInfo = "null";
} else if (c == txtOne) {
focusInfo = "txtOne";
} else if (c == txtTwo) {
focusInfo = "txtTwo";
}
lblFocus.setText(System.currentTimeMillis()
+ " - Focus owner " + focusInfo);
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
}
}
};
t.start();
}
private FocusDemo() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame f = new JFrame("Focus bug demo");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setPreferredSize(new Dimension(300, 100));
f.setLayout(new GridLayout(3, 1));
NotEmpty validator = new NotEmpty();
txtOne.setInputVerifier(validator);
txtTwo.setInputVerifier(validator);
f.add(txtOne);
f.add(txtTwo);
f.add(lblFocus);
f.pack();
f.setVisible(true);
}
});
}
private class NotEmpty extends InputVerifier {
#Override
public boolean verify(JComponent input) {
JTextField txtField = (JTextField) input;
return (txtField.getText().length() > 0);
}
}
}

Process output only becomes available after the process has finished

I have a Runnable that reads Console output from an externally called exe (see below) and writes it to both a log file and a JTextArea.
But my Runnable doesn't show the Console output in the JTextArea until the exe completely finishes. How do I get it to print Console output as it happens?
Short Concise Code Example below:
//Main
import java.awt.*;
import java.io.IOException;
import javax.swing.*;
public class Example extends JFrame {
private static final long serialVersionUID = 1L;
public static int maxX, maxY;
public static JTextArea ta = new JTextArea(20, 60);//For LOG display window
public static void main(String args[] ) throws IOException
{
new Example();
}
public Example() {
this.setTitle("Example");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//MAIN Panel
final JPanel main = new JPanel();
JButton RunButton = button.run(main);
main.add(RunButton);
Container container = getContentPane();
container.add(main);
this.pack();
this.setVisible(true);
}
}
//Button Action Listener
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.*;
public class button {
public static JButton run( final JPanel parent ) {
JButton RunButton = new JButton();
RunButton.setText("Start!");
RunButton.addActionListener(
new ActionListener()
{
public void actionPerformed( ActionEvent event)
{
try
{
//Set up LOG Display
JDialog dialog = new JDialog((JFrame)null, "Working...");
JPanel temp_panel = new JPanel();
temp_panel.add(new JScrollPane(Example.ta));
dialog.getContentPane().add(temp_panel);
dialog.pack();
dialog.setVisible(true);
//Build the Command
ArrayList<String> command = new ArrayList<String>();
command.add("ping");
command.add("127.0.0.1");
//Start the process
Process p = new ProcessBuilder(command).start();
//Starts LOG display capture in separate thread
SwingUtilities.invokeLater(new execute(p));
//Wait for call to complete
p.waitFor();
}
catch(Exception err)
{
JOptionPane.showMessageDialog( parent, "Error Executing Run!", "Warning", JOptionPane.ERROR_MESSAGE );
}
}//end ActionPerformed
});
return RunButton;
}
}
//Runnable
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class execute implements Runnable {
String line;
Process p;
public execute ( Process process ) {
p = process;
}
public void run() {
try {
//Read Process Stream Output and write to LOG file
BufferedReader is = new BufferedReader( new InputStreamReader(p.getInputStream()));
while ( (line = is.readLine()) != null ) {
Example.ta.append(line + "\n");
}
System.out.flush();
} catch(Exception ex) { ex.printStackTrace(); }
}
}
Maybe it's because you don't respect Swing's threading policy. All accesses to swing components must be done in the event dispatch thread. Your runnable should thus use SwingUtilities.invokeLater to update the text area in the EDT, rather than in your separate thread.
EDIT : as alf mentions in his comment: JTextArea.append is thread-safe, so it's not absolutely needed here. I would still do it, though, because if the append to a text area was replaced or complemented by any other Swing interaction, it wouldn't be thread-safe anymore.
It could also be that the external process doesn't send any newline character, which makes readLine block until one is found or the end of communication is reached.
Just to help the miner - below's a complete minimalistic (left out everything not absolutely necessary) example that indeed works in my context: each line appears in the textArea as read. It's basically using the SwingWorker as suggested by Justin and re-arranged thingies a bit for clarity.
public class ProcessExample {
public static class ProcessWorker extends SwingWorker<Void, String> {
private JTextArea ta;
private List<String> process;
public ProcessWorker(List<String> command, JTextArea ta) {
this.process = command;
this.ta = ta;
}
#Override
protected Void doInBackground() throws Exception {
Process p = new ProcessBuilder(process).start();
// Read Process Stream Output and write to LOG file
BufferedReader is = new BufferedReader(new InputStreamReader(
p.getInputStream()));
String line;
while ((line = is.readLine()) != null) {
publish(line);
}
is.close();
return null;
}
#Override
protected void process(List<String> chunks) {
for (String string : chunks) {
ta.append(string + "\n");
}
}
}
private void startProcess(JTextArea ta) {
ArrayList<String> command = new ArrayList<String>();
command.add("ping");
command.add("127.0.0.1");
new ProcessWorker(command, ta).execute();
}
private JComponent getContent() {
JPanel main = new JPanel(new BorderLayout());
final JTextArea ta = new JTextArea(20, 60);
main.add(new JScrollPane(ta));
Action action = new AbstractAction("Start!") {
#Override
public void actionPerformed(ActionEvent e) {
startProcess(ta);
}
};
main.add(new JButton(action), BorderLayout.SOUTH);
return main;
}
public static void main(String args[]) throws IOException {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame("Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new ProcessExample().getContent());
frame.pack();
frame.setVisible(true);
}
});
}
}
You could try the same logic with a SwingWorker instead. You can extend this class, instead of implementing runnable. It could take your Text area as a paramater, and you can publish the data, without having to deal with the SwingUtils.invokeLater, which is guiltily easier...
Try:
public class execute extends javax.swing.SwingWorker {
String line;
Process p;
JTextArea jta;
File f = new File( properties.prop.getProperty( "LOG_FILE_DIR" ) + "\\PartGen.log");
public execute ( Process process , JTextArea jta ) {
p = process;
this.jta = jta;
}
//implements a method in the swingworker
public void doInBackground() throws Exception {
//Read Process Stream Output and write to LOG file
BufferedReader is = new BufferedReader( new InputStreamReader(p.getInputStream()));
while ( (line = is.readLine()) != null ) {
osfile.writeline(line, f);
publish(new String(line + "\n"));
}
System.out.flush();
return null;
}
//This will happen on the UI Thread.
public void process(List lines){
for(Object o : lines){
jta.append((String)o);
}
}
public void done(){
try{
get();
//You will get here if everything was OK. So show a popup or something to signal done.
}catch(Exception ex){
//this is where your IO Exception will surface, should you have one.
}
}
}
Also, in your calling code, which I assume is in your ui somewhere:
Process p = new ProcessBuilder(command).start();
execute ex = new execute( p , yourTextArea);
ex.execute();
I didnt attempt to compile this, so you may have to check against the API, but hopefully it will give you a gist of what to do.
The problem was not that the thread wasn't capturing the data, it was the JTextArea just not refreshing. repaint(), revalidate(), and updateUI() did not refresh the JTextArea, but the following did:
Example.ta.update(Example.ta.getGraphics());
The problem in this case is with the waitFor:
p.waitFor();
This causes the Button Action Listener to wait on that point until the process is completed.

Categories