Icons In Qt Quick Controls
Problem: No easy way to specify an icon for e.g. Button and have the correct dimension and DPI chosen automatically
Currently, the following Button's icon will use the same PNG on every platform (besides OS X/iOS), regardless of the device DPI, etc.:
Button { iconSource: "pencil.png" }
What should happen is:
- An icon bitmap for the logical dimensions of the Button is chosen. We might default to 32x32, but if the button is larger, we could use a larger icon if it exists. If the button is in between these sizes, we should choose the larger icon and down-scale it.
- In addition to the above, the DPI of the device is respected. For example, we would choose "icons/32x32/pencil-@2x.png" on high DPI devices, "icons/32x32/pencil-@3x.png" on higher DPI devices, etc.
- The icon chosen should reflect the state of the button, whether that state is disabled, active, selected, etc.
This has been a problem for a while, and I'd like to address it in Qt 5.5. Hopefully people will correct me if I'm wrong anywhere in this page. Note that this only applies to items in Qt Quick Controls that use icons; e.g. Button, ToolButton, MenuItem.
On Android and most other platforms, the "@2x" thing is not implemented, so it's hard for us to switch out images depending on the resolution of the device; you'd have to do it manually:
iconSource: Screen.pixelDensity > someAmount ? "icon@2x.png" : "icon.png"
File selectors have been discussed, but are not the best approach. We can make it easier for developers.
Inherently scalable icons
There are other approaches that are inherently scalable, but come with their own drawbacks.
1. Use Canvas.
Advantages:
- Colour can be changed at runtime
- Can use HTML5 Canvas techniques off the Internet
- Full control over when we repaint as long as you don't resize
- Improvements to performance being made all the time (the latest as of writing being https://codereview.qt.io/#/c/95937/)
Disadvantages:
- Can't be used with Image, so won't currently work with MenuItem (which only accepts a path to an image for the icon)
- Repaints on resizing, but in practice this is not an issue, as most applications won't be resizing
- Means taking something tangible like an SVG file and turning it into a series of coordinates. On the other hand, this is could very easily be automated; it's just one line of XML that needs to be extracted, and in terms of files on a file system, it's the same as using SVGs:
images/ icon-1.svg icon-2.svg qml/ icon-1.qml icon-2.qml
2. Use SVGs.
Advantages:
- We can take them straight from the designer and whack them in.
- We get the same performance as Image after the initial rendering as long as we don't change the sourceSize
- We have control over the above (scaling vs re-rasterising) via the sourceSize property
- Works with Image, so will work with things like MenuItem
Disadvantages:
- libQtSvg dependency for both controls modules
- Re-rasterising is expensive (but how expensive, e.g. compared to Canvas?), but you shouldn't do it often anyway (ideally you paint once)
3. Create an icon font (TTF file, for example) and embed that into the QRC file, converting icons that we get from the designer into text glyphs (is that the correct term?).
Advantages:
- No libQtSvg dependency
- Harnesses the power of text layouting/rendering engines
Disadvantages:
- Doesn't work with Image, hence won't work with MenuItem
- More work required to convert (but how much?)
- Uses text layouting/rendering just to display an icon (Gunnar said "it feels wrong to do text layout to draw an icon")
- Can't colorise (Shawn said "the solution for color fonts is not resolved yet")
Solution: use different icons at different Item sizes and device DPIs
Solution Criteria
- It has to provide a URL (to work with Button and MenuItem, etc.).
A visual item like IconImage would work, but not with Button, MenuItem, etc., which have iconSource properties.
- It needs to support enabled, disabled, active, selected, etc. states; the Item in use should specify its state
- It needs to support different file formats, not just PNG ==
The following solutions are the result of brainstorming; "off the top of our head" stuff, with some quick follow-up research (Googling & grepping) for the more attractive ideas.
Solution #1:
QQmlAbstractUrlInterceptor (http://doc.qt.io/qt-5/qqmlabstracturlinterceptor.html)
Pass full path to non existent file, e.g.:
Button { iconSource: "/home/user/myapp/image.png". anchors.fill: parent // 40 x 40 }
/home/user/myapp is checked for folders named 32x32, etc. (similar to the freedesktop standard: http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html):
/home/user/myapp/ 32x32/ image.png image@2x.png 64x64/ image.png image@2x.png
our QIconUrlInterceptor must know the size of the Item so that it can choose the appropriate directory. in this case, there are two approaches we could take:
1. Choose the smaller image and centre it 2. Choose the larger image and down-scale it
for the sake of this example, the 32x32 size is chosen, so we enter that directory. a file matching the DPI is then searched for. for example, if the device is at the lowest DPI (whatever that is), the plain image.png file is chosen. if the dpi is twice the lowest, image@2x.png is chosen, and so on.
Advantages:
- Happens automatically, no effort needed from user besides organising directories (this could be toolable)
Disadvantages:
- Filesystem reads
- Can't QQmlEngine only have one interceptor at a time? E.g. what if someone is using QFileSelector?
- Path to non-existent file is a bit magic; could be a better way of describing the "base" URL
Conclusion:
Not feasible.
Solution #2: IconSet.qml
Have a QML file that describes which icons at which physical sizes are available. "@2x" logic from above is still used, assuming the files will be in the same directory as the appropriate physically sized image.
// IconSet.qml IconSet { basePath: "path/to/icons/" IconFile { path: "file-open-32x32.png" width: 32 height: 32 } IconFile { path: "file-open-64x64.png" width: 64 height: 64 } IconFile { path: "file-open-128x128.png" width: 128 height: 128 } }
Button { iconSource: IconSet.url("file-open", width, height) // If we can somehow let the url function know the Item that is calling it, // and specify the file name some other way, we could turn this into a property: iconSource: IconSet.url iconName: "file-open" }
Advantages:
- Easily toolable
- Doable by hand if people dislike using tooling for whatever reason
- Can be used with QFileSelector
- No checking if folders exist; the only IO involved is the "@2x" check
Disadvantages:
- A bit of work to set up
- A bit verbose, but you shouldn't be looking at this file outside of the tooling GUI
- It's a function, not a property; properties look nicer, but the function will still get called when the width/height changes
Conclusion:
Too verbose and too much work for the user.
Solution #3: Hijack iconName (in Button, MenuItem, etc.)
Same directory structure as Solution #1.
Advantages:
- It's declarative
- Since the Items own the properties, it's not necessary to specify the physical size of the icon
Disadvantages:
- Behaviour change on Linux (apparently the only place where iconName is actually used - by how many people, who knows)
- Still need to specify base path/directory containing the icons, which brings back the problem of Qt lacking a QML entry point (.json file or whatever) - it needs to be specified once and before any QML is loaded
Conclusion:
Suitable.
Notes About Chosen Solution
We think that #3 is the best.
The bitmap approach is good for icons, because it ensures that they are pixel perfect, which you can't guarantee with inherently scalable approaches (fuzzy pixels with small SVGs, distance field stops working for glyphs larger than 100x100 with icon font approach). Of course you can resize a button and cause the icon to be scaled down, but typically you place a Button on a UI without forcing a size for it; its size is determined by the style for the platform you're on and we'll choose the size that ensures no scaling occurs.
Down-scale when we have to, never scale up.
Button { iconName: "pencil" }
"icons/48x48/pencil.png" - default (normal, off), also fallback if none of the others are available "icons/48x48/pencil-on-disabled.png" "icons/48x48/pencil-disabled-on.png" (order doesn't matter) "icons/48x48/pencil-pressed-off.png"
We may need a platform-specific variation: "android/icons/48x48/pencil.png"
Default path is "qrc:/icons/" At first, we use an undocumented env variable QT_QUICK_CONTROLS_ICON_PATH to override this
qtquickcontrols/src/controls/plugin.cpp
Button, ToolButton, MenuItem normal, disabled, hovered, pressed, focused, selected (for view items), selected+disabled, pressed+selected Focus and Hovered = Active in QIcon
QQuickImageProvider asks QIcons to return a pixmap for the given state (provided by the control)