I'm trying to get an infinite scrolling effect with a list of game items by wrapping itself around itself so if you keep pressing down it will wrap around to the first item smoothly.
The effect I'm trying to get is like this image here where there is no breaks in the menu items being listed.
I'm currently doing this in Qt Quick QML but I think it can be code agnogstic. I'm also trying not to shift by moving item indices within the list, just moving the Y position of the object.
Here is my code currently (just a normal list positioning)
Text
{
id:root
//clip: true
property int spacing
property int currentIndex
property int selectedIndex
property int maxIndex
property int difference: Math.min(selectedIndex - currentIndex, maxIndex-currentIndex)
width: 500
x: -400 + 30 * Math.abs(difference)
y: (currentIndex- selectedIndex) * spacing
Behavior on x { NumberAnimation { duration: 500 } }
Behavior on y { NumberAnimation { duration: 500 } }
}
Any help would be appreciated
So my friend sent me a code snippet on how to do the wrapping I needed. Here's the code for the element item.
Text
{
//externally set
property int itemIndex
property int selectedIndex
property int numberOfElements
property int span
//internally set
property int shortestDistance
readonly property int distancePerItem: 100
id: root
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.verticalCenterOffset: shortestDistance * distancePerItem
anchors.rightMargin: Math.abs(shortestDistance) > span ? -contentWidth * Math.abs(shortestDistance) : 0
Behavior on anchors.rightMargin { NumberAnimation { duration: 500 } }
Behavior on anchors.verticalCenterOffset { NumberAnimation { duration: 500 } }
onSelectedIndexChanged: updatePosition()
function updatePosition()
{
shortestDistance = itemIndex - selectedIndex;
if (shortestDistance > numberOfElements/2)
{
shortestDistance -= numberOfElements;
}
else if (shortestDistance < -numberOfElements/2)
{
shortestDistance += numberOfElements;
}
}
}
Related
my question is about using a QML DragHandler to move a QML Item. I have successfully implemented position through dragging (when holding the Ctrl modifier) like so:
DragHandler {
dragThreshold: 0
acceptedModifiers: Qt.ControlModifier
}
Now I would like to add another handler that allows me to precisely position the element. Other software does this throught the use of the shift modifier. So what I want to do is move the element not by the pixel amount that the mouse moves, but an amount smaller than that. Ideally I would want to do something like this:
DragHandler {
dragThreshold: 0
acceptedModifiers: Qt.ShiftModifier
onActiveTranslationChanged: {
activeTranslation *= 0.5;
}
}
Unfortunatelly activeTranslation is read-only and I don't see any other property I could use and I can't think of any other way to do it... Does anybody have an idea?
Thank you very much in advance!
Unfortunately Qt doesn't provide any way to change the drag speed AFAIK.
But this is a way to achieve it:
Rectangle
{
id: theDraggableElement
width: 100
height: width
color: "red"
DragHandler
{
id: dragHandlerFast
dragThreshold: 0
acceptedModifiers: Qt.ControlModifier
target: theDraggableElement
}
}
Item
{
id: invisibleItemForSlowDragging
width: theDraggableElement.width
height: theDraggableElement.height
Binding { restoreMode: Binding.RestoreBinding; when: !dragHandlerSlow.active; target: invisibleItemForSlowDragging; property: "x"; value: theDraggableElement.x }
Binding { restoreMode: Binding.RestoreBinding; when: !dragHandlerSlow.active; target: invisibleItemForSlowDragging; property: "y"; value: theDraggableElement.y }
DragHandler
{
id: dragHandlerSlow
dragThreshold: 0
acceptedModifiers: Qt.ShiftModifier
target: invisibleItemForSlowDragging
onTranslationChanged:
{
theDraggableElement.x = invisibleItemForSlowDragging.x - dragHandlerSlow.translation.x / 2
theDraggableElement.y = invisibleItemForSlowDragging.y - dragHandlerSlow.translation.y / 2
}
}
}
I have tested this with Qt 5.15.2.
I'm trying to do the following.
So far the only solution I found is to create my button from C++ and to use setMask. Is there any other way in order to achieve the same results but just using QML?
My shape button:
import QtQuick 2.0
import QtQuick.Shapes 1.14
Item {
id: sideButtonID
property alias mouseX: mouseArea.mouseX
property alias mouseY: mouseArea.mouseY
readonly property bool pressed: containsMouse && mouseArea.pressed
property point topLeftOffset: Qt.point(0, 0)
property point topRightOffset: Qt.point(0, 0)
property point bottomRightOffset: Qt.point(0, 0)
property point bottomLeftOffset: Qt.point(0, 0)
property var colorPressed: "green"
property var colorSelected: "red"
property var colorUnselected: "darkorange"
signal clicked
property var coordinate: generateButtonShapeCoordinates(x,y,width,height)
property point topLeft: coordinate.topLeft
property point topRight: coordinate.topRight
property point bottomRight: coordinate.bottomRight
property point bottomLeft: coordinate.bottomLeft
function generateButtonShapeCoordinates(x, y, width, height){
var topLeft = Qt.point(x-width/2 - topLeftOffset.x, y-height-topLeftOffset.y)
var topRight = Qt.point(x+width/2-topRightOffset.x, y-height-topRightOffset.y)
var bottomRight = Qt.point(x+width/2-bottomRightOffset.x, y-bottomRightOffset.y)
var bottomLeft = Qt.point(x - width/2-bottomLeftOffset.x, y-bottomLeftOffset.y)
return {
topLeft : topLeft,
topRight : topRight,
bottomRight : bottomRight,
bottomLeft : bottomLeft
};
}
function inside(point, polygon) {
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
var x = point[0], y = point[1];
var inside = false;
for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
var xi = polygon[i][0], yi = polygon[i][1];
var xj = polygon[j][0], yj = polygon[j][1];
var intersect = ((yi > y) != (yj > y))
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
property bool containsMouse: {
var topLeft = Qt.point(0-topLeftOffset.x, 0-topLeftOffset.y)
var topRigh = Qt.point(width-topRightOffset.x, 0-topRightOffset.y)
var bottomRight = Qt.point(width-bottomRightOffset.x, height-bottomRightOffset.y)
var bottomLeft = Qt.point(0-bottomLeftOffset.x, height-bottomLeftOffset.y)
var polygon = [[topLeft.x,topLeft.y],[topRigh.x,topRigh.y],[bottomRight.x,bottomRight.y],[bottomLeft.x,bottomLeft.y]]
var point = [mouseX, mouseY]
return inside(point, polygon)
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: if (sideButtonID.containsMouse) sideButtonID.clicked()
}
Shape {
antialiasing: true
vendorExtensionsEnabled: true
asynchronous: true
anchors.centerIn: parent
ShapePath {
id: shapepathId
strokeWidth: -1
fillColor: sideButtonID.pressed ? sideButtonID.colorPressed : (sideButtonID.containsMouse ? sideButtonID.colorSelected : sideButtonID.colorUnselected)
startX: sideButtonID.topLeft.x; startY: sideButtonID.topLeft.y
PathLine { x: sideButtonID.topRight.x; y: sideButtonID.topRight.y }
PathLine { x: sideButtonID.bottomRight.x; y: sideButtonID.bottomRight.y }
PathLine { x: sideButtonID.bottomLeft.x; y: sideButtonID.bottomLeft.y }
}
Component.onCompleted: {
console.log("TopLeft: "+sideButtonID.topLeft)
console.log("TopRight: "+sideButtonID.topRight)
console.log("BottomLeft: "+sideButtonID.bottomLeft)
console.log("BottomRight: "+sideButtonID.bottomRight)
}
}
}
The usage of my button:
SideButtons {
z: 100
id: sideButtonID
width: 130
height: 248
anchors.verticalCenterOffset: 9
anchors.horizontalCenterOffset: -255
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
bottomRightOffset: Qt.point(100,0)
onClicked: {
print("X: "+mouseX)
print("Y: "+mouseY)
print("clicked")
}
}
Some of the options that I've heard so far but there are not good in my case.
using an image with a transparent background (sure but I'm going to have the same problem)
creating a C++ class which will inherit QAbstractButton (I would prefer to avoid as much as I can usage of C++ in the Ui side)
usage of a Shape in combination with MouseArea (I'm doing this already but I am encountering the bounding box problem.
Is there any other way which I don't know yet about?
I follow the Using C++ Models with Qt Quick Views and AbstractItemModel example project in Qt 5.5. Screen class is the model data; ScreenManager is derived from QAbstractListModel acting as the model source in QML GridView. In the GridView, I also add a MouseArea and some animation in delegate to implement items drag and drop.
My expected result is when dragging a screen around, whenever it's on top of another screen, the target screen will be moved to the last position of dragged screen until button release to drop the dragged screen. All the movement should use the animation declared in QML.
Now it could show screens correctly and recognize the selected screen. But it fails to swap the dragged and dropped screens. The underlying list has swapped the element but it doesn't reflect to the view.
A similar question is this, I try to use beginMoveRows and endMoveRows. But my program crashes on calling endMoveRows. layoutChanged would rearrange the whole model. Because I have animation on grid item movement. layoutChanged would cause non affected screens shifting from top left to their original position.
Edit: endMoveRows crash is caused by invalid operation described here.
Edit: the GridView has a 3 * 5 items. Since it's in a QList, I assume I only need to move on rows.
Screen.h
class Screen
{
public:
Screen(QString name, int gridId, bool active = false);
QString name() const;
int gridId() const;
bool active() const;
void setActive(bool a);
private:
QString m_name;
int m_gridId;
bool m_active;
};
ScreenManager.h
#include "Screen.h"
#include <QAbstractListModel>
class ScreenManager : public QAbstractListModel
{
Q_OBJECT
public:
enum ScreenRoles {
NameRole = Qt::UserRole + 1,
GridIDRole,
ActiveRole
};
ScreenManager();
void addScreen(const Screen& screen);
int rowCount(const QModelIndex& parent = QModelIndex()) const;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
Q_INVOKABLE int getScreenGridId(int index);
Q_INVOKABLE bool getScreenActive(int index);
Q_INVOKABLE void swapScreens(int index1, int index2);
protected:
QHash<int, QByteArray> roleNames() const;
private:
QList<Screen> m_screens;
};
ScreenManager.cpp
#include "ScreenManager.h"
#include "Screen.h"
ScreenManager::ScreenManager()
{
int index = 0;
for (;index < 15; index++) {
addScreen(Screen(QString ("Screen%1").arg(index), index, true));
}
}
void ScreenManager::addScreen(const Screen& screen)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_screens << screen;
endInsertRows();
}
int ScreenManager::rowCount(const QModelIndex& parent) const {
Q_UNUSED(parent);
return m_screens.count();
}
QVariant ScreenManager::data(const QModelIndex& index, int role) const
{
if (index.row() < 0 || index.row() >= m_screens.count())
return QVariant();
const Screen& screen = m_screens[index.row()];
if (role == NameRole)
return screen.name();
else if (role == GridIDRole)
return screen.gridId();
else if (role == ActiveRole)
return screen.active();
return QVariant();
}
QHash<int, QByteArray> ScreenManager::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[GridIDRole] = "gridId";
roles[ActiveRole] = "active";
return roles;
}
int ScreenManager::getScreenGridId(int index)
{
return m_screens.at(index).gridId();
}
bool ScreenManager::getScreenActive(int index)
{
return m_screens.at(index).active();
}
void ScreenManager::swapScreens(int index1, int index2)
{
int min = index1 < index2 ? index1 : index2;
int max = index1 > index2 ? index1 : index2;
bool r = beginMoveRows(QModelIndex(), min, min, QModelIndex(), max);
r = beginMoveRows(QModelIndex(), max-1, max-1, QModelIndex(), min);
m_screens.swap(index1, index2);
endMoveRows();
}
QML GridView related code
ScreenManager {
id : screenManager
}
GridView {
id: gridView
x: 82
y: 113
width: cellWidth * 5
height: cellHeight * 3
clip: true
anchors.bottom: parent.bottom
anchors.bottomMargin: 70
anchors.topMargin: 100
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
flickableDirection: Flickable.HorizontalAndVerticalFlick
cellWidth: 90; cellHeight: 90;
property bool ignoreMovementAnimation: true
MouseArea {
id: gridViewMouseArea
hoverEnabled: true
preventStealing : true
property int currentGridId: -1
property int preIndex
property int index: gridView.indexAt(mouseX, mouseY)
anchors.fill: parent
onPressAndHold: {
currentGridId = screenManager.getScreenGridId(index)
preIndex = index
preIndexBackup = preIndex
gridView.ignoreMovementAnimation = false
}
onReleased: currentGridId = -1
onPositionChanged: {
if (currentGridId != -1 && index != -1 && index != preIndex) {
if (screenManager.getScreenActive(index)) {
screenManager.swapScreens(preIndex, index)
preIndex = index
}
}
}
}
model: screenManager
delegate: Component {
Item {
id: gridViewDelegate
width: gridView.cellWidth; height: gridView.cellHeight
Image {
id: itemImage
parent: gridView
x: gridViewDelegate.x + 5
y: gridViewDelegate.y + 5
width: gridViewDelegate.width - 10
height: gridViewDelegate.height - 10;
fillMode: Image.PreserveAspectFit
smooth: true
source: "qrc:/res/image/screen_icon.png"
visible: active
Text {
text: name
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
anchors.fill: parent;
border.color: "grey"
border.width: 6
color: "transparent"; radius: 5
visible: itemImage.state === "active"
}
// specify the movement's animation for non-active screen icons
Behavior on x {
enabled: !gridView.ignoreMovementAnimation && itemImage.state !== "active"
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
}
Behavior on y {
enabled: !gridView.ignoreMovementAnimation && itemImage.state !== "active"
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
}
// specify the shaking animation for non-active screen icons when hold one icon
SequentialAnimation on rotation {
NumberAnimation { to: 2; duration: 60 }
NumberAnimation { to: -2; duration: 120 }
NumberAnimation { to: 0; duration: 60 }
running: gridViewMouseArea.currentGridId != -1 && itemImage.state !== "active"
loops: Animation.Infinite
alwaysRunToEnd: true
}
// specify the active screen's new position and size
states: State {
name: "active"
when: gridViewMouseArea.currentGridId == gridId
PropertyChanges {
target: itemImage
x: gridViewMouseArea.mouseX - width/2
y: gridViewMouseArea.mouseY - height/2
scale: 0.5
z: 10
}
}
// specify the scale speed for the active screen icon
transitions: Transition {
NumberAnimation { property: "scale"; duration: 200}
}
}
}
}
}
main.cpp
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
qmlRegisterType<ScreenManager>("com.gui", 1, 0, "ScreenManager");
QQmlApplicationEngine engine(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
It turns out I need to deal some corner cases to avoid no-op or invalid move operation.
void ScreenManager::swapScreens(int index1, int index2)
{
int min = index1 < index2 ? index1 : index2;
int max = index1 > index2 ? index1 : index2;
m_screens.swap(index1, index2);
beginMoveRows(QModelIndex(), max, max, QModelIndex(), min);
endMoveRows();
if (max - min > 1) {
beginMoveRows(QModelIndex(), min + 1, min + 1, QModelIndex(), max + 1);
endMoveRows();
}
}
I'm trying to find a way to do a transition on a QML element, when a binding changes. Say you have a Text element, with the text property bound to something. What I want is when the data in the binding changes, the element fades out (Still displaying old data), switches and fades back in with the new data (the actual transition occurring while the element isn't visible.)
I've been searching everywhere for a way to do this but I can figure it out. I've tried using Qt Quick animations within QML, but the data itself changes before the animation runs, leaving the animation unnecessary. I've tried creating a custom QDeclarativeItem object that calls an animation within the QDeclarativeItem::paint() but I can't figure out how to get it to actually run.
I should note here that I know my bindings are working fine as the displayed data changes, I just can't get these animations to run at the proper time.
Here is what I tried with QML:
Text {
id: focusText
text: somedata
Behavior on text {
SequentialAnimation {
NumberAnimation { target: focusText; property: "opacity"; to: 0; duration: 500 }
NumberAnimation { target: focusText; property: "opacity"; to: 1; duration: 500 }
}
}
}
And here is what I tried in implementing a custom QDeclarativeItem:
// PAINTER
void AnimatedBinding::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) {
// Setup the pen
QPen pen(m_color, 2);
painter->setPen(pen);
painter->setOpacity(this->opacity());
// Draw the item
if (m_bindingType == QString("text")) {
QPropertyAnimation animation(this, "opacity");
animation.setDuration(1000);
animation.setStartValue(1);
if (drawn) {
animation.setStartValue(1);
animation.setEndValue(0);
animation.start();
} else drawn = true;
painter->drawText(boundingRect(), m_data.toString());
animation.setEndValue(0);
animation.start();
} else {
qCritical() << "Error unknown binding type!";
return;
}
}
But like I said, the animation that I start within the painter never actually fires.
Any tips? Anyone ever done this before? I've been banging my head on this for about a week.
How about doing it in qml only this ways :
Define a custom element of your own type, that behaves the way you want it to.
Use this element instead of traditional element to be animated.
eg. I have create a custom 'AnimatedText' type to have the fading in and fading out behavior on the text elements whenever text related to them changes.
File 1 : AnimatedText.qml
import QtQuick 1.0
Item
{
id: topParent
property string aText: ""
property string aTextColor: "black"
property int aTextFontSize: 10
property int aTextAnimationTime : 1000
Behavior on opacity { NumberAnimation { duration: aTextAnimationTime } }
onATextChanged:
{
topParent.opacity = 0
junkTimer.running = true
}
Timer
{
id: junkTimer
running: false
repeat: false
interval: aTextAnimationTime
onTriggered:
{
junkText.text = aText
topParent.opacity = 1
}
}
Text
{
id: junkText
anchors.centerIn: parent
text: ""
font.pixelSize: aTextFontSize
color: aTextColor
}
}
and in your main.qml
import QtQuick 1.0
Rectangle
{
id: topParent
width: 360
height: 360
AnimatedText
{
id: someText
anchors.centerIn: parent
aText: "Click Me to change!!!.."
aTextFontSize: 25
aTextColor: "green"
aTextAnimationTime: 500
}
MouseArea
{
anchors.fill: parent
onClicked:
{
someText.aText = "Some random junk"
}
}
}
How can I select a few elements in the QML ListView and send its indices to C++ code?
Do something like that: if an element is clicked, set its property selected (or however you call it), and set in delegate that if selected is true, then it should be formatted differently. Plus add it to some list, to work with it.
I am pretty sure there is no way to make a QML ListView multi-selectable. Qt Declarative is focused on touch screen use and there is no meaningful way to multiselect in a pure touch UI.
i had the same issue and i found the best way to implement it, is to create a new role to the listview. Lets assume it is firstname and selected. you need to use both onCurrentIndexChanged and onClicked, because if you scroll, this will change the item but it is not a click. In both of them change the role selected into true, or adjust as it suits you, may be you don't need scroll to select and thus use only the onClicked. When clicked you can change the role selected into true
onCurrentIndexChanged:
{
mListModel.append({"firstName": newEntry,"selected":true})
}
and
onClicked:
{
mListModel.append({"firstName": newEntry,"selected":true})
}
then you may use a highlight in the deligate, this will change the color based on the state of the selected.
Here is a full code that is tested to work
//copyright: Dr. Sherif Omran
//licence: LPGL (not for commercial use)
import QtQuick 2.12
import QtQuick.Layouts 1.12
Item {
property string addnewitem:""
property int removeitemindex: -1
property string appenditemstring: ""
property int appenditemindx:-1
property int fontpoint: 20
property int radiuspoint: 14
property int spacingvalue: 0
property string delegate_color:"beige"
property string delegate_border_color:"yellowgreen"
property string highlight_color:"deeppink"
signal selectedvalueSignal (string iTemstring, string stateval)
property string sv: ""
property int indexcopy:0
id:lstmodelitem
width: parent.width
height: parent.height
ListModel {
id : mListModel
// ListElement {
// firstName : "John"
// }
}
ColumnLayout {
anchors.fill: parent
ListView{
id : mListViewId
model:mListModel
delegate :delegateId
Layout.fillWidth : true
Layout.fillHeight: true
clip: true
snapMode: ListView.SnapToItem //this stops the view at the boundary
spacing: spacingvalue
highlight: Rectangle
{
id: highlightid
width: parent.width
color: mListModel.selected==="true"?"blue":highlight_color
border.color: "beige"
z:3
opacity: 0.2
}
highlightRangeMode: ListView.StrictlyEnforceRange
highlightFollowsCurrentItem:true
onCurrentIndexChanged:
{
console.log("olistdynamic Indexchanged" + currentIndex)
mListViewId.currentIndex=currentIndex
lstmodelitem.selectedvalueSignal(currentIndex, mListModel.selected)
indexcopy=currentIndex
}
}
}
function getindex()
{
console.log("current index = " + indexcopy)
return mListViewId.currentIndex
}
function setindex(index)
{
//console.log("olistdynamic set index"+index)
mListViewId.currentIndex=index
}
function add2Item(newEntry,statev){
console.log("added item with value = " + newEntry + "state " + statev)
mListModel.append({"firstName": newEntry,"selected":statev})
}
function deleteItem(index){
mListModel.remove(index,1)
}
function appendIdem(index,valueEntry,newselectedsate)
{
console.log("append item")
mListModel.set(index,{"firstName": valueEntry,"selected":newselectedsate})
}
Component {
id : delegateId
Rectangle {
id : rectangleId
width : parent.width // Remember to specify these sizes or you'll have problems
height: textId.implicitHeight*1.2
color: selected==="true"?"blue":delegate_color
border.color: delegate_border_color
radius: radiuspoint
Text {
id : textId
anchors.centerIn: parent
text : firstName
font.pointSize: fontpoint
}
MouseArea {
anchors.fill: parent
onClicked: {
lstmodelitem.selectedvalueSignal(mListModel.firstName,mListModel.selected)
mListViewId.currentIndex=index
console.log("current index = " + index)
indexcopy=index
appendIdem(index,firstName,"true")
}
onClipChanged:
{
//console.log("a")
}
}
}
}
//if the item has been changed from null to text
onAddnewitemChanged: {
console.log("added item" + addnewitem)
add2Item(addnewitem)
}
//remove item with index
onRemoveitemindexChanged: {
console.log("remove item")
deleteItem(removeitemindex)
}
//to change the item, change the index first then the string
onAppenditemstringChanged: {
appendIdem(appenditemindx,appenditemstring)
}
}
You may try to get the ListItem's data and store it to an array on odd click and remove the ListItem's data from the array on even click. May be a simple workout, instead of creating a list of check box like items.