使用Java复制文件时的进度条

14

我相信这个问题以前已经被问过了,但是我找到的答案都不能很好地与我的现有代码配合使用。如果有一种方法可以在不完全重做我已有的代码的情况下完成它,我会发表这个问题。

这个想法是在从一个驱动器复制文件和目录的同时显示一个非常基本的进度条。我有一个名为BasicCopy的类,旨在将Pictures、Documents、videos和Music文件夹(标准的Windows机器上)的内容复制到第二个驱动器上的同名文件夹的备份目录中。以下是目前为止的类:

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.commons.io.FileUtils;

public class BasicCopy {

    String username = "";
    String mainDrive = "";
    String backupDrive = "";
    String backupDir = "";
    String[] directories;

    public BasicCopy(String inDrive, String outDrive, String username){
        mainDrive = inDrive;
        backupDrive = outDrive;
        this.username = username;

        createBackupDirectory();
        copyDirectories();

        close();
    }

    //Create backup directory
    public void createBackupDirectory(){
        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("MM-dd-yyyy_HMMSS");
        String timestamp = sdf.format(date);
        backupDir = backupDrive + ":\\" + "Backup " + timestamp;
        File backupDirectory = new File(backupDir);
        backupDirectory.mkdir();
    }

    public void copyDirectories(){
        //Main directories
        String pics = mainDrive + ":\\Users\\" + username + "\\Pictures";
        String docs = mainDrive + ":\\Users\\" + username + "\\Documents";
        String vids = mainDrive + ":\\Users\\" + username + "\\Videos";
        String musc = mainDrive + ":\\Users\\" + username + "\\Music";
        //Backup directories
        String bkPics = backupDir + "\\Pictures";
        String bkDocs = backupDir + "\\Documents";
        String bkVids = backupDir + "\\Documents";
        String bkMusc = backupDir + "\\Pictures";

        String[] directories = {pics, docs, vids, musc};
        String[] bkDirectories = {bkPics, bkDocs, bkVids, bkMusc};

        //Loop through directories and copy files
        for (int i = 0; i < directories.length; i++){
            File src = new File(directories[i]);
            File dest = new File(bkDirectories[i]);
            try{
                FileUtils.copyDirectory(src, dest);
            } catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    /* Close current process */
    public void close(){
        System.exit(0);
    }
}

我之前有一个类中的方法,可以测量目录的总大小,因此如果需要的话,我可以将其传递给这个类。但是,目前我仅循环遍历四个目录,因此我预计在每个刻度上无法以高于25%的分辨率增加进度条。我在想是否有办法更改它,以便可以包含一个进度条来监视它,并且让它更准确一些?此外,我不确定是否应在不同的线程中提出此问题,但是这种文件复制方法需要非常长的时间。复制500MB的文件需要数小时,我想知道是否有方法可以加快速度?虽然那部分不是优先考虑的,但现在我主要是想添加一个进度条。干杯!

编辑:

经过一些试验,我意识到我可能可以使用类似于以下代码的代码(该代码可能有效,也可能无效 - 我只是快速写下来以免忘记,它仍未经测试)。这将允许我为每个复制的文件更新进度条。

for (int i = 0; i < directories.length; i++){
    File dir = new File(directories[i]);
    File dest = new File(bkDirectories[i]);
    for(File file: dir.listFiles()){
        try{
            FileUtils.copyDirectory(file, dest);
            //update progress bar here
        } catch (IOException e){
            e.printStackTrace();
        }
    }
}

编辑 #2:

我已经在代码上做了更多的工作,我相信大部分问题都解决了。现在的问题是关于SwingWorker,我相信我需要它来在后台运行长期的方法。否则GUI会变得不响应(Java文档中有很多关于这方面的文档)。然而,这就是我被卡住的地方。我之前只用过一次SwingWorker,而且那主要是复制了别人的代码。我在想如何使用下面的代码来实现SwingWorker,以便进度条(以及整个框架)能够真正出现。

更新的代码:

import java.awt.Toolkit;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.JTextArea;
import javax.swing.JButton;
import javax.swing.JProgressBar;
import javax.swing.JLabel;

import org.apache.commons.io.FileUtils;

@SuppressWarnings("serial")
public class BasicCopy extends JFrame {

    private JPanel contentPane;
    private JTextArea txtCopiedDirs;
    private JButton btnCancel;
    private JProgressBar progressBar;
    private JLabel lblCopying;
    private String mainDrive;
    private String backupDrive;
    private String username;
    private String backupDir;
    long totalSize = 0; //total size of directories/files
    long currentSize = 0;   //current size of files counting up to ONE_PERCENT
    int currentPercent = 0; //current progress in %
    long ONE_PERCENT;       //totalSize / 100

    public BasicCopy() {
    }

    public BasicCopy(String inDrive, String outDrive, String username, long space){
        mainDrive = inDrive;
        backupDrive = outDrive;
        this.username = username;
        totalSize = space;
        ONE_PERCENT = totalSize/100;
        createGUI();

        /*  Should not have these here!
         *  Pretty sure I need a SwingWorker
         */
        createBackupDirectory();
        copyDirectories();
    }

    public void createGUI(){
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setTitle("Backup Progress");
        setBounds(100, 100, 450, 300);
        contentPane = new JPanel();
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        setContentPane(contentPane);
        contentPane.setLayout(null);

        txtCopiedDirs = new JTextArea();
        txtCopiedDirs.setBounds(10, 56, 414, 125);
        contentPane.add(txtCopiedDirs);

        btnCancel = new JButton("Cancel");
        btnCancel.setBounds(169, 227, 89, 23);
        contentPane.add(btnCancel);

        progressBar = new JProgressBar(0, 100);
        progressBar.setBounds(10, 192, 414, 24);
        progressBar.setValue(0);
        contentPane.add(progressBar);

        lblCopying = new JLabel("Now backing up your files....");
        lblCopying.setBounds(10, 11, 157, 34);
        contentPane.add(lblCopying);

        setVisible(true);
    }

    //Create backup directory
    public void createBackupDirectory(){
        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("MM-dd-yyyy_HMMSS");
        String timestamp = sdf.format(date);
        backupDir = backupDrive + ":\\" + "Backup " + timestamp;
        File backupDirectory = new File(backupDir);
        backupDirectory.mkdir();
    }

    public void copyDirectories(){
        //Main directories
        String pics = mainDrive + ":\\Users\\" + username + "\\Pictures";
        String docs = mainDrive + ":\\Users\\" + username + "\\Documents";
        String vids = mainDrive + ":\\Users\\" + username + "\\Videos";
        String musc = mainDrive + ":\\Users\\" + username + "\\Music";
        //Backup directories
        String bkPics = backupDir + "\\Pictures";
        String bkDocs = backupDir + "\\Documents";
        String bkVids = backupDir + "\\Documents";
        String bkMusc = backupDir + "\\Pictures";

        String[] directories = {pics, docs, vids, musc};
        String[] bkDirectories = {bkPics, bkDocs, bkVids, bkMusc};

        //Loop through directories and copy files
        for (int i = 0; i < directories.length; i++){
            File dir = new File(directories[i]);
            File dest = new File(bkDirectories[i]);
            for(File file: dir.listFiles()){
                try{
                    FileUtils.copyDirectory(file, dest);
                    if(getDirSize(file) >= ONE_PERCENT){
                        currentPercent++;
                        progressBar.setValue(currentPercent);
                        currentSize = 0;
                    } else {
                        currentSize = currentSize + getDirSize(file);
                        if(currentSize >= ONE_PERCENT){
                            currentPercent++;
                            progressBar.setValue(currentPercent);
                            currentSize = 0;
                        }
                    }
                } catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
    }

    public static Long getDirSize(File directory) {
        long size = 0L;

        if (directory.listFiles() != null){       
            for (File file : directory.listFiles()) {
                size += file.isDirectory() ? getDirSize(file) : file.length();
            }
        }
        return size;
    }

    /* Close current window */
    public void closeWindow() {
        WindowEvent close = new WindowEvent(this, WindowEvent.WINDOW_CLOSING);
        Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(close);
        System.exit(0);
    }
}

我找到的所有答案都不能很好地与我的现有代码配合使用,为什么呢? - Andrew Thompson
它们都需要完全重写,需要不同的方法来循环遍历文件、创建目录等。我按照自己的方式设置了循环,有其原因,真的希望能够适应进度条,因为我的代码正在正常运行。 - DerStrom8
  1. btnCancel.setBounds(169, 227, 89, 23); 你从哪里获取代码示例的?总之,这是非常糟糕的代码,不应该这样做。我想知道是什么资源导致人们走上了这条错误的道路。
  2. 一般建议:不要阻塞EDT(事件分派线程)-当发生这种情况时,GUI将“冻结”。不要调用Thread.sleep(n),而是实现Swing定时器进行重复任务或SwingWorker进行长时间运行的任务。有关更多详细信息,请参见Swing中的并发性
- Andrew Thompson
"并且我真的希望我能够使进度条适用于它。" 我曾经非常期望在圣诞节能得到一匹小马,但我们并不总是得到自己想要的东西。 - Andrew Thompson
是使用WindowBuilder创建的。嗯...我以为GUI构建器随着时间的推移会变得更加明智(通过鼓励人们使用布局)。Java GUI可能需要在许多平台上工作,在不同的屏幕分辨率上使用不同的PLAF。因此,它们不利于组件的精确放置。为了组织健壮的GUI组件,请使用布局管理器或它们的组合,以及布局填充和边框来创建空白间隔。详见:https://dev59.com/pG035IYBdhLWcg3wGMHa#5630271 和 https://dev59.com/3GMm5IYBdhLWcg3wG7_A。 - Andrew Thompson
显示剩余2条评论
4个回答

7

我已经找到了一个可行的解决方案。虽然还有一些小故障,但主要是一些不重要的细节。以下是我现在拥有的内容,它似乎正在运作。

class Task extends SwingWorker<Void, Void>{
    @Override
    public Void doInBackground(){
        setProgress(0);

        //Create Backup Directory
        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("MM-dd-yyyy_HMMSS");
        String timestamp = sdf.format(date);
        backupDir = backupDrive + ":\\" + "Backup_" + timestamp;
        File backupDirectory = new File(backupDir);
        backupDirectory.mkdir();

        //Copy Files
        //Main directories
        String pics = mainDrive + ":\\Users\\" + username + "\\Pictures\\";
        String docs = mainDrive + ":\\Users\\" + username + "\\Documents\\";
        String vids = mainDrive + ":\\Users\\" + username + "\\Videos\\";
        String musc = mainDrive + ":\\Users\\" + username + "\\Music\\";
        //Backup directories
        String bkPics = backupDir + "\\Pictures\\";
        String bkDocs = backupDir + "\\Documents\\";
        String bkVids = backupDir + "\\Documents\\";
        String bkMusc = backupDir + "\\Pictures\\";

        String[] directories = {pics, docs, vids, musc};
        String[] bkDirectories = {bkPics, bkDocs, bkVids, bkMusc};

        //Loop through directories and copy files
        for (int i = 0; i < directories.length; i++){
            File dir = new File(directories[i]);
            File dest = new File(bkDirectories[i]);
            for(File file: dir.listFiles()){
                try{
                    if(file.isFile()){
                        FileUtils.copyFileToDirectory(file, dest);
                        txtCopiedDirs.append(file.getAbsolutePath() + "\n");
                    } else if (file.isDirectory()){
                        FileUtils.copyDirectoryToDirectory(file, dest);
                        txtCopiedDirs.append(file.getAbsolutePath() + "\n");
                    }
                    if(getDirSize(file) >= ONE_PERCENT){
                        currentPercent = getDirSize(file)/ONE_PERCENT;
                        progressBar.setValue((int)currentPercent);
                        currentSize = 0;
                    } else {
                        currentSize = currentSize + getDirSize(file);
                        if(currentSize >= ONE_PERCENT){
                            currentPercent = currentSize/ONE_PERCENT;
                            currentPercent++;
                            progressBar.setValue((int)currentPercent);
                            currentSize = 0;
                        }
                    }
                } catch (IOException e){
                    e.printStackTrace();
                }
            }
        }

        return null;
    }
    @Override
    public void done(){
        closeWindow();
    }
}

这个类被包含在主类中,并且可以使用以下代码启动:

Task task = new Task();
task.execute();

在框架创建并设置可见后立即调用此函数。

现在,正如我所提到的,这仍然不完美。例如,在复制之前检查它是文件还是目录的操作应该被替换为一个递归函数,使其循环遍历每个单独的文件,而不仅仅是复制整个目录,如果它是一个完整的目录。这将允许更准确地更新进度条,并显示在文本区域中复制的单个文件,而不是文件和目录。

无论如何,我只是想发布这段代码,以展示对我有效的内容,而不是完全重做我所拥有的东西。尽管如此,我仍将继续努力,并发布任何重要的更新,可能会帮助未来的读者。

感谢大家的回复,它们对我很有帮助!


1
你应该意识到这个例子违反了Swing的单线程规则,因为它直接从doInBackground方法中更新进度条,这就失去了使用SwingWorker的目的。最好使用属性更改支持并通过它来更新进度条。 - MadProgrammer

3
我假设您想要一个图形化进度条,而不是基于控制台的(tui)解决方案。您是否阅读了这个Swing tutorial
编辑:如果您希望每个文件有一个进度条刻度,您只需要告诉进度条构造函数有多少总刻度,例如:
progressBar = new JProgressBar(0, allFilesInAllDirectoriesLength);

重新组织您的for循环,使其针对每个文件进行操作,而不是在目录上循环。


你说得对,我正在寻找一个带有GUI的进度条。然而,我的主要问题并不是关于创建进度条,而是关于将其与我的代码链接起来。顺便说一下,我已经阅读了那篇教程,但还是谢谢你提供的链接--我以后可能需要参考它。 - DerStrom8
每个目录一个勾,是的。我宁愿每个文件有一个勾,例如。我试图在这句话中解释基本概念(尽管没有真正的细节):“我当前只循环遍历四个目录,因此我指望我无法使用比每个刻度高于25%更高的分辨率增加进度条。” - DerStrom8
嗯,实际上我有一个进一步的问题--在什么时候我会检查总大小?看起来我只能在每次循环迭代时执行一次,这仍然是25%的分辨率? - DerStrom8
每次缓冲区键入内容时,取出数量并将其添加到某种计数器中。当进度达到1%或超过1%时,请更新进度条。您需要制作自己的copyDir方法。是的,您必须仅在复制开始时检查文件大小。在循环过程中,您只需跟踪已经被复制到另一个目录的字节。 - Tomas Bisciak
我已经在原始帖子中添加了一些新的代码。现在,我需要让它循环遍历每个文件,而不是目录,并且现在我需要实现一个SwingWorker,以便我可以在后台运行方法。我在我的代码中有一个注释(在我调用创建备份目录和复制目录的方法的地方),显示需要完成的工作。不幸的是,我对SwingWorkers的使用不熟悉,所以我希望有人能够帮我解决这个问题。非常感谢! - DerStrom8
显示剩余4条评论

2

这里是我的 Java 8 的想法,使用 apache FileUtils(可以替换为其他)。它会在单独的线程中每隔 n 秒检查目录大小:

函数式接口:

public interface DirectoryMovementStatusFeedback {
    void notifyStatus(double percentMoved, double speedInMB);
}

服务:

public class FileMovementStatusUpdater {

    private long checkInterval = 2000;
    private boolean interrupted = false;

    public long getCheckInterval() {
        return checkInterval;
    }

    public void setCheckInterval(long checkInterval) {
        this.checkInterval = checkInterval;
    }

    public void monitor(File directory, long desiredSize, DirectoryMovementStatusFeedback feedback) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    double percentageMoved = 0;
                    double lastCheckedSize = 0;
                    do {
                        double currentSize = directory.exists() ? FileUtils.sizeOfDirectory(directory) : 0;
                        double speed = (currentSize - lastCheckedSize) / (1024 * checkInterval);
                        lastCheckedSize = currentSize;
                        percentageMoved = 100 * currentSize / desiredSize;
                        feedback.notifyStatus(percentageMoved, speed);
                        Thread.sleep(checkInterval);
                    } while (percentageMoved < 100 && !interrupted);
                } catch (Exception e) {
                    System.err.println("Directory monitor failed. " + e.getMessage());
                }
            }
        }).start();
    }

    public void stopMonitoring() {
        this.interrupted = true;
    }

