2

Can anybody give me a working example of use CopyFileEx with progress callback in Qt?

I found some scratch and tried to merge it but with no success. I even couldn't pass CopyProgressRoutine function as an argument of CopyFileEx because I couldn't declare pointer to this function.

I'm not so good to port code from other IDEs, so I need help of yours.

Kamyllus
  • 23
  • 1
  • 3
  • 2
    If you're porting code, why not port the CopyFileEx function and use something like QFile::copy ? – TheDarkKnight Oct 02 '13 at 12:34
  • Yes, I know. However QFile::copy is useless for me because it doesn't emit the bytesWritten() signal so I cannot show progress of copying. For portable code I use macros and OS specific methods. Generally I use QIODevice::read and ::write but AFIK it is slower than CopyFileEx so for Windows I would like to use the fastest one. – Kamyllus Oct 02 '13 at 19:59
  • Are you just assuming QIODevice is slower? I'd be interested if you have any info about that. – TheDarkKnight Oct 03 '13 at 07:34
  • @Merlin069 Like I said I had to rely on the experience of others. Before I placed my ask here I was googling many hours. I found then a lot of hints and scratch of code (e.g. [link](http://www.qtcentre.org/archive/index.php/t-14215.html)) but I couldn't merge it. I've read many times that CopyFileEx is the fastest and the most secure method of copying files for Windows. I believe that some guru can squeeze out of QIODevice impressive effects but as you know I'm not a guru and CopyFileEx gives me exactly what I want. – Kamyllus Oct 03 '13 at 11:11
  • http://stackoverflow.com/questions/32952474/non-blocking-worker-interrupt-file-copy – dtech Sep 17 '16 at 14:59

1 Answers1

7

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(), &params);
      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(),
                            &copyProgress, 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"
Community
  • 1
  • 1
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • This is what I was looking for! I tested it under Qt 5 and it works like a charm. Thank you very much! – Kamyllus Oct 04 '13 at 12:13
  • How would one modify the CopyFileEx example to be thread-safe if run from a worker object moved to a worker thread via a worker->moveToThread(workerThread); call? It would be most elegant to perform CopyFileEx in the same thread as the rest of the worker thread object logic instead of spinning off in another thread via QtConcurrent... but that's just my style :) – riverofwind May 07 '18 at 19:50
  • Unfortunately `CopyFileEx` takes a whole thread for itself, so no other logic will work in that thread while the copy is underway - it monopolizes it. `CopyFileEx` does not do message dispatch and doen't spin an event loop. There's nothing else to be done to make it "more" thread-safe: the methods marked thread-safe remain thread-safe even if you don't use `QtConcurrent` -- and yes, you certainly may want to move the `Copier` to a thread where it's used from, i.e. where all the other non-thread-safe methods are called. In that respect, `CopyFile[Ex|2]` is an unfortunate PITA. – Kuba hasn't forgotten Monica May 08 '18 at 02:34
  • What's the difference between CopyFileEx and CopyFile2 other than the obviously different class members and implementation? Also those scary mutexes are just for pausing right? – riverofwind May 12 '18 at 19:47
  • Please correct me if I'm wrong - I ran an experiment using CopyCallEx. I did a qDebug() << QThread::currentThreadId(); in the worker thread and in the CopyFileEx static callback function and they displayed the same thread. Also I put a qDebug() after the CopyFileEx call in the worker thread and the CopyFileEx call blocked synchronously until it finished. Does this mean I can avoid using Qt::Concurrent::run, call CopyFileEx directly from my worker thread object, and access worker thread object member functions directly from the static callback function? Thanks for the help! – riverofwind May 13 '18 at 00:03
  • I meant access worker thread object member functions directly from the static callback function using the passed this pointer. – riverofwind May 13 '18 at 00:25
  • `QtConcurrent::run` is there so that you don't have to mess with thread management yourself. The difference between `CopyFileEx` and `CopyFile2` is that the latter has an extensible interface that Microsoft maintains, and will gain features over time, and should be used on platforms that support it. – Kuba hasn't forgotten Monica May 13 '18 at 20:15
  • Thanks Kuba. Is it thread-safe to execute CopyFileEx/2 from a worker thread object, use a static callback worker thread object method for progress, and access worker thread object methods directly from the progress callback static function? Then I just emit signals from those methods to the main GUI thread to update the GUI re progress. – riverofwind May 13 '18 at 23:46
  • `CopyFileEx/2` is a single-threaded function. The callback is called with `CopyFile` on the call stack, i.e. from the same thread. The worker thread object should never be used from within the thread, because its own thread (`workerObj.thread()`) is the thread where it was created, not the thread it controls, and when dealing with `QObject` instances, the non-thread-safe methods should be called only with `object.thread() == QThread::currentThread`. Really, you shouldn't be deriving from `QThread`. – Kuba hasn't forgotten Monica May 14 '18 at 01:28
  • I'm pretty sure I didn't derive from QThread I did a worker->moveToThread(workerThread) call with my worker thread object, worker, a class derived from QObject, containing all the worker thread logic. Does that change the situation at all? – riverofwind May 14 '18 at 05:14
  • Is it thread safe to use the previously mentioned approach though? The comment that starts with Thanks Kuba has the details. I need somebody to hold my hand in the face of scary race conditions ha. Thanks again. – riverofwind May 14 '18 at 19:24