Icons In Qt Quick Controls: Difference between revisions
No edit summary |
m (Fix code formatting) |
||
(9 intermediate revisions by 3 users not shown) | |||
Line 1: | Line 1: | ||
=Problem: No easy way to specify an icon for e.g. Button and have the correct dimension and | = 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: | What should happen is: | ||
# An icon bitmap for the logical dimensions of the Button is chosen. We might default to | # 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 | # 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. | # 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 | 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 | 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" | |||
==Inherently scalable icons== | File selectors have been discussed, but are [http://lists.qt-project.org/pipermail/development/2013-March/010564.html 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. | There are other approaches that are inherently scalable, but come with their own drawbacks. | ||
===1. Use Canvas.=== | === 1. Use Canvas. === | ||
====Advantages:==== | ==== Advantages: ==== | ||
* Colour can be changed at runtime | * Colour can be changed at runtime | ||
* Can use HTML5 Canvas techniques off the Internet | * Can use HTML5 Canvas techniques off the Internet | ||
* Full control over when we repaint as long as you | * 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/) | * Improvements to performance being made all the time (the latest as of writing being https://codereview.qt.io/#/c/95937/) | ||
====Disadvantages:==== | ==== 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 | === 2. Use SVGs. === | ||
====Advantages:==== | ==== Advantages: ==== | ||
* We can take them straight from the designer and whack them in. | * 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 | * We get the same performance as Image after the initial rendering as long as we don't change the [http://doc.qt.io/qt-5/qml-qtquick-image.html#sourceSize-prop sourceSize] | ||
* We have control over the above (scaling vs re-rasterising) via the sourceSize property | * We have control over the above (scaling vs re-rasterising) via the sourceSize property | ||
* Works with Image, so will work with things like MenuItem | * Works with Image, so will work with things like MenuItem | ||
====Disadvantages:==== | ==== Disadvantages: ==== | ||
* libQtSvg dependency for both controls modules | * libQtSvg dependency for both controls modules | ||
* Re-rasterising is expensive (but how expensive, e.g. compared to Canvas?), but you | * 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 ( | === 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:==== | ==== Advantages: ==== | ||
* No libQtSvg dependency | * No libQtSvg dependency | ||
* Harnesses the power of text layouting/rendering engines | * Harnesses the power of text layouting/rendering engines | ||
====Disadvantages:==== | ==== Disadvantages: ==== | ||
* | * Doesn't work with Image, hence won't work with MenuItem | ||
* More work required to convert (but how much?) | * More work required to convert (but how much?) | ||
* Uses text layouting/rendering just to display an icon (Gunnar said | * 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 = | |||
# It has to provide a | == 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 enabled, disabled, active, selected, etc. states; the Item in use should specify its state | ||
# It needs to support different file formats, not just | # It needs to support different file formats, not just PNG == | ||
The following solutions are the result of brainstorming; | 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:== | == Solution #1: == | ||
QQmlAbstractUrlInterceptor (http://doc.qt.io/qt-5/qqmlabstracturlinterceptor.html) | QQmlAbstractUrlInterceptor (http://doc.qt.io/qt-5/qqmlabstracturlinterceptor.html) | ||
Line 78: | Line 92: | ||
Pass full path to non existent file, e.g.: | Pass full path to non existent file, e.g.: | ||
/home/user/myapp is checked for folders named | 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. | 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 | 1. Choose the smaller image and centre it | ||
2. Choose the larger image and down-scale it | |||
for the sake of this example, the | 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:==== | ==== Advantages: ==== | ||
* Happens automatically, no effort needed from user besides organising directories (this could be toolable) | * Happens automatically, no effort needed from user besides organising directories (this could be toolable) | ||
====Disadvantages:==== | ==== Disadvantages: ==== | ||
* Filesystem reads | * 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 | * Path to non-existent file is a bit magic; could be a better way of describing the "base" URL | ||
====Conclusion:==== | ==== Conclusion: ==== | ||
Not feasible. | Not feasible. | ||
==Solution #2: IconSet.qml== | == 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" | |||
} | |||
</code> | |||
====Advantages:==== | ==== Advantages: ==== | ||
* Easily toolable | * Easily toolable | ||
* Doable by hand if people dislike using tooling for whatever reason | * Doable by hand if people dislike using tooling for whatever reason | ||
* Can be used with QFileSelector | * Can be used with QFileSelector | ||
* No checking if folders exist; the only IO involved is the | * No checking if folders exist; the only IO involved is the "@2x" check | ||
====Disadvantages:==== | ==== Disadvantages: ==== | ||
* A bit of work to set up | * A bit of work to set up | ||
* A bit verbose, but you | * 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:==== | ==== Conclusion: ==== | ||
Too verbose and too much work for the user. | Too verbose and too much work for the user. | ||
==Solution #3: Hijack iconName (in Button, MenuItem, etc.)== | == Solution #3: Hijack iconName (in Button, MenuItem, etc.) == | ||
Same directory structure as Solution #1. | Same directory structure as Solution #1. | ||
====Advantages:==== | ==== Advantages: ==== | ||
* | * It's declarative | ||
* Since the Items own the properties, | * Since the Items own the properties, it's not necessary to specify the physical size of the icon | ||
====Disadvantages:==== | ==== Disadvantages: ==== | ||
* Behaviour change on Linux (apparently the only place where iconName is actually used | * 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 | * 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:==== | ==== Conclusion: ==== | ||
Suitable. | Suitable. | ||
=Notes About Chosen Solution= | = Notes About Chosen Solution = | ||
We think that #3 is the best. | We think that #3 is the best. | ||
The bitmap approach is good for icons, because it ensures that they are pixel perfect, which you | 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. | 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: | We may need a platform-specific variation: | ||
"android/icons/48x48/pencil.png" | |||
Default path is | 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 | qtquickcontrols/src/controls/plugin.cpp | ||
Button, ToolButton, MenuItem | 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) | QQuickImageProvider asks QIcons to return a pixmap for the given state (provided by the control) |
Latest revision as of 03:09, 11 October 2022
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)