Dial and StyleItem
As I promised, I'm starting a new series of articles dedicated to QtQuick. When I started playing with QtQuick and the QtQuick Controls module, I noticed that it implements QtQuick equivalents of most widgets, and Dial is one of the few that are missing. Perhaps QDial is not a very frequently used widget, but in certain kinds of QtQuick applications, especially for mobile devices, a dial control can come in handy:
A bit of googling quickly revealed that a Dial control exists as an experimental part of the QtQuick Controls module (you can find it here), but for whatever reason it never made it into the official package.
Here's my final version of Dial.qml after some tweaking:
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Controls.Private 1.0
Control {
id: root
width: 100
height: 100
property alias minimumValue: range.minimumValue
property alias maximumValue: range.maximumValue
property alias value: range.value
property alias stepSize: range.stepSize
property alias pressed: mouseArea.pressed
property bool notchesVisible: false
property real notchSize: 1
property bool activeFocusOnPress: false
activeFocusOnTab: true
Accessible.role: Accessible.Dial
Accessible.name: range.value
Keys.onLeftPressed: { range.value -= range.stepSize; }
Keys.onRightPressed: { range.value += range.stepSize; }
RangeModel {
id: range
minimumValue: 0
maximumValue: 1
stepSize: 0
value: 0
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onPositionChanged: {
if ( mouseArea.pressed )
range.value = mouseArea.valueFromPoint( mouseArea.mouseX, mouseArea.mouseY );
}
onPressed: {
range.value = mouseArea.valueFromPoint( mouseArea.mouseX, mouseArea.mouseY );
if ( root.activeFocusOnPress )
root.focus = true;
}
onWheel: { range.value += wheel.angleDelta.y * range.stepSize / 120.0; }
function valueFromPoint( x, y ) {
var yy = root.height / 2 - y;
var xx = x - root.width / 2;
var angle = ( xx || yy ) ? Math.atan2( yy, xx ) : 0;
if ( angle < -Math.PI / 2 )
angle += 2 * Math.PI;
var dist = 0;
var minv = range.minimumValue;
var maxv = range.maximumValue;
if ( minimumValue < 0 ) {
dist = -range.minimumValue;
minv = 0;
maxv = range.maximumValue + dist;
}
var v = ( minv + ( maxv - minv ) * ( Math.PI * 4 / 3 - angle ) / ( Math.PI * 10 / 6 ) );
if ( dist > 0 )
v -= dist;
return Math.max( range.minimumValue, Math.min( range.maximumValue, v ) );
}
}
style: Qt.createComponent( "DialStyle.qml" )
}
Note: for copyright purposes, the code snippets are licensed under the BSD-style open source license, just like the original code.
The code is very straightforward. The internal RangeModel object is used for handling minimum, maximum and step values (the Slider uses it too). The valueFromPoint() function converts the position of the mouse to the angle and then to the actual value. I renamed "tickmarksEnabled" to "notchesVisible" and added "notchSize" for compatibility with QDial. I also removed "wrapping", because the underlying StyleItem doesn't support it anyway. Finally, I added the missing mouse wheel and keyboard support.
What about drawing the control? The original Dial contains a StyleItem, another internal component of QtQuick Controls, which uses the application's default QStyle to paint items so that they look just like widgets. Now, a little digression. Qt5 has two separate UI modules - QtGui for handling windows and QtWidgets which implements the widgets. The former can be used without the latter, for example if your applications uses OpenGL or QtQuick or a custom painting engine and doesn't need widgets. By excluding widgets, you can reduce dependencies by a few megabytes.
The QtQuick Controls can work perfectly fine without QtWidgets. However, in that case they are drawn using built-in bitmap images, and they look the same on all platforms. On the other hand, when QtWidgets are enabled, the QtQuick Controls use the StyleItem components, which emulate the "native" look of widgets on the given platform. To be more specific, the style used by QtQuick Controls depends on whether you are using QGuiApplication or QApplication (the former is part of QtGui; the latter is part of QtWidgets). This information is deeply buried in the Qt documentation, so it can be quite confusing. If you dig into the Qt sources, you will find that the QtQuick.Controls.Styles module actually contains two subdirectories: "Base" with platform-independent styles and "Desktop" with ones that use StyleItem. The internal QQuickControlSettings class is used to select the right styles at run-time, and you can even use the "QT_QUICK_CONTROLS_STYLE" environment variable to use an entirely different set of styles.
Back to the main topic, embedding a StyleItem directly in the Dial is not a good idea, because it only works with the QtWidgets module and it's not possible to implement a custom or platform-independent style. That's why I changed the code to use the "style" property of the "Control" component, decoupling the style from the control, just like other QtQuick Controls do. The default implementation of the style is based on the original code, so it uses the StyleItem to do the job:
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Controls.Private 1.0
Style {
property Component panel: StyleItem {
id: styleItem
property real visualPos: control.maximumValue - control.value
property real granularity: {
if ( ( control.maximumValue - control.minimumValue ) < 10 )
return 100;
else if ( ( control.maximumValue - control.minimumValue ) > 1000 )
return 1 / Math.ceil( ( control.maximumValue - control.minimumValue ) / 1000 );
else
return Math.floor( 1000 / ( control.maximumValue - control.minimumValue ) );
}
elementType: "dial"
activeControl: control.notchesVisible ? "ticks" : ""
hasFocus: control.focus
enabled: control.enabled
sunken: control.pressed
maximum: control.maximumValue * styleItem.granularity
minimum: control.minimumValue * styleItem.granularity
value: styleItem.visualPos * styleItem.granularity
step: Math.ceil( control.notchSize * styleItem.granularity )
Behavior on visualPos {
enabled: !control.pressed
NumberAnimation {
duration: 300
easing.type: Easing.OutSine
}
}
}
}
This code simply passes information from the control to the StyleItem. The only interesting part is the "granularity" property. The original code uses a hard-coded value of 100 instead. By increasing the number of steps internally, it's possible to have a nice looking, smooth animation when the Dial rotates. However, the hard-coded value of 100 doesn't always work, because the internal implementation of QStyle only paints the notches correctly if the total number of steps doesn't exceed 1000. That's why my code uses a dynamic granularity instead which works correctly in most cases.
A very simple, platform-independent implementation of the style could use a rotating image instead:
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Controls.Private 1.0
Style {
property Component panel: Image {
id: image
property real visualPos: control.value
source: control.pressed ? "dial-on.png" : "dial.png"
rotation: 180 * 7 / 6 + ( image.visualPos - control.minimumValue ) * ( 180 * 10 / 6 )
/ ( control.maximumValue - control.minimumValue )
Behavior on visualPos {
enabled: !control.pressed
NumberAnimation {
duration: 300
easing.type: Easing.OutSine
}
}
}
}
In order to create the notches, you can place a small Rectangle in a Repeater and use appropriate rotation.
- Read more about Dial and StyleItem
- Log in to post comments