How to use fileDialog with QML for Web Assembly
-
Hello, I'm slowly discovering QML / QT Quick in order to be able to use 3D into a Web Assembly project that I initially made in Qt C++. QML is a very interesting language, but I'm struggling to find a way to use a file selector that will work on WA. For the moment I simply have a menubar with an entry that fires fileDialog.open(), that works fine on Desktop, but nothing happens on WA, it is maybe normal as on the C++ application I needed to use QFileDialog::getOpenFileContent (I mean to have it working on WA) and on QML FileDialog I only found open().
These are my very first steps with QML, so maybe I'm doing it wrong. I looked for all docs but didn't find any, maybe I need to dig the examples project some more. Does someone have any information that could help me ?
Here's my code
import QtQuick import QtQuick.Window import QtQuick.Controls import QtQuick.Dialogs ApplicationWindow { width: 640 height: 480 visible: true title: qsTr("debut") FileDialog { id: fileDialog nameFilters: ["Fichier 3D WaveFront (*.obj)"] } MenuBar { Menu { title: qsTr("&Fichier") Action { text: qsTr("&Nouveau depuis .OBJ...") onTriggered: { fileDialog.open() } } Action { text: qsTr("&Ouvrir .DEP...") } Action { text: qsTr("&Sauver .DEP") } Action { text: qsTr("&Exporter en SVG...") } MenuSeparator { } Action { text: qsTr("&Quitter") onTriggered: Qt.quit(); } } Menu { title: qsTr("&Edit") Action { text: qsTr("Cu&t") } Action { text: qsTr("&Copy") } Action { text: qsTr("&Paste") } } Menu { title: qsTr("Aide") Action { text: qsTr("&A propos") } } } }
here's what is on the console.log
-
Still investigating, I did what's written into fileDialog QML Type documentation, Availability paragraph :
The Qt Labs Platform module uses Qt Widgets as a fallback on platforms that do not have a native implementation available. Therefore, applications that use types from the Qt Labs Platform module should link to QtWidgets and use QApplication instead of QGuiApplication.
To link against the QtWidgets library, add the following to your qmake project file:QT += widgets
(I did the equivalent for cmake as stated on QApplication documentation :
find_package(Qt6 REQUIRED COMPONENTS Widgets) target_link_libraries(mytarget PRIVATE Qt6::Widgets)
)
Create an instance of QApplication in main():
#include <QApplication> #include <QQmlApplicationEngine> int main(int argc, char *argv[]) { QApplication app(argc, argv); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); return app.exec(); }
but nothing changed and I still have the same warning saying modules are not installed.
Maybe do I need to add those modules into the cmake file ? Or maybe am I using a too recent QT 6.6.0 (even if I'm using same versions for Desktop and WA kits, but I'm new to that) and need to get back to a previous one ?
Or maybe what I'm trying to do is not possible and this is not what QML for WA is supposed to be used for ?
-
Well, I didn't find any QML project that enables the user to use local file like it can be done with Qt C++ using QFileDialog::getOpenFileContent() and its saving counterpart, and don't know where to search. I will keep searching as I have no feedback that it is not possible. Anyway at the same time I am learning the basics of QML.
It's a little odd, my posts here seems to be standalone as it's been some time that I post without receiving some feedback.
-
@Gilboonet it's been a while since you've asked the question, but as I found it then I'll just reply for future generations :)
Basically you can't directly access any files on your devices from WASM. You need to use Browser API to achieve this, so the only option is to call native browser file picker for open & browser download for saving. You can pick which file to open, but when you save, then it's gonna be downloaded by a browser like any other file from a web (yes, it'll be saved in Downloads folder).
It seems that Qt also supports file picking on WASM but unfortunately I didn't have a chance to test it yet: https://doc.qt.io/qt-6/qfiledialog.html#getOpenFileContent
Here's a great example on how to use native file picker in Qt project:
https://github.com/msorvig/qt-webassembly-examples/tree/master/gui_localfiles
Here's my code, heavily based on above example, if you're interested:
std::function<void(const QByteArray &, const QString &)> fileDataReadyCallback; extern "C" EMSCRIPTEN_KEEPALIVE void wasmFileDataReadyCallback(char *content, size_t contentSize, const char *fileName) { if (fileDataReadyCallback == nullptr) return; QByteArray data(content, contentSize); free(content); fileDataReadyCallback(data, QString::fromUtf8(fileName)); fileDataReadyCallback = nullptr; } void WasmPlatformIntegration::openFile() { openFile("*", [](const QByteArray &content, const QString &fileName) { qDebug() << "load file" << fileName << "size" << content.size(); }); } void WasmPlatformIntegration::openFile(const QString &fileNameFilter, std::function<void(const QByteArray &, const QString &)> fileDataReady) { if (::fileDataReadyCallback) puts("Warning: Concurrent loadFile() calls are not supported. Cancelling " "earlier call"); ::fileDataReadyCallback = nullptr; ::wasmFileDataReadyCallback(nullptr, 0, nullptr); ::fileDataReadyCallback = fileDataReady; EM_ASM_( { const fileNameFilter = UTF8ToString($0); // Crate file file input which whil display the native file dialog let fileElement = document.createElement("input"); document.body.appendChild(fileElement); fileElement.type = "file"; fileElement.style = "display:none"; fileElement.accept = fileNameFilter; fileElement.onchange = function(event) { const files = event.target.files; // Read files for (var i = 0; i < files.length; i++) { const file = files[i]; var reader = new FileReader(); reader.onload = function() { const name = file.name; let contentArray = new Uint8Array(reader.result); const contentSize = reader.result.byteLength; const heapPointer = _malloc(contentSize); const heapBytes = new Uint8Array(Module.HEAPU8.buffer, heapPointer, contentSize); heapBytes.set(contentArray); // Null out the first data copy to enable GC reader = null; contentArray = null; // Call the C++ file data ready callback ccall("wasmFileDataReadyCallback", null, [ "number", "number", "string" ], [ heapPointer, contentSize, name ]); }; reader.readAsArrayBuffer(file); } // Clean up document document.body.removeChild(fileElement); }; // onchange callback // Trigger file dialog open fileElement.click(); }, fileNameFilter.toUtf8().constData()); } void WasmPlatformIntegration::saveFile(const QByteArray &content, const QString &fileNameHint) { EM_ASM_( { const contentPointer = $0; const contentLength = $1; const fileNameHint = UTF8ToString($2); const fileContent = Module.HEAPU8.subarray(contentPointer, contentPointer + contentLength); const arrayBuffer = fileContent.slice(); const uint8Array = new Uint8Array(arrayBuffer); uint8Array.fill(255); const fileblob = new Blob([arrayBuffer]); // Create a hidden download link and click it programatically let link = document.createElement("a"); document.body.appendChild(link); link.download = fileNameHint; link.href = window.URL.createObjectURL(fileblob); link.style = "display:none"; link.click(); document.body.removeChild(link); }, content.constData(), content.size(), fileNameHint.toUtf8().constData()); }
-