Copyfileex with Progress Callback in Qt

CopyFileEx with progress callback in Qt

The code below is a complete, self-contained example. It works under both Qt 5 and Qt 4, and uses C++11 (e.g. Visual Studio 2015 & newer).

main.cpp

// https://github.com/KubaO/stackoverflown/tree/master/questions/copyfileex-19136936
#include <QtGui>
#include <QtConcurrent>
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
#include <QtWidgets>
#endif
#include <windows.h>
#include <comdef.h>
//#define _WIN32_WINNT _WIN32_WINNT_WIN7

static QString toString(HRESULT hr) {
_com_error err{hr};
return QStringLiteral("Error 0x%1: %2").arg((quint32)hr, 8, 16, QLatin1Char('0'))
.arg(err.ErrorMessage());
}

static QString getLastErrorMsg() {
return toString(HRESULT_FROM_WIN32(GetLastError()));
}

static QString progressMessage(ULONGLONG part, ULONGLONG whole) {
return QStringLiteral("Transferred %1 of %2 bytes.")
.arg(part).arg(whole);
}

class Copier : public QObject {
Q_OBJECT

BOOL m_stop;
QMutex m_pauseMutex;
QAtomicInt m_pause;
QWaitCondition m_pauseWait;

QString m_src, m_dst;
ULONGLONG m_lastPart, m_lastWhole;
void newStatus(ULONGLONG part, ULONGLONG whole) {
if (part != m_lastPart || whole != m_lastWhole) {
m_lastPart = part;
m_lastWhole = whole;
emit newStatus(progressMessage(part, whole));
}
}
#if _WIN32_WINNT >= _WIN32_WINNT_WIN8
static COPYFILE2_MESSAGE_ACTION CALLBACK copyProgress2(
const COPYFILE2_MESSAGE *message, PVOID context);
#else
static DWORD CALLBACK copyProgress(
LARGE_INTEGER totalSize, LARGE_INTEGER totalTransferred,
LARGE_INTEGER streamSize, LARGE_INTEGER streamTransferred,
DWORD streamNo, DWORD callbackReason, HANDLE src, HANDLE dst,
LPVOID data);
#endif
public:
Copier(const QString & src, const QString & dst, QObject * parent = nullptr) :
QObject{parent}, m_src{src}, m_dst{dst} {}
Q_SIGNAL void newStatus(const QString &);
Q_SIGNAL void finished();
/// This method is thread-safe
Q_SLOT void copy();
/// This method is thread-safe
Q_SLOT void stop() {
resume();
m_stop = TRUE;
}
/// This method is thread-safe
Q_SLOT void pause() {
m_pause = true;
}
/// This method is thread-safe
Q_SLOT void resume() {
if (m_pause)
m_pauseWait.notify_one();
m_pause = false;
}
~Copier() override { stop(); }
};

#if _WIN32_WINNT >= _WIN32_WINNT_WIN8
void Copier::copy() {
m_lastPart = m_lastWhole = {};
m_stop = FALSE;
m_pause = false;
QtConcurrent::run([this]{
COPYFILE2_EXTENDED_PARAMETERS params{
sizeof(COPYFILE2_EXTENDED_PARAMETERS), 0, &m_stop,
Copier::copyProgress2, this
};
auto rc = CopyFile2((PCWSTR)m_src.utf16(), (PCWSTR)m_dst.utf16(), ¶ms);
if (!SUCCEEDED(rc))
emit newStatus(toString(rc));
emit finished();
});
}
COPYFILE2_MESSAGE_ACTION CALLBACK Copier::copyProgress2(
const COPYFILE2_MESSAGE *message, PVOID context)
{
COPYFILE2_MESSAGE_ACTION action = COPYFILE2_PROGRESS_CONTINUE;
auto self = static_cast<Copier*>(context);
if (message->Type == COPYFILE2_CALLBACK_CHUNK_FINISHED) {
auto &info = message->Info.ChunkFinished;
self->newStatus(info.uliTotalBytesTransferred.QuadPart, info.uliTotalFileSize.QuadPart);
}
else if (message->Type == COPYFILE2_CALLBACK_ERROR) {
auto &info = message->Info.Error;
self->newStatus(info.uliTotalBytesTransferred.QuadPart, info.uliTotalFileSize.QuadPart);
emit self->newStatus(toString(info.hrFailure));
action = COPYFILE2_PROGRESS_CANCEL;
}
if (self->m_pause) {
QMutexLocker lock{&self->m_pauseMutex};
self->m_pauseWait.wait(&self->m_pauseMutex);
}
return action;
}
#else
void Copier::copy() {
m_lastPart = m_lastWhole = {};
m_stop = FALSE;
m_pause = false;
QtConcurrent::run([this]{
auto rc = CopyFileExW((LPCWSTR)m_src.utf16(), (LPCWSTR)m_dst.utf16(),
©Progress, this, &m_stop, 0);
if (!rc)
emit newStatus(getLastErrorMsg());
emit finished();
});
}
DWORD CALLBACK Copier::copyProgress(
const LARGE_INTEGER totalSize, const LARGE_INTEGER totalTransferred,
LARGE_INTEGER, LARGE_INTEGER, DWORD,
DWORD, HANDLE, HANDLE,
LPVOID data)
{
auto self = static_cast<Copier*>(data);
self->newStatus(totalTransferred.QuadPart, totalSize.QuadPart);
if (self->m_pause) {
QMutexLocker lock{&self->m_pauseMutex};
self->m_pauseWait.wait(&self->m_pauseMutex);
}
return PROGRESS_CONTINUE;
}
#endif

