Porting Slate to Qt for WebAssembly
将Slate移植到Qt for WebAssembly
October 17, 2022 by Morten Johan Sørvig | Comments
2022年10月17日 Morten Johan Sörvig 评论
The 6.4 release makes Qt for WebAssembly a supported platform. In this post we'll take a look at how to port an existing application to this platform, using the Slate pixel art drawing app as an example. Slate is a Qt Quick-based application which means Qt Quick will be the focus of this blog post, however Qt for WebAssembly supports Qt Widgets as well and a lot should carry over to that module.
6.4版使Qt for WebAssembly成为一个受支持的平台。在本文中,我们将以Slate像素艺术绘图应用程序为例,看看如何将现有应用程序移植到此平台。Slate是一个基于Qt-Quick的应用程序,这意味着Qt-Quick将成为本博客文章的重点,然而Qt-for-WebAssembly也支持Qt-Widgets,很多内容应该继续到该模块中。
Slate is available as a web application - follow that link if you want to skip ahead and view the finished result.
Slate可以作为web应用程序使用-如果您想跳过并查看完成的结果,请访问该链接。
Porting may not be the right word to use here. Depending on the nature of the application it might run on WebAssembly without modifications, in which case building for WebAssembly would be a better term. Usually there is something to fix or adapt to the platform, though.
移植可能不是这里使用的正确单词。根据应用程序的性质,它可能在WebAssembly上运行而不进行修改,在这种情况下,为WebAssemply构建将是一个更好的术语。不过,通常需要对平台进行修复或调整。
A simplified layer diagram for an application using Qt on the web looks like this:
web上使用Qt的应用程序的简化层图如下所示:
There are a couple of things to take note of here. First, we are building on the "Web" platform and ignore everything below, since web browsers will abstract this away from us. Second, Emscripten has a thick and a thin layer. This reflects the different ways to use it: as a provider of the libc & POSIX platform implementation and as an interface layer to the web platform. Finally, Qt is in the center (this is a Qt blog after all) but does not cover all functionality the app might need.
这里有几件事需要注意。首先,我们是在“Web”平台上构建的,忽略了下面的一切,因为Web浏览器将从我们这里抽象出来。其次,Emscripten有厚层和薄层。这反映了使用它的不同方式:作为libc&POSIX平台实现的提供者和作为web平台的接口层。最后,Qt位于中心位置(毕竟这是一个Qt博客),但并没有涵盖应用程序可能需要的所有功能。
Getting Started
入门
We'll skip over the very basics here, head over to the Emscripten documumentation and the Qt documentation which covers these topics. I reccomend starting with something simpler than a complete application, for example by building small "hello world" app first.
我们将跳过这里的基础知识,转到涵盖这些主题的Emscripten文档和Qt文档。我建议从一个比完整应用程序更简单的东西开始,例如首先构建一个小型的“hello world”应用程序。
Qt for WebAssembly differs from the other platforms Qt supports in that the app runs in the web browser sandbox. This places restrictions on the applcation, such as limiting access to local files and local fonts. We’ll tackle some of those here in the process of porting building Slate for the web.
Qt for WebAssembly与Qt支持的其他平台不同,因为该应用程序在web浏览器沙盒中运行。这对应用程序设置了限制,例如限制对本地文件和本地字体的访问。我们将在为web移植构建Slate的过程中解决其中一些问题。
The modifcations to Slate have not been upstreamed to the main repo yet, and are currently available here. The utility functions used are published separately at Qt Web Utils.
对Slate的修改尚未上传至主回购,目前可在此处获得。使用的实用程序函数在Qt Web Utils上单独发布。
Building for WebAssembly
为WebAssembly构建
Qt for WebAssembly builds use static linking, are without thread support by default, and are 32-bit builds. For Slate the latter two were unproblematic, however at startup it greets us with the following error:
Qt for WebAssembly版本使用静态链接,默认情况下不支持线程,并且是32位版本。对于Slate来说,后两者都没有问题,但在启动时,它向我们发出以下错误提示:
I usually fall back to a static desktop build of Qt on any sort of plugin loading error (this requires building Qt from source, which is arguably easier to do if you are doing that all day anyway), and then debug from there using the desktop host debug tools. Having a successful static desktop build does not guarantee that the WebAssembly build will work, however if the static desktop build does not work then there is little hope for the wasm build.
对于任何类型的插件加载错误(这需要从源代码构建Qt,如果你整天都在这么做的话,这可能更容易做到),我通常会回到静态桌面构建的Qt,然后使用桌面主机调试工具从那里进行调试。成功的静态桌面构建并不能保证WebAssembly构建能够正常工作,但是如果静态桌面构建不能正常工作,那么wasm构建的希望就很渺茫。
In the case of Slate the issue was that it has a slightly custom project layout which was preventing the static build logic from finding and linking to an internal QML component. I fixed this by making a small change to the build system, though I doubt if this specific fix will be relevant in many other cases.
在Slate的情况下,问题是它有一个稍微自定义的项目布局,这阻止了静态构建逻辑查找和链接到内部QML组件。我通过对构建系统做一个小的更改来解决这个问题,但我怀疑这个特定的修复是否与许多其他情况相关。
With that in place Slate builds and runs successfully to the point where it is usuable as a pixel drawing app. Next, we are going to take a look at several adjustments for the web platform.
有了这一点,Slate成功构建并运行到可以作为像素绘图应用程序使用的程度。接下来,我们将看一看web平台的几个调整。
Menu Cleanup
菜单清理
The slate file menu has some desktop-isms which do not neccesarily make sense on the web. For example, users do not quit a web app using the "Quit" menu item, instead they close the tab. Also, "Save" and "Save as" can be combined to a single menu item.
slate文件菜单中有一些桌面图标,这些图标在web上不一定有意义。例如,用户不会使用“退出”菜单项退出web应用程序,而是关闭选项卡。此外,“保存”和“另存为”可以组合到单个菜单项。
This can be implemented by testing for the "wasm" platform at run-time, and then hiding the superflous menu items by setting visibility to false and the height to zero. (Remember to hide the menu separators as well!)
这可以通过在运行时测试“wasm”平台来实现,然后通过将可见性设置为false和高度设置为零来隐藏多余的菜单项。(请记住也要隐藏菜单分隔符!)
Menu {
readonly property bool isWebPlatform: Qt.platform.os == "wasm"
MenuItem {
text: qsTr("Quit")
visible: !isWebPlatform
height: visible ? implicitHeight : 0
}
}
Local File Access
本地文件存取
QFile and friends are available also on WebAssembly, with an important caveat that the files operated on by this API are stored on the in-memory file system provided by Emscripten, which means that any data written with QFile is lost after the user navigates away from the app.
QFile和friends在WebAssembly上也可用,但有一个重要的警告,即此API操作的文件存储在Emscripten提供的内存文件系统中,这意味着用户离开应用程序后,用QFile写入的任何数据都会丢失。
Widgets-based applications may use QFileDialog::getOpenFileContent() which covers this use case. That API returns the content of the opened file as a QByteArray. This works out well for C++ code, but not so well for QML code which works with file Urls.
基于Widget的应用程序可以使用QFileDialog::getOpenFileContent(),它涵盖了这个用例。该API将打开的文件的内容作为QByteArray返回。这对于C++代码很有效,但对于使用文件URL的QML代码则不太有效。
Qt does not have a built-in solution for QML appliactions yet, but we can make one. The apprach will be to save a copy of the file to the in-memory file system, and then give the path to that file to QML code. We can build on top of the existing getOpenFileContent() API (which requires linking against widgets), or on top of the private API in QtGui (but don't tell anyone).
Qt还没有用于QML应用的内置解决方案,但我们可以制作一个。解决方法是把文件的副本保存到内存文件系统中,然后将该文件的路径指定给QML代码。我们可以在现有的getOpenFileContent()API(需要与widget链接)之上构建,也可以在QtGui中的私有API之上构建(但不要告诉任何人)。
The implementation is available as a helper function in the Qt Web Utils repo. Usage in Slate looks like the following:
该实现在Qt Web Utils repo中作为助手函数提供。Slate中的用法如下:
MenuItem {
objectName: "openMenuItem"
text: qsTr("Open")
onTriggered: {
if (Qt.platform.os == "wasm") {
WebUtils.loadFileToFileSystem("*.slp", "/tmp/tmploadfile.slp",
function(tmpFilePath) {
loadProject("file://" + tmpFilePath)
}
);
} else {
openProjectDialog.open();
}
}
}
Fullscreen
全屏
Slate has a fullscreen button. On desktop platforms, this uses the QWindow::setWindowState() API to transition the window in and out of fullscreen when clicked. This does unfortunately not work out of the box on WebAssembly, since the Qt window is embedded in the web page and does not control the browser window. We need to find a different solution.
Slate有一个全屏按钮。在桌面平台上,这使用QWindow::setWindowState()API在单击时将窗口转换为全屏。不幸的是,这在WebAssembly上无法开箱即用,因为Qt窗口嵌入在网页中,并且无法控制浏览器窗口。我们需要找到不同的解决方案。
The web platform provides API for transitioning the browser window in and out of fullscreen state, which is relatively easy to call from C++ using emscripten::val. If you are unfamiliar with emscripten::val, then the short description is that it provides support for accessiong the DOM and native JavaScript API from C++ code.
web平台提供了用于将浏览器窗口转换为全屏状态和全屏状态的API,使用emscripten::val从C++调用相对容易。如果您不熟悉emscripten::val,那么简单的描述就是它支持从C++代码访问DOM和本地JavaScript API。
#include <emscripten/val.h>
void toggleFullscreen() {
using emscripten::val;
const val document = val::global("document");
const val fullscreenElement = document["fullscreenElement"];
if (fullscreenElement.isUndefined() || fullscreenElement.isNull())
document["documentElement"].call("requestFullscreen");
else
document.call("exitFullscreen");
}
Warn on Unsaved Changes on Tab Close
关闭选项卡时警告未保存的更改
We’d like to warn the user if they are about to lose unsaved data when closing the application. On the web this can happen if the user closes the tab or the browser window, and also if they navigate away using the forward or back buttons.
如果用户在关闭应用程序时将丢失未保存的数据,我们将警告用户。在web上,如果用户关闭选项卡或浏览器窗口,或者使用前进或后退按钮离开,则可能发生这种情况。
The beforeunload event fires in these cases. Web pages can make the browser prompt the user before close by adding an event listener to this event. The recommended practice is to connect to beforeunload only if we actually want to block tab close, so that's what we do.
在这些情况下,预卸载事件触发。通过向该事件添加事件侦听器,网页可以使浏览器在关闭之前提示用户。建议的做法是,只有当我们确实想阻止选项卡关闭时,才连接到beforeunload,所以我们就是这样做的。
The wording of the dialog fixed and can't be changed by applications, in order to prevent web pages from impersonating the browser.
对话框的措辞已修复,应用程序无法更改,以防止网页模拟浏览器。
#include <emscripten/val.h>
#include <emscripten/bind.h>
void beforeUnloadhandler(emscripten::val event) {
// This event handler does not need to take any action in order to display the
// confirmation dialog; calling preventDefault() and setting event.returnValue
// is sufficient.
event.call("preventDefault");
event.set("returnValue", std::string(" "));
}
// Export event handler to JavaScript
EMSCRIPTEN_BINDINGS(app) {
function("beforeUnloadHandler", &beforeUnloadhandler);
}
void enableTabCloseConfirmation(bool enable) {
using emscripten::val;
const val window = val::global("window");
const val eventHandler = val::module_property("beforeUnloadHandler");
if (enable)
window.call("addEventListener", std::string("beforeunload"), eventHandler);
else
window.call("removeEventListener", std::string("beforeunload"), eventHandler);
}
Implementation and usage in Slate.
在Slate中的实现和使用。
Conclusion
结论
With this Slate should be a usable web application with the usual web app properties; users can access it without installing anything (other than a browser), developers can push application updates directly to all users without any intermediate steps such as app store reviews or code signing requirements.
有了这个Slate,应该是一个具有常见web应用属性的可用web应用程序;用户无需安装任何东西(浏览器除外)就可以访问它,开发人员可以直接向所有用户推送应用程序更新,而无需任何中间步骤,如应用商店审查或代码签名要求。
Have you encountered other challenges when targeting Qt for WebAssembly? Are there additional features you would like to see in Qt or in helper libraries like Qt Web Utils? Let me know in the comments below.
您在为WebAssembly定位Qt时遇到过其他挑战吗?您希望在Qt或Qt Web Utils等帮助程序库中看到其他功能吗?请在下面的评论中告诉我