How to set initial value of a custom slider in qml? - qt

I am using Qt 5.4.1. I have made a custom slider element to be used in other qml components like so:
Slider.qml
import QtQuick 2.4
import QtQuick.Controls 1.3
import QtQuick.Controls.Styles 1.3
Item {
id: root
width: 150
height: 30
property int val: slider.value
property int maxVal: slider.maximumValue
property int minVal: slider.minimumValue
property int step: slider.stepSize
Slider {
id: slider
anchors.margins: 20
stepSize: step
maximumValue: maxVal
minimumValue: minVal
style: customStyle
// onValueChanged: print("From Slider.qml" ,value)
}
Component {
id: customStyle
SliderStyle {
handle: Rectangle {
width: 20
height: 12
antialiasing: true
color: Qt.lighter("#468bb7", 1.2)
}
groove: Item {
implicitHeight: root.height
implicitWidth: root.width
Rectangle {
height: 8
width: parent.width
anchors.verticalCenter: parent.verticalCenter
color: "#847878"
opacity: 0.8
Rectangle {
antialiasing: true
radius: 1
color: "#1a0d0d"
height: parent.height
width: parent.width * control.value / control.maximumValue
}
}
}
}
}
}
And in another file test.qml I am using this slider like so
test.qml
import QtQuick 2.3
Rectangle {
id: test
width: 640; height: 480
Slider {
id: slider
width: 300
height: 30
anchors.centerIn: parent
maxVal: 1000
minVal: 0
step: 50
val: 500 // when commented out, onValChanged is triggered on sliding
onValChanged: print(val)
}
}
I want to set the slider to an initial value when instantiated in test.qml, using the property val. But when I set initial value, onValChanged does not get triggered when sliding the slider. But when I comment that line out (val: 500), onValChanged is triggered when the slider is slid, but the slider starts with initial value of 0 which I don't want. I don't understand what I am doing wrong here!

The setting of the property val to a specific value overrides the binding, as defined in your slider component. Once the binding is lost, any update of the slider is not delivered to val resulting in the behaviour you experienced. On the other way around, if you don't set the property the binding is maintained, i.e. as the slider value changes the value of val changes accordingly, triggering the signal.
That's not the way to go in this case, also because you are adding a set of properties which simply exposes inner properties of Slider. Just use properties alias:
Property aliases are properties which hold a reference to another property. Unlike an ordinary property definition, which allocates a new, unique storage space for the property, a property alias connects the newly declared property (called the aliasing property) as a direct reference to an existing property (the aliased property).
Rewrite your properties inside Slider.qml as follows:
property alias val: slider.value
property alias maxVal: slider.maximumValue
property alias minVal: slider.minimumValue
property alias step: slider.stepSize
This way val is slider.value and setting it to 500 directly affect the slider without breaking any binding.
On a sidenote, you can also for example write
property alias maximumValue: slider.maximumValue
i.e. expose inner properties with their very same name to maintain the consistency in API naming.

Related

NumberAnimation does not take into account property change until stopped

