0

I'm trying to implement a progress bar that determines the (approximate) percentage of completion from another thread in Java. Right now I have the two threads running at the same time but i'm unsure of how to link to the other thread. I need it to start at 0 and by the time the other friend is finished, i want the progress bar to say 100. This is what I have so far. I've been searching almost all day for a solution but can't seem to find anything. Suggestions?

import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.filechooser.FileSystemView;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.*;
import java.io.File;

public class ProgressBarTest extends JFrame {
    JButton browse, search;
    JTextField directory;
    JProgressBar progressBar;
    File file;
    JList<File> results;
    JScrollPane scrollPane;

    // Launch the app
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    new ProgressBarTest();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    // Create the app
    private ProgressBarTest() {
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setTitle("We are testing the JProgressBar");

        browse = new JButton("Browse");
        browse.setActionCommand("browse");
        browse.addActionListener(new ButtonListener());

        search = new JButton("Search");
        search.setActionCommand("search");
        search.addActionListener(new ButtonListener());

        directory = new JTextField(20);
        directory.setEnabled(false);

        file = new File(System.getProperty("user.home"));
        results = new JList<File>(file.listFiles());
        results.setCellRenderer(new MyCellRenderer());
        results.setLayoutOrientation(JList.HORIZONTAL_WRAP);
        results.setVisibleRowCount(-1);

        scrollPane = new JScrollPane(results, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.setPreferredSize(new Dimension(408, 100));

        progressBar = new JProgressBar(0,100);
        progressBar.setValue(0);
        progressBar.setStringPainted(true);

        JPanel panel1 = new JPanel();
        panel1.add(browse);
        panel1.add(directory);
        panel1.add(search);

        JPanel panel2 = new JPanel();
        panel2.add(scrollPane);

        JPanel panel3 = new JPanel();
        panel3.add(progressBar);

        this.add(panel1, BorderLayout.PAGE_START);
        this.add(panel2, BorderLayout.CENTER);
        this.add(panel3, BorderLayout.PAGE_END);

        this.pack();
        this.setLocationRelativeTo(null);
        this.setVisible(true);
    }

    private class MyCellRenderer extends DefaultListCellRenderer {
        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
            Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
            JLabel l = (JLabel)c;
            File f = (File)value;
            l.setText(f.getName());
            l.setPreferredSize(new Dimension(130, 20));
            l.setIcon(FileSystemView.getFileSystemView().getSystemIcon(f));
            l.setBorder(new EmptyBorder(3,3,3,3));

            return l;
        }
    }

    private class ButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent event) {
            if(event.getActionCommand().equals("browse")) {
                JFileChooser chooser = new JFileChooser(System.getProperty("user.home"));
                chooser.setDialogTitle("Search what?");
                chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
                chooser.showOpenDialog(null);

                directory.setText(chooser.getSelectedFile().getPath());
            } else if (event.getActionCommand().equals("search")) {
                FileThread ft = new FileThread();
                ft.start();

                ProgressBarThread pbt = new ProgressBarThread();
                pbt.start();

            }
        }
    }

    private class FileThread extends Thread {
        public void run() {
            file = new File(directory.getText().trim());
            results.setListData(file.listFiles());
        }
    }

    private class ProgressBarThread extends Thread {
        public void run() {
            for (int i = 0; i <= 100; i++) {
                progressBar.setValue(i);
                progressBar.setString(Integer.toString(i));
                try {
                    Thread.sleep(50);
                } catch(InterruptedException err) {
                    err.printStackTrace();
                }
            }
        }
    }
}

I know I'll have to take out the for loop with something that determines the percentage / time taken for the first thread. Just not sure how this is done.

