diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt
index 84920c0ed..66f4d5b7e 100644
--- a/src/duckstation-qt/CMakeLists.txt
+++ b/src/duckstation-qt/CMakeLists.txt
@@ -34,6 +34,8 @@ add_executable(duckstation-qt
qtdisplaywidget.h
qthostinterface.cpp
qthostinterface.h
+ qtprogresscallback.cpp
+ qtprogresscallback.h
qtsettingsinterface.cpp
qtsettingsinterface.h
qtutils.cpp
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj
index e2cd6e290..1f7507dcf 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj
+++ b/src/duckstation-qt/duckstation-qt.vcxproj
@@ -49,6 +49,7 @@
+
@@ -61,6 +62,7 @@
+
@@ -128,6 +130,7 @@
+
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters
index 9f5f7c2b8..849bb9d35 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj.filters
+++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters
@@ -33,11 +33,14 @@
+
+
+
diff --git a/src/duckstation-qt/qtprogresscallback.cpp b/src/duckstation-qt/qtprogresscallback.cpp
new file mode 100644
index 000000000..b4c1597b2
--- /dev/null
+++ b/src/duckstation-qt/qtprogresscallback.cpp
@@ -0,0 +1,102 @@
+#include "qtprogresscallback.h"
+#include
+#include
+#include
+#include
+
+QtProgressCallback::QtProgressCallback(QWidget* parent_widget)
+ : QObject(parent_widget), m_dialog(QString(), QString(), 0, 1, parent_widget)
+{
+ m_dialog.setWindowTitle(tr("DuckStation"));
+ m_dialog.setMinimumSize(QSize(500, 0));
+ m_dialog.setModal(parent_widget != nullptr);
+ m_dialog.show();
+}
+
+QtProgressCallback::~QtProgressCallback() = default;
+
+bool QtProgressCallback::IsCancelled() const
+{
+ return m_dialog.wasCanceled();
+}
+
+void QtProgressCallback::SetCancellable(bool cancellable)
+{
+ BaseProgressCallback::SetCancellable(cancellable);
+ m_dialog.setCancelButtonText(cancellable ? tr("Cancel") : QString());
+}
+
+void QtProgressCallback::SetStatusText(const char* text)
+{
+ BaseProgressCallback::SetStatusText(text);
+ m_dialog.setLabelText(QString::fromUtf8(text));
+}
+
+void QtProgressCallback::SetProgressRange(u32 range)
+{
+ BaseProgressCallback::SetProgressRange(range);
+ m_dialog.setRange(0, static_cast(range));
+}
+
+void QtProgressCallback::SetProgressValue(u32 value)
+{
+ BaseProgressCallback::SetProgressValue(value);
+
+ if (m_dialog.value() == static_cast(value))
+ return;
+
+ m_dialog.setValue(value);
+ QCoreApplication::processEvents();
+}
+
+void QtProgressCallback::DisplayError(const char* message)
+{
+ qWarning() << message;
+}
+
+void QtProgressCallback::DisplayWarning(const char* message)
+{
+ qWarning() << message;
+}
+
+void QtProgressCallback::DisplayInformation(const char* message)
+{
+ qWarning() << message;
+}
+
+void QtProgressCallback::DisplayDebugMessage(const char* message)
+{
+ qWarning() << message;
+}
+
+void QtProgressCallback::ModalError(const char* message)
+{
+ QMessageBox::critical(&m_dialog, tr("Error"), QString::fromUtf8(message));
+}
+
+bool QtProgressCallback::ModalConfirmation(const char* message)
+{
+ return (QMessageBox::question(&m_dialog, tr("Question"), QString::fromUtf8(message), QMessageBox::Yes,
+ QMessageBox::No) == QMessageBox::Yes);
+}
+
+u32 QtProgressCallback::ModalPrompt(const char* message, u32 num_options, ...)
+{
+ enum : u32
+ {
+ MAX_OPTIONS = 3,
+ };
+
+ std::array options;
+
+ std::va_list ap;
+ va_start(ap, num_options);
+
+ for (u32 i = 0; i < num_options && i < MAX_OPTIONS; i++)
+ options[i] = QString::fromUtf8(va_arg(ap, const char*));
+
+ va_end(ap);
+
+ return static_cast(QMessageBox::question(&m_dialog, tr("Question"), QString::fromUtf8(message), options[0],
+ options[1], options[2], 0, 0));
+}
diff --git a/src/duckstation-qt/qtprogresscallback.h b/src/duckstation-qt/qtprogresscallback.h
new file mode 100644
index 000000000..726c731e9
--- /dev/null
+++ b/src/duckstation-qt/qtprogresscallback.h
@@ -0,0 +1,31 @@
+#pragma once
+#include "common/progress_callback.h"
+#include
+
+class QtProgressCallback final : public QObject, public BaseProgressCallback
+{
+ Q_OBJECT
+
+public:
+ QtProgressCallback(QWidget* parent_widget);
+ ~QtProgressCallback();
+
+ bool IsCancelled() const override;
+
+ void SetCancellable(bool cancellable) override;
+ void SetStatusText(const char* text) override;
+ void SetProgressRange(u32 range) override;
+ void SetProgressValue(u32 value) override;
+
+ void DisplayError(const char* message) override;
+ void DisplayWarning(const char* message) override;
+ void DisplayInformation(const char* message) override;
+ void DisplayDebugMessage(const char* message) override;
+
+ void ModalError(const char* message) override;
+ bool ModalConfirmation(const char* message) override;
+ u32 ModalPrompt(const char* message, u32 num_options, ...) override;
+
+private:
+ QProgressDialog m_dialog;
+};
\ No newline at end of file