I have a QML application with 2 rectangles: a large rectangle and a small rectangle inside the large one.
The small rectangle is animated and moves inside the large rectangle.
The animation is done by combining 2 NumberAnimation in a SequentialAnimation.
It works well, except that the to property of one of the NumberAnimation can change.
I would except the change of value to be applied immediately.
However, it is not taken into account until the animations are fully stopped and restarted.
Calling stop()/start() or restart() does not do anything.
I need to wait for the animation to actually finish and then start it again.
This can be demonstrated with the following QML code:
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Window {
width: 640
height: 480
visible: true
RowLayout {
Rectangle {
id: topRect
width: 400
height: 400
border {
color: "red"
width: 2
}
Rectangle {
id: animatedRectangle
width: 100
height:100
color: "blue"
}
SequentialAnimation{
id: animation
loops: Animation.Infinite
running: cbAnimate.checked
alwaysRunToEnd: true
NumberAnimation {
id: forwardAnimation
target: animatedRectangle
property: "x"
to: sbWidth.value
duration: 2000
}
NumberAnimation {
id: backwardAnimation
target: animatedRectangle
property: "x"
to: 0
duration: 2000
}
}
}
ColumnLayout {
CheckBox {
id: cbAnimate
text: "Animate"
}
SpinBox {
id: sbWidth
value: 300
to: 400
}
SpinBox {
value: forwardAnimation.to
to: 999
}
}
}
}
Start the animation with the checkbox
Change the value of to with sbWidth
See in the other SpinBox that the value of to was changed
Observe that the animation is still using the old value
Stop the animation, wait for the Rectangle to stop moving, Start the animation
Observe that the animation is using the value set in step 2
Isn't there a way to make the animation use the new value of to immediately?
This behavior is particularly painful when a QML element is animated by default and the to value depends on the geometry of Items, as during the creation of the QML scene Qt will create and then resize Items. Meaning that animation started at creation time won't get their values updated during the resize.
TLDR
In this particular case the best solution is to use the workaround suggedted by #stephen-quan: animate a proxy property property real animatedX between 0.0 and 1.0. And then bind the property I want to animate to this animated property and do the extra computation in this binding: x: animatedX * sbWidth.value. Eliminating the need of changing the to property of the animation.
Details
The issue of animations not taking property change into account until restarted is a very old issue. It has been reported numerous times:
PropertyAnimation ignores from/to changes
Changing an Animation
duration has no effect
Since Qt 6.4, the state has slightly improved. Quoting Qt documentation:
Since Qt 6.4, it is possible to set the from, to, duration, and easing properties on a top-level animation while it is running. The animation will take the changes into account on the next loop.
However, it still does not affect the current loop and requires the animation to be top-level. So even with this improvement, I still need to animate a proxy property, ensuring changes are taken into account in real-time.
I made various changes to your sample.
I introduced from: 0 to your first NumberAnimation. This ensures that whenever you stop/start the animation, it will reset. Also, removing alwaysRunToEnd helps with that.
I introduced a new property double val: 0 property which will range from 0.0 to 1.0. This is what I used NumberAnimation on instead of x. The advantage is, we know that the NumberAnimation will happily move from 0.0 to 1.0 and back to 0.0 consistently.
Then, I introduced a formula linking val to x and takes into account of sbWidth.value.
To make it easier to change sbWidth.value I changed it from a SpinBox to a Slider.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
RowLayout {
Rectangle {
id: topRect
width: 400
height: 400
border {
color: "red"
width: 2
}
Rectangle {
id: animatedRectangle
property double val: 0.0
x: val * sbWidth.value
width: 100
height:100
color: "blue"
Label {
anchors.centerIn: parent
text: parent.x.toFixed(2)
color: "white"
}
}
SequentialAnimation{
id: animation
loops: Animation.Infinite
running: cbAnimate.checked
//alwaysRunToEnd: true
NumberAnimation {
id: forwardAnimation
target: animatedRectangle
property: "val"
from: 0
to: 1.0
duration: 2000
}
NumberAnimation {
id: backwardAnimation
target: animatedRectangle
property: "val"
to: 0
duration: 2000
}
}
}
ColumnLayout {
CheckBox {
id: cbAnimate
text: "Animate"
}
Slider {
id: sbWidth
value: 300
from: 100
to: 400
}
Text {
text: sbWidth.value.toFixed(2)
}
}
}
}
You can Try it Online!

ListModel in repeater

When using a ListModel for a repeater, if a property is not set in the first element of the model, then it is not considered in the following elements. Why?
import QtQuick 2.7
import QtQuick.Controls 2.3
Item{
id: root
property var labels: ListModel{}
Button{
text: 'create labels'
onClicked:{
root.labels.append({})
root.labels.append({name: '2'})
root.labels.append({name: '3'})
}
}
Column{
x: 10
y: 200
spacing: 2
Repeater{
model: root.labels
Button{
width: 120
height: 30
text: model.name
}
}
}
}
This code is ok:
....
onClicked:{
root.labels.append({name: '1'})
root.labels.append({})
root.labels.append({name: '3'})
}
....
It doesn't work because the roles of the ListModel get evaluated based on its first element. The property of the first element defines the roles of the model. If you had other properties in the following elements, those will be ignored.
That behavior is the default one when the dynamicRoles property is not set to true.
When set to true, the model will recalculate its roles for each inserted element and emit a modelReset every time the roles change. This is costly and not generally needed so its disabled by default.

QML - setting width and height has no effect