用法:

    FileMovementStatusUpdater dirStatus = new FileMovementStatusUpdater();
    try {
        dirStatus.monitor(destination, FileUtils.sizeOfDirectory(source), (percent, speed) -> {
            progressBar.setValue(percent);
            speedLabel.setValue(speed+" MB/s");
        });
        FileUtils.moveDirectory(source, destination);
    } catch (Exception e) {
        // something
    }
    dirStatus.stopMonitoring();

1
这可能是一个更复杂、甚至有些愚蠢的想法,但或许它能够帮助,所以我决定发布它。使用您自己的方法来复制文件,检查文件大小,取其中1%的大小,每当您达到表示该文件1%的字节数/MB时,更新进度条。
我时间紧张,所以至少我会粘贴一些代码并加上注释,以便您了解我的想法。
//check size of files/dir to copy,before calling method bellow
//then you coud use for loop to do all the work

//make counter of how many mb/bits you already did.

    public void copyDirectory(File sourceLocation , File targetLocation) throws IOException {
        if (sourceLocation.isDirectory()) {
            if (!targetLocation.exists()) {
                targetLocation.mkdir();
            }

            String[] children = sourceLocation.list();
            for (int i=0; i<children.length; i++) {
                copyDirectory(new File(sourceLocation, children[i]),
                        new File(targetLocation, children[i]));
            }
        } else {

            InputStream in = new FileInputStream(sourceLocation);
            OutputStream out = new FileOutputStream(targetLocation);


            // Copy the bits from instream to outstream
            byte[] buf = new byte[1024];
            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
                //increment counter 
                //check if counter is above next 1% of size of your dirs

                //if you are then update progress bar by one percent
            }
            in.close();
            out.close();
        }
    }

这个解决方案尚未经过测试,但这是我开始解决这个问题的方式。

我知道我告诉过你我会接受你的答案,但事实证明它对我来说并不完全可行。我已经给了你一个赞,但我即将发布自己的解决方案,似乎已经起作用了。还有一些小问题,但它能够工作,我想让你们和未来的读者知道我做了什么。非常感谢你们的帮助,你们让我跳出了思维定势! - DerStrom8
谢谢你,没问题,我只是想像我们这里所有人一样帮助你。很高兴你找到了解决方案! - Tomas Bisciak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接