Vince
  • 2,596
  • 11
  • 43
  • 76
  • 4
    Link threads? I think that you're confused on what you need to do. You don't want to link threads but rather simply communicate progress from one bit of code, run on a background thread, to another bit of code that is running on the Swing event thread. – Hovercraft Full Of Eels Mar 05 '17 at 02:44
  • 2
    Your code also is making Swing mutation changes from a background thread, something that you should never do. Please read the Swing threading tutorial to see how to correctly handle this: [Lesson: Concurrency in Swing](http://docs.oracle.com/javase/tutorial/uiswing/concurrency/). – Hovercraft Full Of Eels Mar 05 '17 at 02:51
  • @HovercraftFullOfEels I'm new to Java language and Threading all together. I'm just going off what I saw on youtube videos and some examples. – Vince Mar 05 '17 at 02:57
  • If your goal is to make the progress bar progress as the directory is being searched, you may be out of luck. For this to work, the task that is running in the background must be able to send your program notification and quantification of its progress, and I don't see your file searching being able to do this. This situation usually calls for displaying an indeterminate JProgressBar. – Hovercraft Full Of Eels Mar 05 '17 at 03:02
  • @HovercraftFullOfEels that is my goal. In my real project (non test apps) I have it returning all subfolders and files as well, which can have hundreds of files and take a while to display them. – Vince Mar 05 '17 at 03:05
  • `File.listFiles()` is an atomic operation from the point of view of Java. It either has not started or is in progress or has finished. The only progress values you can display for it are 0%, 100%, and a thoroughly misleading 50%. It's also a reasonably quick operation, unless you have millions of files. There is no point to this. – user207421 Mar 05 '17 at 03:47
  • @EJP my main project list subfiles and folders, and even though its not millions. It's hundreds, maybe thousands if i change it to my hard drive haha. But even my hundreds it takes a good 30 seconds or so to finish. I think it may be the cell rendering that's causing the delay – Vince Mar 05 '17 at 04:00

1 Answers1

0

The key will be to

  1. Be sure that you try to off-load any and all long-running processing to a background thread
  2. Avoid making Swing calls, especially mutational Swing calls on any thread other than the Swing event thread.
  3. Consider setting your JProgressBar to be indeterminate if you cannot quantify the amount of work that needs to be done and cannot be notified by the background task of what percent of this work has been done.
  4. Otherwise use a determinate progress bar, but have your background task notify the Swing GUI in an indirect way the amount of progress, and usually this is done through a listener with a call-back such as a PropertyChangeListener.

For example, I've tried to do the above with the code below, one that recursively lists files in directories and subdirectories, but I'm not yet 100% satisfied with it. It uses completion of the main subdirectories of the chosen directory, but it somehow freezes the GUI, meaning that I still have cpu or time-hogging code being called on the event thread. More work needs to be done here...

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileFilter;
import java.util.List;
import java.util.concurrent.ExecutionException;

import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.filechooser.FileSystemView;

@SuppressWarnings("serial")
public class ProgressBarTest2 extends JPanel {
    private static final int PREF_W = 420;
    private static final int PREF_H = 200;
    private JProgressBar progressBar = new JProgressBar(0, 100);
    private DefaultListModel<File> fileListModel = new DefaultListModel<>();
    private JList<File> results = new JList<>(fileListModel);
    private Action browseAction = new BrowseAction("Browse", KeyEvent.VK_B);
    private JButton browseButton = new JButton(browseAction);

    public ProgressBarTest2() {
        progressBar.setValue(0);
        progressBar.setStringPainted(true);

        results.setCellRenderer(new MyCellRenderer());
        results.setLayoutOrientation(JList.HORIZONTAL_WRAP);
        results.setVisibleRowCount(-1);
        JScrollPane scrollPane = new JScrollPane(results);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

        JPanel topPanel = new JPanel();
        topPanel.add(browseButton);

        setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
        setLayout(new BorderLayout(3, 3));
        add(topPanel, BorderLayout.PAGE_START);
        add(scrollPane, BorderLayout.CENTER);
        add(progressBar, BorderLayout.PAGE_END);
    }

    @Override
    public Dimension getPreferredSize() {
        Dimension superSz = super.getPreferredSize();
        if (isPreferredSizeSet()) {
            return superSz;
        }
        int prefW = Math.max(superSz.width, PREF_W);
        int prefH = Math.max(superSz.height, PREF_H);
        return new Dimension(prefW, prefH);
    }

    public void workerIsDone() {
        browseAction.setEnabled(true);
    }


    private class BrowseAction extends AbstractAction {
        private FileWorker fileWorker;

        public BrowseAction(String name, int mnemonic) {
            super(name);
            putValue(MNEMONIC_KEY, mnemonic);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            setEnabled(false);
            progressBar.setValue(0);

            JFileChooser chooser = new JFileChooser(System.getProperty("user.home"));
            chooser.setDialogTitle("Search what?");
            chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
            int choice = chooser.showOpenDialog(null);
            if (choice != JFileChooser.APPROVE_OPTION) {
                return;
            }

            File file = chooser.getSelectedFile();
            if (file == null) {
                return;
            }
            if (!file.isDirectory()) {
                file = file.getParentFile();
                if (file == null || !file.isDirectory()) {
                    return;
                }
            }

            fileWorker = new FileWorker(file);
            fileWorker.addPropertyChangeListener(new FileWorkerListener());
            fileWorker.execute();
        }
    }

    class FileWorkerListener implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if ("progress".equals(evt.getPropertyName())) {
                int progress = (int) evt.getNewValue();
                progressBar.setValue(progress);
            } else if ("state".equals(evt.getPropertyName())) {
                if (SwingWorker.StateValue.DONE == evt.getNewValue()) {
                    workerIsDone();
                    FileWorker worker = (FileWorker) evt.getSource();
                    try {
                        worker.get();
                    } catch (InterruptedException | ExecutionException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    class FileWorker extends SwingWorker<Void, File> {
        private MyFileFilter dirFilter = new MyFileFilter(true);
        private MyFileFilter nonDirFilter = new MyFileFilter(false);
        private File file;

        public FileWorker(File file) {
            this.file = file;
        }

        @Override
        protected Void doInBackground() throws Exception {
            fileListModel.clear();
            File[] files = file.listFiles(nonDirFilter);
            for (File file2 : files) {
                publish(file2);
            }
            File[] dirs = file.listFiles(dirFilter);
            for (int i = 0; i < dirs.length; i++) {
                recurseThruDirs(dirs[i]);
                int prog = (100 * i) / dirs.length;
                setProgress(prog);
            }

            setProgress(100);
            return null;
        }

        private void recurseThruDirs(File file) {
            if (file == null) {
                return;
            }
            File[] files = file.listFiles(nonDirFilter);
            if (files != null && files.length > 0) {
                for (File file2 : files) {
                    publish(file2);
                }
            }
            File[] dirs = file.listFiles(dirFilter);
            if (dirs != null && dirs.length > 0) {
                for (int i = 0; i < dirs.length; i++) {
                    recurseThruDirs(dirs[i]);
                }
            }
        }

        @Override
        protected void process(List<File> chunks) {
            for (File file : chunks) {
                fileListModel.addElement(file);
            }
        }
    }

    class MyFileFilter implements FileFilter {
        private boolean directory;

        public MyFileFilter(boolean directory) {
            this.directory = directory;
        }

        @Override
        public boolean accept(File pathname) {
            return directory == pathname.isDirectory();
        }
    }

    private class MyCellRenderer extends DefaultListCellRenderer {
        public Component getListCellRendererComponent(JList<?> list, Object value, int index,
                boolean isSelected, boolean cellHasFocus) {
            Component c = super.getListCellRendererComponent(list, value, index, isSelected,
                    cellHasFocus);
            JLabel l = (JLabel) c;
            File f = (File) value;
            l.setText(f.getName());
            l.setPreferredSize(new Dimension(130, 20));
            l.setIcon(FileSystemView.getFileSystemView().getSystemIcon(f));
            l.setBorder(new EmptyBorder(3, 3, 3, 3));

            return l;
        }
    }

    private static void createAndShowGui() {
        ProgressBarTest2 mainPanel = new ProgressBarTest2();

        JFrame frame = new JFrame("ProgressBarTest2");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(mainPanel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }
}
Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373