Quick Controls 2, Qt 5.10.
I created table control based on ListView item.
One of its columns is displayed using this component:
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
Item
{
id: root
implicitHeight: item1.implicitHeight
ColumnLayout
{
id: item1
visible: !model.finished
width: parent.width
RowLayout
{
Label
{
text: "38%"
Layout.alignment: Qt.AlignLeft
}
Label
{
text: "Paused"
Layout.alignment: Qt.AlignRight
}
}
ProgressBar
{
from: 0; to: 100; value: 40
// Variant A
/*Layout.preferredWidth: 30
Layout.preferredHeight: 10*/
// Variant B
width: 30
height: 10
}
}
}
Can somebody please explain me why Variant B does not "work". I may specify any width/height values or even just remove them - no effect. Variant A (Layout.preferredWidth/Layout.preferredHeight) works fine.
Variant A:
Variant B:
The ...Layout items alter the dimensions of their children. That is their purpose, and the behavior is documented.
As per documentation of the ColumnLayout Layout.preferredWidth the behavior is:
This property holds the preferred width of an item in a layout. If the preferred width is -1 it will be ignored, and the layout will use implicitWidth instead. The default is -1.
Since the default is -1, it will take the implicitWidth - it is not written "and use width instead".
If you don't want to use Layout don't use Layout. You can just take Column instead.

ListView signals and slots for menu elements

I'm trying to implement some sort of custom Menu with custom elements. The ultimate goal is to create some sort of popup menu with text and icons. But during creation I faced with some issues. I can show 2 primary problems:
There is a strange menu element with title Hello world at the first position (looks like it's read title of application window):
From time to time I'm getting errors like qrc:/BreezeQuickMenu.qml:45: TypeError: Property 'clicked' of object QQuickListView(0x1120830) is not a function
Here is my actual code:
main.qml
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Window 2.2
ApplicationWindow {
title: qsTr("Hello World")
width: Screen.width
height: Screen.height
visible: true
id: win
color: brPalette.normalBackground
BreezeQuickMenu{
id: brMenu
x: 490
y: 199
width: 128
height: 256
palette: brPalette
menuFont.pointSize: 16
BreezeQuickMenuItem{
title: "Item 1"
onClicked: mbox.show()
}
BreezeQuickMenuItem{
title: "Item 2"
}
BreezeQuickMenuItem{
title: "Item 3"
}
}
}
BreezeQuickMenu.qml
import QtQuick 2.4
Item {
id: root
property BreezeQuickPalette palette: BreezeQuickPalette
property alias currentIndex: menuList.currentIndex
property font menuFont
property bool menuVisible: false
implicitWidth: 128
implicitHeight: menuList.height
ListView{
id: menuList
anchors.fill: parent
model: root.children
clip: true
delegate: Component {
id: menuItem
Rectangle {
id: menuElement
property bool isCurrentItem: ListView.isCurrentItem
anchors {
left: parent.left
right: parent.right
}
color: palette.normalBackground
height: menuText.font.pixelSize*1.2
Text {
id: menuText
anchors.fill: parent
text: title
color: palette.normalText
font: menuFont
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
menuList.currentIndex = index
menuList.model[index].clicked()
}
}
}
}
}
}
BreezeQuickMenuItem.qml
import QtQuick 2.4
Item {
id: root
property string title: "Menu Element"
signal clicked
}
As you can see I'm trying to implement menu list and menu items with their own signals. I have 2 questions:
how can I properly get rid of using title property of parent element, since I need to read title property of childrens
what is the correct approach of using signals and slots in menu elements to avoid above error?
Please help me to understand. Full project can be pulled here:
git clone git://git.code.sf.net/p/breezequick/code breezequick-code
The problem with the signal is related to its declaration. Signals are always declared as a function would be: with a signature. In other words, a signal without parameters has the form
signal <signal_name>()
That's also why you got the error "is not a function". Apart from that, the usage of signals/signal handlers is correct. Anyhow, reading carefully the documentation wouldn't hurt. This page covers in detail the argument.
Coming to the other problem, you made the wrong assumption: anything that is declared inside a component is part of the children of the component itself. Here you declared a BreezeQuickMenu which has a child ListView. When you use it and add the BreezeQuickMenuItems, you add them to the same set to which the ListView belongs. In the end you have four elements in the children property. Also, by adding the ListView to itself through the model you mess up things to the point that a totally unrelated string is rendered.
There are several ways to handle Items as model members for a view, inclusing VisualItemModel and using object Instanced as models. However, by skimming your code, it is clear that you want to define a component which adds menu items in a declarative fashion. Using children is not sufficient in this case. You also need the default property:
An object definition can have a single default property. A default property is the property to which a value is assigned if an object is declared within another object's definition without declaring it as a value for a particular property.
Hence you can define the default property for your BreezeQuickMenu and exploit it to obtain the desired children for your list. A common approach would be the following (code simplified):
import QtQuick 2.4
Item {
id: root
property BreezeQuickPalette palette: BreezeQuickPalette
property alias currentIndex: menuList.currentIndex
// default declaration (1)
default property alias contents: addItem.children
// Item to which the inner declared meantime will belong (2)
Item {
id: addItem
}
property font menuFont
property bool menuVisible: false
implicitWidth: 128
implicitHeight: menuList.height
ListView{
id: menuList
anchors.fill: parent
model: contents // usage of the default property (3)
clip: true
delegate: Rectangle {
// your current delegate code
}
}
}
The basic idea is to exploit also property alias: basically in (1) we are saying that "all the Items declared inside BreezeQuickMenu are automatically children of addItem which is an inner declared Item (2). In this way the ListView is kept apart whereas all the BreezeQuickMenuItem are gathered together, under addItem children property. At this point, it is sufficient to use the same children property as the model (3) for the ListView and that's it.