struct PathWidget : public QWidget {
QHBoxLayout layout{this};
QLineEdit edit;
QPushButton select{"..."};
QFileDialog dialog;
explicit PathWidget(const QString & caption) : dialog{this, caption} {
layout.setMargin(0);
layout.addWidget(&edit);
layout.addWidget(&select);
connect(&select, SIGNAL(clicked()), &dialog, SLOT(show()));
connect(&dialog, SIGNAL(fileSelected(QString)), &edit, SLOT(setText(QString)));
}
};

class Ui : public QWidget {
Q_OBJECT
QFormLayout m_layout{this};
QPlainTextEdit m_status;
PathWidget m_src{"Source File"}, m_dst{"Destination File"};
QPushButton m_copy{"Copy"};
QPushButton m_cancel{"Cancel"};

QStateMachine m_machine{this};
QState s_stopped{&m_machine};
QState s_copying{&m_machine};

Q_SIGNAL void stopCopy();
Q_SLOT void startCopy() {
auto copier = new Copier(m_src.edit.text(), m_dst.edit.text(), this);
connect(copier, SIGNAL(newStatus(QString)), &m_status, SLOT(appendPlainText(QString)));
connect(copier, SIGNAL(finished()), SIGNAL(copyFinished()));
connect(copier, SIGNAL(finished()), copier, SLOT(deleteLater()));
connect(this, SIGNAL(stopCopy()), copier, SLOT(stop()));
copier->copy();
}
Q_SIGNAL void copyFinished();
public:
Ui() {
m_layout.addRow("From:", &m_src);
m_layout.addRow("To:", &m_dst);
m_layout.addRow(&m_status);
m_layout.addRow(&m_copy);
m_layout.addRow(&m_cancel);

m_src.dialog.setFileMode(QFileDialog::ExistingFile);
m_dst.dialog.setAcceptMode(QFileDialog::AcceptSave);
m_status.setReadOnly(true);
m_status.setMaximumBlockCount(5);

m_machine.setInitialState(&s_stopped);
s_stopped.addTransition(&m_copy, SIGNAL(clicked()), &s_copying);
s_stopped.assignProperty(&m_copy, "enabled", true);
s_stopped.assignProperty(&m_cancel, "enabled", false);
s_copying.addTransition(&m_cancel, SIGNAL(clicked()), &s_stopped);
s_copying.addTransition(this, SIGNAL(copyFinished()), &s_stopped);
connect(&s_copying, SIGNAL(entered()), SLOT(startCopy()));
connect(&s_copying, SIGNAL(exited()), SIGNAL(stopCopy()));
s_copying.assignProperty(&m_copy, "enabled", false);
s_copying.assignProperty(&m_cancel, "enabled", true);
m_machine.start();
}
};

int main(int argc, char *argv[])
{
QApplication a{argc, argv};
Ui ui;
ui.show();
return a.exec();
}
#include "main.moc"

Proper way to Copy files usign QT with GUI avoiding freeze

Your approach is correct, if a bit verbose. The "lag" is likely due to the kernel being silly about how it manages the page cache and evicting parts of your application to make room for pages from the files being copied. The only remedy for that might be to use a platform-specific file copying mechanism that advises the kernel that you'll be reading and writing sequentially, and that you don't need the data anymore after you've done the writing. See this answer for details and some Linux code. On Windows, one would hope that CopyFileEx does the correct advisory calls, a Qt example is here.

Progress bar with QFile::copy()?

You can't do this using the static QFile::copy() method.

