Fun with Paths and URLs in QML Managing Your QML Assets with Ease
There are a few small, and sometimes already quite old, features in Qt that, when combined, can be a very nice way to deal with assets in your QML application — especially if some of them live on the file system, some in a resource, and some may need localization or translation. Let’s dive in!
QDir’s searchPaths Feature
There is a seldom-used feature in QDir: to have it search for your files in a variety of places. You register a custom file prefix together with a list of possible paths where files with this prefix may be found. Then, whenever you need such a file, you simply use the custom prefix. Qt will locate the file for you in any of the locations you told it to look. You use QDir’s static methods setSearchPaths and addSearchPath to setup such special paths, after which you can utilize the new prefix using the QFile (and related) API’s.
QQmlAbstractUrlInterceptor
QQmlAbstractUrlInterceptor is a simple interface class from which you can inherit to implement a class that gets to intercept all* URLs used in QML, by the engine itself in resolving paths to QML files or dirs and also when setting Image or SoundEffect sources, for instance. You implement a function that takes a QUrl and returns a QUrl, allowing you to manipulate the URL in the process. You can then install the interceptor on the QML engine by using addUrlInterceptor on Qt6 or setUrlInteceptor on Qt5 (undocumented, but does not require really private API). Warning: The Qt5 version can clash with Qt classes using the same mechanism, such as FileSelector. The Qt6 version that allows for multiple interceptors to be installed should not have that issue.
The Problem
In a customer project, we had two problems. On the one hand, we had some resources like icons stored in resource files, while we had others like video files and sound effects stored on the file system. This meant many uses of either "qrc:///assets/<the asset>.png" or "file:///" + baseDir + "/assets/<the asset>.mp4," littered around the code. On top of this, some of these files needed to be localized, sometimes only on language and other times more on region. It was quite a messy situation and, also, not very flexible in changing between locations from which we want to use assets.
So, I figured I could use a combination of the above two to implement an elegant solution to this issue.
Building the `asset:` Schemes
Use QQmlAbstractUrlInterceptor to implement a custom URL-scheme I called “asset,” which would resolve to either an asset in the resources or an asset on the file system. Finding the files is easy because we can rely on the custom `asset:` file scheme, implemented using QDir‘s searchPaths feature described above. To do that, we register the search paths in the constructor of the interceptor:
class AssetUrlHandler : public QQmlAbstractUrlInterceptor { public: AssetUrlHandler() { QDir::setSearchPaths("asset",{":/assets", QGuiApplication::applicationDirPath() + "/assets"}); } //... }
We need to then re-implement the actual intercept call, which, in a basic version, would look like this:
QUrl AssetUrlHandler::intercept(const QUrl& path, QQmlAbstractUrlInterceptor::DataType type) { if (type == QQmlAbstractUrlInterceptor::DataType::QmldirFile) return path; // no need to lookup these files; this is about assets, not about QML files auto scheme = path.scheme(); if (scheme == QLatin1String(assetScheme)) { QFileInfo fi("asset:" + path.mid(1)); if (fi.exists()) { if (fi.filePath().startsWith(":/")) { // we need to deal with files in the resources by adding the url scheme for them return QUrl("qrc" + fi.filePath()); } return QUrl::fromLocalFile(fi.filePath()); } return {}; } //followed by other (related) schemes if needed }
After installing an instance of the interceptor on the QmlEngine, we can change our QML from code like this:
SoundEffect { id: someEffect source: "file:///" + baseDir + "/assets/someEffect.wav" } //... Image { source: "qrc://assets/images/niceImage.png" }
to code like this:
SoundEffect { id: someEffect source: "asset:/sounds/someEffect.wav" } //... Image { source: "asset:/images/niceImage.png" }
Our URL interceptor handles actually locating the asset in the resources or the file system.
Localized Assets
Then, we also added a second scheme I called “localizedAsset” (casing doesn’t matter, but this is easier to read) that will try to find a localized variant of the requested asset, by either using an explicit locale passed in via a query parameter or using the current locale from the application settings. It will try a progressively more generic version of the file, starting from the full locale inserted as an additional suffix between the base name and the original suffix, via simply using the country or the language, down to just using the file itself as a fallback — again in either the resources or the file system.
For example, instead of just looking for myAsset.png, looking for a localized asset in QML with the locale set to german/Germany for language and country would first look for myAsset.de_DE.png, then for myAsset.de_EU.png (we’re treating the European Union (EU) as a special meta-country in our code), then for myAsset.DE.png, myAsset.EU.png, myAsset.de.png and, finally, as a default fallback using just myAsset.png. Though we could have chosen to use directories, we didn’t because that would have complicated the distribution of the application and spread out the related assets over multiple locations. So, we settled on using the additional suffix instead.
The interceptor we implemented caches the paths it resolves, so especially localized assets don’t get hit with too many locations, so if the requested path is already known (and returns it if so), if not looks it up and inserts it into the cache.
The result is that the QML code has become much cleaner, and localizations are trivial to add as they require no further code changes. We can also decide later where to best place a resource without changing the code where it is used.
Additional Benefits
We actually found that the asset scheme can make automatic tests a bit easier at times. By adding an additional constructor to the AssetUrlHandler that allows one to pass an additional resource path that is searched first, it becomes easier to write tests that need access to some resources without having to have those resources in the normal locations for them. It’s a small benefit, but it made writing some tests easier for us.
Caveats
There is no such thing as a perfect solution, unfortunately, and there are a few snags to take into account:
First of all, the MultiMedia components don’t use the url interceptors to resolve the URLs they get passed. That’s a bug. It’s not so hard to work around though. Using Qt.resolvedUrl around the asset URL is enough.
Second, in Qt 5, using this results in some weirdness in what you get back, depending on how the URL got set on the property. In Qt 5, URLs in principle get resolved by the engine when they get set on a QUrl-type property. But there are many ways this can happen, and the behavior isn’t always consistent:
// property initialization Image { id: img1 source: "asset:/icons/icon.png" } // property binding Image { id: img2 source: someProperty ? "asset:/icons/icon1.png" : "asset:/icons/icon2.png" } // assign through state changes Image { id: img3 } PropertyChanges { target: img3; source: "asset:/icons/icon.png" } // assign in JavaScript Image { id: img4 Component.onCompleted: source = "asset:/icons/icon.png" }
We ran into some interesting issues while testing these properties with Squish:
img1.source === "qrc:///assets/icons/icon.png" img2.source === "asset:/icons/icon1.png" (or icon2.png) img3.source === "asset:/icons/icon.png" img4.source === "asset:/icons/icon.png"
So, that makes for some inconsistent results, depending on how the property was set. Again, Qt.resolvedUrl can be used as a work-around.
Qt 6 does not have this issue, as, in Qt 6, the URL resolving has been moved from the engine to the items, so that items can also get relative URLs. That does mean that if you have custom C++ based items that have URL properties, your item is responsible for resolving the URLs passed in (and for that: should be using any URL interceptors installed on the QML engine), and not just if you use the asset handling system from this blog or you will have to start adding Qt.resolvedUrl calls in your QML.
Android
On Android, Qt provides the special “assets:/” file system, which maps to the Android system of packaging assets in a special directory in your package. The difference is just in the last ‘s’ of the name of the url scheme and file system, and this can be confusing.
*) All URLs? No, see the Caveats section for details.
If you like this article and want to read similar material, consider subscribing via our RSS feed.
Subscribe to KDAB TV for similar informative short video content.
KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.
URL interception is a rather dangerous feature in Qt6 and I regret making it publicly usable. The original idea was to use it with QtQuick.Controls, but in the end it turned out impossible to do in a safe way. QtQuick.Controls uses optional imports and qmldir import statements now, which is a much better solution.
If you intercept URLs of QML files, you cannot compile the QML files on which you do that to C++. Otherwise, you may end up with a mismatch between qmlcachegen’s type analysis and the types at run time. That’s dangerous and will probably crash your application.
We still need to find a generic way to prohibit such usage.
Intercepting URLs of images and other non-code assets is generally fine.
Hi Ulf,
Thanks for your comments. Note that this post is about dealing with assets: images, sound clips, video files… I am not advocating using this for messing with urls for QML files.
Great! It may be a good idea to state that in a more prominent place in the blog post itself. People will get ideas.
It’s right there, in the sub-header as well as in the first paragraph of the blog. Futhermore, none of the examples deal with manipulating the urls to QML files, and the example handler explicitly rejects modifying urls to QML files. I think it’s clear enough.