I have a somehow very hard to solve problem in my QML code. I will summarize it the best I can since it is very long code..
I write a color picker qml file that is called when the user wants to pick a color. This is done in a big rectangle with little rectangles in it evenly distributed that have flat colors to choose from.
I have a parent rectangle, 1 outer repeater and nested in this repeater is another inner repeater that creates little rectangle in a row. The outer repeater places the inner repeaters under another so it fills the rectangle with little rectangles fully, preferably with different colors.
Every little rectangle also has a function that highlights itself with an animation. This animation is a circle that gets bigger than the rectangle itself. This is done so when the user clicks a color from e.g. a color history on the right, it should highlight the corresponding colors rectangle if is there.
Now, the problem:
No matter what z values I use, this animation won't show above the other rectangles. It will get blocked by neighboring rectangles. I have researched and it seems that z values don't account for non siblings, just for all items in a single parent.
Here's some code that leaves out all the unnecessary junk.
To note is that every rectangle has its own animation and mousearea.
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
color: 'black'
width: 640
height: 480
title: qsTr("Hello World")
Rectangle {
id: parentRectangle
width: 400
height: 400
property int numberOfBoxesInARow: 5
property int numberOfBoxesInAColumn: 5
Repeater {
id: outerRepeater
model: parentRectangle.numberOfBoxesInARow
Repeater {
id: innerRepeater
model: parentRectangle.numberOfBoxesInAColumn
y: parentRectangle.height / parentRectangle.numberOfBoxesInAColumn * outerIndex
x: 0
height: parent.height / parentRectangle.numberOfBoxesInAColumn
width: parent.width
property int outerIndex: index
Rectangle {
id: individualRectangle
color: Qt.rgba(1 / (outerIndex + 1), 0, 1 / (index + 1), 1)
x: parentRectangle.width / parentRectangle.numberOfBoxesInARow * index
y: outerIndex * parentRectangle.height / parentRectangle.numberOfBoxesInAColumn
width: parentRectangle.width / parentRectangle.numberOfBoxesInARow
height: parent.height / parentRectangle.numberOfBoxesInAColumn
Component.onCompleted: {
console.log("Rectangle at " + outerIndex + "|" + index + " created, width / height: " + width.toFixed(2) + "| " + height.toFixed(2))
}
MouseArea {
anchors.fill: parent
onClicked: {
highlightAnimation.running = true
}
}
Rectangle {
id: highlightCircle
visible: highlightAnimation.running
color: 'white'
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
property real size: 0
width: size
height: size
radius: size/2
}
PropertyAnimation {
id: highlightAnimation
target: highlightCircle
property: 'size'
from: 200
to: 0
duration: 500
}
}
}
}
}
}
Ok, to paint an item over another one you have at least 2 ways:
z (for siblings items only)
creating order (the last created is the highest)
I guess that the second way is preferable for you. So you just need to create the circle item after all others. For example:
Repeater {
id: outerRepeater
Repeater {
id: innerRepeater
...
MouseArea {
anchors.fill: parent
onClicked: {
highlightCircle.item = individualRectangle;
highlightAnimation.running = true;
}
}
}
}
}
Rectangle {
id: highlightCircle
property var item
...
anchors.horizontalCenter: item ? item.horizontalCenter : undefined
anchors.verticalCenter: item ? item.verticalCenter : undefined
}
PropertyAnimation {
id: highlightAnimation
...
}
Related
I'm trying to make an item that can be resized by its edges.
For showing a minimal testcase of the problem it is enough to have its left edge draggable, so here it is:
Rectangle {
id: root
border.width: 1
border.color: 'black'
color: 'red'
// save original position and size at drag start
property real origX: 0
property real origWidth: 0
// drag this item:
Item {
id: dragDummy
x: 0
onXChanged: {
root.x = root.origX + x
root.width = root.origWidth - x
}
}
MouseArea {
anchors.fill: root
drag.target: dragDummy
drag.axis: Drag.XAxis
drag.onActiveChanged: {
// onDragStarted -> Cannot assign to non-existent property "onDragStarted" ???
if(!active) return
root.origX = root.x
root.origWidth = root.width
}
}
}
the problem seems to be that if drag causes parent position to change, that triggers another drag event, causing this flicker:
I'm guessing MouseArea can't help here? Then low level mouse events should be used like in "old-school" apps (i.e. capturing events at root Item, manually compute offset with respect to initial mouse down position, etc...)?
(or I have to move the MouseArea to an ancestor that won't move during drag, which is almost the same...)
There is a nice QML Item type called DragHandler which people often overlook, but I find that it works very well.
This solution is a little more idiomatic than other suggestions in that it uses a declarative style rather than imperative:
import QtQuick 2.15
Item {
id: root
width: 500
height: 100
Item {
height: 100
width: handle.x + handle.width / 2
}
Rectangle {
x: handle.x + handle.width / 2
width: root.width - (handle.x - handle.width/2)
height: 100
border{
width: 1
color: 'black'
}
color: 'red'
}
Item {
id: handle
x: -width / 2
width: 50
height: 100
DragHandler {
yAxis.enabled: false
xAxis{
minimum: -handle.width
maximum: root.width
}
}
}
}
The solution I come up with consists of having two MouseAreas:
a MouseArea moves with the item to drag, that is used only for hit-testing, so its onPressed handler is something like this:
onPressed: (mouse) => {
mouse.accepted = false
root.container.myDragTarget = root
}
onReleased: root.container.myDragTarget = null
another MouseArea, stacked below the others and not moving, handles the mouse position change and the dragging:
onPressed: _start = Qt.point(mouseX, mouseY)
onPositionChanged: {
if(myDragTarget) {
var delta = Qt.point(mouseX - _start.x, mouseY - _start.y)
// do any rounding/snapping of delta here...
_start.x += delta.x
_start.y += delta.y
myDragTarget.x += delta.x
myDragTarget.y += delta.y
}
}
This is able to drag the item reliably.
This is also what I wanted to avoid, because it reinvents mouse drag, but in absence of a better solution it is what I am going to use.
I won't accept this answer as I'm curious to see other ways to approach this problem.
You can workaround the movement and new positioning of the dragged Item by mapping the coordinates with the mapToItem functions.
In my solution, I've not used the drag functionality of the MouseArea as it needs a drag.target. I've used the pressed and position changed signals to implement drag behavior. The only downside is the background Item which is needed for the mapToItem function as it doesn't accept the Window due to it not being an Item.
import QtQuick
import QtQuick.Window
import QtQuick.Shapes
Window {
id: root
visible: true
width: 400
height: 400
Item {
id: background
anchors.fill: parent
Rectangle {
id: rectangle
property int rightX
x: 50
y: 50
width: 200
height: 80
border.width: 1
border.color: "black"
color: "red"
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 10
color: mouseArea.containsMouse || mouseArea.pressed ? "#ff808080" : "#aa808080"
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onPressed: rectangle.rightX = rectangle.x + rectangle.width
onPositionChanged: function(mouse) {
if (mouseArea.pressed) {
var tmp = mouseArea.mapToItem(background, mouse.x, 0)
if (tmp.x <= rectangle.rightX)
rectangle.x = tmp.x
else
rectangle.x = rectangle.rightX
rectangle.width = rectangle.rightX - rectangle.x
}
}
}
}
}
}
}
I have a row for a listview delegate with buttons on it. On click of a button, i need a dialog to open just below that button. I tried mapToItem property and partially succeeded but this listview is scrollable and on scrolling the dialog stays in its initial position. Unsure of how to get it working. Also, new to posting questions. Kindly ignore if I am being vague and help me out.
The dialog i want to open is placed outside of this delegate. I have provided a short outline of my code.
Listview{
delegate: Row{
Button1{
}
Button2{
id: button2Id
onCheckedChanged{
var coords = button2Id.mapToItem(null,0,0)
dialogId.x = coords.x
dialogId.y= coords.y
dialogId.visible = true
}
}
}
}
//dialog rect outside of my listview
Rectangle{
id: dialogId
}
You could add the dialog to the highlight item of the list. I have modified your example a little so that I could test it. I encapsulated your Rectangle in an Item because ListView controls the size and position of the root object of the highlight. The Rectangle then just has to be anchored to the bottom of that Item.
ListView {
id: lv
width: 200
height: parent.height
model: 50
spacing: 1
currentIndex: -1
delegate: Row {
spacing: 1
height: 40
Button {
text: index
}
Button {
id: button2Id
text: ">"
onClicked: {
lv.currentIndex = index;
}
}
}
highlight: Item { // ListView controls the size/pos of this Item
z: 1
Rectangle {
id: dialogId
anchors.top: parent.bottom // Anchor to bottom of parent
width: 200
height: 100
color: "red"
}
}
}
UPDATE:
Here is a way to keep the dialog directly under the button without calculating margins. I put it in a Loader so that each item in the list doesn't always carry the whole dialog around with it. It might make a performance difference.
The ugly part of this solution is the z-ordering. Each item in the list is drawn after the one that comes sequentially before it. (I'm not actually sure if that's even guaranteed.) That means the dialog gets drawn underneath any item that comes after it in the list. I was able to get around that by changing the z value of each item in the list to be less than the item before it.
ListView {
id: lv
width: 200
height: parent.height
model: 50
spacing: 1
currentIndex: -1
delegate: Row {
z: lv.count - index // <<- z-value fix
spacing: 1
height: 40
Button {
text: index
}
Button {
id: button2Id
text: ">"
onClicked: {
lv.currentIndex = index;
}
Loader {
anchors.top: parent.bottom
asynchronous: true
sourceComponent: (index === lv.currentIndex) ? dialogComp : null
}
}
}
}
Component {
id: dialogComp
Rectangle {
id: dialogId
width: 200
height: 100
color: "red"
}
}
I have a Qt QML application. Following is the complete code of the application:
Code:
import QtQuick 2.9
import QtQuick.Window 2.2
Window {
id: app_main_window
visible: true
width: 800
height: 600
title: qsTr("Hello QML")
Rectangle {
id: my_item_1
x: 100
y: 100
width: 100
height: 100
color: "grey"
visible: true
z: 1
}
Rectangle {
id: my_item_2
x: 400
y: 400
width: 100
height: 100
color: "grey"
visible: true
z: 1
MouseArea {
anchors.fill: parent
onClicked: {
// How can I change the center of my_item_1 to some arbitrary new center. lets say app_main_window's center. this involves another question. how can I get app_main_window's center?
}
}
}
}
Question:
My question is simple. How can change the center of any QQuickitem to a new center? So in above case how can change the center of my_item_1 to a new center (Qt.point(some_x_center, some_y_center)) when my_item_2 gets a click event.
Additionally, is possible to get another item's center? Like app_main_window or my_item_2's center to apply to the target my_item_1?
PS:
I have made the code simple to make the question objective. I have quite a complicated logic in my actual code where I need to realign something like my_item_1 to a new center without usage of anchors as the QQuickitem I am trying to do that with is scaled and panned into a new point.
If anchors can't be used, you have to calculate manually the center and assign it.
Quick example:
Item
{
anchors.fill: parent
Rectangle
{
id: nonParentOrSibling
width: 200
height: 200
x: 350
y: 240
color: "red"
}
}
Rectangle
{
id: rectToMove
width: 100
height: 100
y: 200
color: "blue"
MouseArea
{
anchors.fill: parent
onClicked:
{
var itemCenter = Qt.point(nonParentOrSibling.x + nonParentOrSibling.width / 2, nonParentOrSibling.y + nonParentOrSibling.height / 2)
rectToMove.x = itemCenter.x - rectToMove.width / 2
rectToMove.y = itemCenter.y - rectToMove.height / 2
}
}
}
Note that this is a one time moving only, and the rectangle won't move if the target is moved.
To make it follow the target you have to rebind it with Qt.binding.
If rectToMove is not in the same coordinate system as nonParentOrSibling, you can use Item.mapToItem() and mapFromItem() to adapt the coordinates.
I am using a ComboBox in QML and when populated with a lot of data it exceeds my main windows bottom boarder. From googling I have learned that the drop-down list of a ComboBox is put on top of the current application window and therefore it does not respect its boundaries.
Ideally I would want the ComboBox to never exceed the main applications boundary, but I can not find any property in the documentation.
A different approach would be to limit the number of visible items of the drop-down list so that it do not exceed the window limits for a given window geometry. I was not able to find this in the documentation either and I have run out of ideas.
Take a look to the ComboBox source code, the popup is of a Menu type and it doesn't have any property to limit its size. Moreover, the z property of the Menu is infinite, i.e. it's always on top.
If you Find no way but to use the ComboBox of Qt you can create two models one for visual purpose, I will call it visual model, you will show it in your ComboBox and the complete one , it will be the reference model. Items count in your VisualModel wil be equal to some int property maximumComboBoxItemsCount that you declare . you'll need o find a way that onHovered find the index under the mouse in the visualmodel if it's === to maximumComboBoxIemsCount you do visualModel.remove(0) et visualModel.add(referenceModel.get(maximum.. + 1) and you'll need another property minimumComboBoxIemsCount, same logic but for Scroll Up , I dont know if it will work. but it's an idea
I think there is no solution using the built-in component and you should create your own comboBox. You can start from the following code.
ComboBox.qml
import QtQuick 2.0
Item {
id: comboBox
property string initialText
property int maxHeight
property int selectedItem:0
property variant listModel
signal expanded
signal closed
// signal sgnSelectedChoice(var choice)
width: 100
height: 40
ComboBoxButton {
id: comboBoxButton
width: comboBox.width
height: 40
borderColor: "#fff"
radius: 10
margin: 5
borderWidth: 2
text: initialText
textSize: 12
onClicked: {
if (listView.height == 0)
{
listView.height = Math.min(maxHeight, listModel.count*comboBoxButton.height)
comboBox.expanded()
source = "qrc:/Images/iconUp.png"
}
else
{
listView.height = 0
comboBox.closed()
source = "qrc:/Images/iconDown.png"
}
}
}
Component {
id: comboBoxDelegate
Rectangle {
id: delegateRectangle
width: comboBoxButton.width
height: comboBoxButton.height
color: "#00000000"
radius: comboBoxButton.radius
border.width: comboBoxButton.borderWidth
border.color: comboBoxButton.borderColor
Text {
color: index == listView.currentIndex ? "#ffff00" : "#ffffff"
anchors.centerIn: parent
anchors.margins: 3
font.pixelSize: 12
text: value
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: {
listView.height = 0
listView.currentIndex = index
comboBox.selectedItem = index
tools.writePersistence(index,5)
comboBoxButton.text = value
comboBox.closed()
}
}
}
}
ListView {
id: listView
anchors.top: comboBoxButton.bottom
anchors.left: comboBoxButton.left
width: parent.width
height: 0
clip: true
model: listModel
delegate: comboBoxDelegate
currentIndex: selectedItem
}
onClosed: comboBoxButton.source = "qrc:/Images/iconDown.png"
Component.onCompleted: {
var cacheChoice = tools.getPersistence(5);
listView.currentIndex = tools.toInt(cacheChoice)
selectedItem = listView.currentIndex
comboBoxButton.text = cacheModel.get(selectedItem).value
}
}
ComboBoxButton.qml
import QtQuick 2.0
Item {
id: container
signal clicked
property string text
property alias source : iconDownUp.source
property string color: "#ffffff"
property int textSize: 12
property string borderColor: "#00000000"
property int borderWidth: 0
property int radius: 0
property int margin: 0
Rectangle {
id: buttonRectangle
anchors.fill: parent
color: "#00000000"
radius: container.radius
border.width: container.borderWidth
border.color: container.borderColor
Image {
id: image
anchors.fill: parent
source: "qrc:/Images/buttonBackground.png"
Image {
id: iconDownUp
source: "qrc:/Images/iconDown.png"
sourceSize.height:20
sourceSize.width: 20
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
id:label
color: container.color
anchors.centerIn: parent
font.pixelSize: 10
text: container.text
font.bold: true
}
MouseArea {
id: mouseArea;
anchors.fill: parent
onClicked: {
container.clicked()
buttonRectangle.state = "pressed"
startTimer.start()
}
}
Timer{
id:startTimer
interval: 200
running: false;
repeat: false
onTriggered: buttonRectangle.state = ""
}
states: State {
name: "pressed"
when: mouseArea.pressed
PropertyChanges { target: image; scale: 0.7 }
PropertyChanges { target: label; scale: 0.7 }
}
transitions: Transition {
NumberAnimation { properties: "scale"; duration: 200; easing.type: Easing.InOutQuad }
}
}
}
I've used it in some software of mine, hence it is possible that It could not work "out of the box". I use it like this:
ComboBox{
id:cacheChoice
initialText: "None"
anchors.top: baseContainer.top
anchors.topMargin: 2
anchors.right: baseContainer.right
maxHeight: 500
listModel: cacheModel
onExpanded: {
cacheChoice.height = 500
}
onClosed: {
cacheChoice.height = 20
}
}
In case you are working with ComboBox from Qt Quick Controls 2, here's the source code for it:
https://github.com/qt/qtquickcontrols2/blob/5.12/src/imports/controls/ComboBox.qml
Based on that, this override of the behavior works to limit the height to something reasonable:
myComboBox.popup.contentItem.implicitHeight = Qt.binding(function () {
return Math.min(250, myComboBox.popup.contentItem.contentHeight);
});
It is possible to access the hidden MenuStyle within the ComboBoxStyle component. There you can use all the things and hidden things you have within a MenuStyle, including its maximum height.
The thing looks roughly like this.
Not pretty but it works well enough.
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.3
import QtQuick.Window 2.2
ComboBox {
id: comboBox
style: ComboBoxStyle {
// drop-down customization here
property Component __dropDownStyle: MenuStyle {
__maxPopupHeight: 400
__menuItemType: "comboboxitem" //not 100% sure if this is needed
}
}
As it came up resonantly in our team, here is a updated version of the idea shown above. The new version restricts the size automatically to the size of your application.
ComboBox {
id: root
style: ComboBoxStyle {
id: comboBoxStyle
// drop-down customization here
property Component __dropDownStyle: MenuStyle {
__maxPopupHeight: Math.max(55, //min value to keep it to a functional size even if it would not look nice
Math.min(400,
//limit the max size so the menu is inside the application bounds
comboBoxStyle.control.Window.height
- mapFromItem(comboBoxStyle.control, 0,0).y
- comboBoxStyle.control.height))
__menuItemType: "comboboxitem" //not 100% sure if this is needed
} //Component __dropDownStyle: MenuStyle
} //style: ComboBoxStyle
} //ComboBox
I have a GridView in QML ApplicationWindow which should be filled
with some Items.
I place my items with JS function "placeItems".
But the problem is that when Component.onCreated signal of ApplicationWindow is called the GridView is not yet layouted.
For example, the GridView has x coordinate equal to -425 in Component.onCreated of ApplicationWindow.
If I call the same function a second later - everything is ok and GridView
has correct coordinates (=75).
I've check the Qt reference back and forth and haven't found other signals (something like onLayouted or onLayoutComplete) that may be helpful.
The question is when to call "placeItems" so the GridView in ApplicationWindow
already has correct coordinates?
UPDATE1:
To observe the bad behaviour just click File->Start after the application started. It will place the item in the correct place.
import QtQuick 2.2
import QtQuick.Window 2.1
import QtQuick.Controls 1.1
ApplicationWindow {
id: mainWindow
width:1000
height: 900
color : "white"
visible: true
flags: Qt.Window
function max (a,b) { return a>b ? a : b; }
function min (a,b) { return a<b ? a : b; }
property int sizeMin: width < height ? width : height
property int dimField: sizeMin - 50
property int dimCellSpacing: 3
property int dimCell: (dimField / 5 ) - 1 - dimCellSpacing
GridView {
id: field
anchors.centerIn: parent
model: 20
width: dimField
height: dimField
cellWidth: dimCell
cellHeight: dimCell
delegate: cell
property var items: []
function centerCell(column,row) {
return {x: field.x + (column + 0.5) * cellWidth,
y: field.y + (row + 0.5) * cellHeight}
}
function placeItem(name, col, row) {
var c = centerCell(col,row)
items[name].centerX = c.x
items[name].centerY = c.y
}
function placeItems() {
placeItem ("a", 3, 3)
//placeItem ("b", 4, 4)
}
}
Component.onCompleted: field.placeItems()
Component {
id: cell
Rectangle {
id: rectCell
width: dimCell
height: dimCell
color: "lightgray"
border.width: 3
border.color: "brown"
}
}
Rectangle
{
id: rectItemA
property int dimItem: 100
property int centerX: 0
property int centerY: 0
property int margin: 5
property var cell: field.items["a"] = this
border.color: "black"
border.width: 3
width: dimItem
height: dimItem
x: centerX - width/2
y: centerY - height/2
color: "red"
opacity: 0.5
}
menuBar: MenuBar {
Menu {
title: qsTr("File")
MenuItem {
text: qsTr("Start")
onTriggered: field.placeItems();
}
MenuItem {
text: qsTr("Exit")
onTriggered: Qt.quit();
}
}
}
}
function placeItem(name, col, row) {
items[name].anchors.horizontalCenter = field.left;
items[name].anchors.verticalCenter = field.top;
items[name].anchors.horizontalCenterOffset = (col + 0.5) * cellWidth;
items[name].anchors.verticalCenterOffset = (row + 0.5) * cellHeight;
}
The key is to anchor the element in the grid view and then move it according to your calculations.
BTW, you know that QML has built in functions Math.min/Math.max?
EDIT
Or better yet, why not define the bindings in rectItemA directly?
Another, less hackish way to have the right behavior (don't play with Timer with layout, really, it's a bad idea):
You are defining your Rectangle as an item centered in a instance of a item belonging to your GridView. So, I use a little of your way (getting an item at the r row and the c column in the gridview), and then I reparent the Rectangle to this item. To make it centered, it is only needed to anchor it to the center of its newly bound parent.
import QtQuick 2.2
import QtQuick.Window 2.1
import QtQuick.Controls 1.1
ApplicationWindow {
id: mainWindow
width:1000
height: 900
color : "white"
visible: true
flags: Qt.Window
property int sizeMin: Math.min(width, height)
property int dimField: sizeMin - 50
property int dimCellSpacing: 3
property int dimCell: (dimField / 5 ) - 1 - dimCellSpacing
GridView {
id: field
anchors.centerIn: parent
model: 20
width: dimField
height: dimField
cellWidth: dimCell
cellHeight: dimCell
delegate: cell
function cellAt(row, col) {
return itemAt(row * (dimCell + dimCellSpacing), col * (dimCell + dimCellSpacing));
}
}
Component {
id: cell
Rectangle {
id: rectCell
width: dimCell
height: dimCell
color: "lightgray"
border.width: 3
border.color: "brown"
}
}
Rectangle
{
id: rectItemA
property int dimItem: 100
property int margin: 5
border.color: "black"
border.width: 3
width: dimItem
height: dimItem
anchors.centerIn: parent
color: "red"
opacity: 0.5
}
Component.onCompleted: {
rectItemA.parent = field.cellAt(3, 3);
}
menuBar: MenuBar {
Menu {
title: qsTr("File")
MenuItem {
text: qsTr("Exit")
onTriggered: Qt.quit();
}
}
}
}
Why don't you just delay the placeItems function so it runs with a tiny delay so that when it runs the "static" components are all completed.
Timer {
interval: 250 // might want to tune that
repeat: false
onTriggered: placeItems()
}
In a perfect world, Component.onCompleted would nest perfectly and the root item would be the last one to be emitted. But unfortunately Qt does not guarantee the order, and indeed as I did a quick test, the parent item emits (or at least responds to) onCompleted BEFORE the child items.
And if you don't want to pollute your QML file with the timer, you can actually instantiate a "helper object" from a JS function dynamically, set it to do its work and then delete itself when done. Similar to the "reparent helper" I outlined in this answer: Better way to reparent visual items in QML but rather delete itself on the timer timeout rather than in the JS function which would not give it the time to trigger.