As Maciej stated before you need to write your own class. It should use two QFile objects, one for reading one for writing. Transfer the data in portions (e.g 1% of the entire file size) and emit a progress signal after each portion. You can connect this signal to a progress dialog.

If you need this to work in the background you should implement it using a QThread.

First try to decide if you need a class that does the copy work asynchonously (without blocking the GUI) or synchronously (blocking the GUI). The latter is easier to programming but most times not what is intended (e.g. you can not cancel or pause a copy operation by button click if the GUI is blocked).

You can have a look here for a pretty extensive Qt 4 class: http://docs.huihoo.com/qt/solutions/4/qtcopydialog/qtfilecopier.html but I am not sure if this will help due to its complexity.

Non-blocking worker - interrupt file copy

I don't think the file size has any effect on how long a renaming will take.

For the copy - Qt offers nothing built in, you have to implement it yourself. The key gotcha here is that you will have to find some way to poll for a copy cancellation continuously. This means you cannot lock the main thread in order to be able to process events.

Whether you go for an extra thread in order to keep the main thread responsive, or decide to use the main thread - in both cases you will need to implement "fragmented" copying - one chunk at a time using a buffer, until the file is copied or copying is cancelled. You need this to be able to process user events and track copying progress.

I suggest you implement a QObject derived copy helper worker class which tracks file name, total size, buffer size, progress and clean up on cancellation. Then it is a matter of choice whether you will use it in the main thread or in a dedicated thread.

EDIT: Found it, but you better double check it, since it was done as an example and has not been thoroughly tested:

class CopyHelper : public QObject {
Q_OBJECT
Q_PROPERTY(qreal progress READ progress WRITE setProgress NOTIFY progressChanged)
public:
CopyHelper(QString sPath, QString dPath, quint64 bSize = 1024 * 1024) :
isCancelled(false), bufferSize(bSize), prog(0.0), source(sPath), destination(dPath), position(0) { }
~CopyHelper() { free(buff); }

qreal progress() const { return prog; }
void setProgress(qreal p) {
if (p != prog) {
prog = p;
emit progressChanged();
}
}

public slots:
void begin() {
if (!source.open(QIODevice::ReadOnly)) {
qDebug() << "could not open source, aborting";
emit done();
return;
}
fileSize = source.size();
if (!destination.open(QIODevice::WriteOnly)) {
qDebug() << "could not open destination, aborting";
// maybe check for overwriting and ask to proceed
emit done();
return;
}
if (!destination.resize(fileSize)) {
qDebug() << "could not resize, aborting";
emit done();
return;
}
buff = (char*)malloc(bufferSize);
if (!buff) {
qDebug() << "could not allocate buffer, aborting";
emit done();
return;
}
QMetaObject::invokeMethod(this, "step", Qt::QueuedConnection);
//timer.start();
}
void step() {
if (!isCancelled) {
if (position < fileSize) {
quint64 chunk = fileSize - position;
quint64 l = chunk > bufferSize ? bufferSize : chunk;
source.read(buff, l);
destination.write(buff, l);
position += l;
source.seek(position);
destination.seek(position);
setProgress((qreal)position / fileSize);
//std::this_thread::sleep_for(std::chrono::milliseconds(100)); // for testing
QMetaObject::invokeMethod(this, "step", Qt::QueuedConnection);
} else {
//qDebug() << timer.elapsed();
emit done();
return;
}
} else {
if (!destination.remove()) qDebug() << "delete failed";
emit done();
}
}
void cancel() { isCancelled = true; }

signals:
void progressChanged();
void done();

private:
bool isCancelled;
quint64 bufferSize;
qreal prog;
QFile source, destination;
quint64 fileSize, position;
char * buff;
//QElapsedTimer timer;
};

The done() signal is used to deleteLater() the copy helper / close copy dialog or whatever. You can enable the elapsed timer and use it to implement an elapsed time property and estimated time as well. Pausing is another possible feature to implement. Using QMetaObject::invokeMethod() allows the event loop to periodically process user events so you can cancel and update progress, which goes from 0 to 1. You can easily tweak it for moving files as well.

QT - QFile copy operation extremely slow

Well, changing the buffer size did no good, since that apparently is just a fallback in case the derived function engine()->copy() fails. I don't know exactly how that function works, nor did I want to waste time modifying core QT engine classes to make this work.

In the end, since my project was only supposed to run on Windows, I ended up using the native Win32 copy function. So I replaced my call to:

QFile::copy(src, dest);

with:

CopyFileExW((LPCWSTR)src.utf16(), (LPCWSTR)dest.utf16(), 0, this, 0, 0);

Note that you must #include "windows.h" for this invocation to work.



Related Topics



Leave a reply



Submit