Building TabBar in QML - Loader doesn't show all the Rectangles

import QtQuick 2.4
import QtQuick.Window 2.2
Window
{
visible: true
height: 500
width: 500
property VisualItemModel contentToBeShownOnTabClick : visualItemModelDemo
property variant tabLabels : ["Navigation", "Payload", "System Control"]
VisualItemModel
{
id: visualItemModelDemo
Rectangle
{
id: navigationTab
color: "green"
height: 200
width: 200
}
Rectangle
{
id: navigationTab1
color: "darkgreen"
height: 200
width: 200
}
Rectangle
{
id: navigationTab2
color: "lightgreen"
height: 200
width: 200
}
}
MainForm
{
Component
{
id: tabsOnBottomComponent
Repeater
{
model: tabLabels
// The Tabs
Rectangle
{
id: tabsOnBottom
// This anchoring places the tabs on the outer top of the parent rectangle.
anchors.top: parent.bottom
anchors.topMargin: 180
color: "lightsteelblue"
border.color: "steelblue"
border.width: 2
implicitWidth: Math.max ((labelTabsBottom.width + 4), 80)
implicitHeight: 20
radius: 2
// Tabs Text/Label
Text
{
id: labelTabsBottom
anchors.centerIn: parent
color: "white"
rotation: 0
// With reference to mode: tabLabels
text: modelData
font.pointSize: 11
}
MouseArea
{
anchors.fill: parent
onClicked: bottomTabClicked (index);
}
}
}
}
Rectangle
{
// The things which get displayed on clicking of a tab will be shown in this rectangle.
id: areaForTabContents
border.color: "black"
border.width: 10
height: parent.height
width : parent.width
color : "pink"
// These are the tabs displayed in one row - horizontally.
Row
{
id: horizontalTabs
Loader
{
anchors.fill: parent
sourceComponent: tabsOnBottomComponent
}
}
}
anchors.fill: parent
}
}
This gets shown as follows:
whereas I want it to see 3 rectangles there side by side.
Loader is not a transparent type w.r.t. the containing type, Row in this case. I think this is an issue related to creation context and the way Repeater works. From the documentation of the latter:
Items instantiated by the Repeater are inserted, in order, as children of the Repeater's parent. The insertion starts immediately after the Repeater's position in its parent stacking list. This allows a Repeater to be used inside a layout.
The Rectangles are indeed added to the parent which is the Loader, they stack up - Loader does not provide a positioning policy - then they are added to the Row resulting in just one Item (the last one) to be visible.
You can tackle the problem with few different approaches, depending on the properties you want to maintain or not. I would get rid of anchoring in the Component and move it to the containing Row. A too specific anchoring inside a Component could be a pain in the neck when it is instanced and used all over a (not so small) project.
As a first approach you can re-parent the Repeater to the Row, i.e. you can rewrite code as:
Row
{
id: horizontalTabs
Loader
{
sourceComponent: tabsOnBottomComponent
onLoaded: item.parent = horizontalTabs
}
}
However this would result in warnings due to the Component anchoring references not working as expected any more.
If you still want to maintain the anchoring, as defined in the Component, and off-load the creation, you can go for the dynamic way (if the semantics fits in your use case), i.e. you can use createObject. This way you totally avoid the Loader and the related issue. For instance, you can create the Repeater once the Row has completed its creation:
Row
{
id: horizontalTabs
Component.onCompleted: tabsOnBottomComponent.createObject(horizontalTabs)
}
Clearly, the creation code can be move anywhere else, depending on your needs.

Resources