How can I animate scrolling in a QML ScrollView?
I've tried a Behavior on the contentItem.contentY, but that isn't working.
With Qt Quick Controls 1
You just have to animate the value changes on the property flickableItem.contentY.
A quick example:
Item {
anchors.fill: parent
ColumnLayout {
anchors.fill: parent
Button {
id: btn
onClicked: scroll.scrollTo(scroll.flickableItem.contentY + 100)
}
ScrollView {
id: scroll
function scrollTo(y) {
scrollAnimation.to = y
scrollAnimation.start()
}
NumberAnimation on flickableItem.contentY {
id: scrollAnimation
duration: 1000
}
contentItem: Column {
Repeater {
model: 30
Rectangle {
width: 100; height: 40
border.width: 1
color: "yellow"
}
}
}
}
}
}
When you click on the button, it will scroll by 100 px with a smooth jump.
With Qt Quick Controls 2
The flickableItem.contentY isn't available anymore. The simpliest way to do the same thing in Qt Quick Controls 2 is to animate the position of the ScrollBar.
Notice that the position of QScrollBar is in percent (expressed between 0 and 1), not in pixels.
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
ScrollView {
id: scroll
width: 200
height: 200
clip: true
function scrollTo(y) {
scrollAnimation.to = y
scrollAnimation.start()
}
ScrollBar.vertical: ScrollBar {
id: test
parent: scroll
x: scroll.mirrored ? 0 : scroll.width - width
y: scroll.topPadding
height: scroll.availableHeight
active: scroll.ScrollBar.horizontal.active
policy: ScrollBar.AlwaysOn
NumberAnimation on position {
id: scrollAnimation
duration: 1000
}
}
ListView {
model: 20
delegate: ItemDelegate {
text: "Item " + index
}
}
}
Button {
id: btn
anchors.top: scroll.bottom
onClicked: scroll.scrollTo(test.position + 0.1)
}
}
When you click on the button, it will scroll by 10% of the height.
Related
I'm trying to set something up, where there's a a HorizontalHeaderView that spans the entire width of the window, and a TableView that also spans the entire width of the window, along with stretching down to the bottom of the window. I also wanted a TextField and Button side by side, to sit on top of all this.
So far, I've gotten the TextField and Button to sit at the top, and part of the HorizontalHeaderView to be visible, but the TableView is completely invisible. I've tried fiddling around with height, Layout.preferredHeight, etc. but nothing makes it stretch to the bottom and fill the width of the window.
Here's what I've got so far:
import QtQuick 6.0
import QtQuick.Controls.Basic 6.0
import QtQuick.Layouts 6.0
ApplicationWindow {
title: "Portmod"
width: 640
height: 480
visible: true
header: TabBar {
id: mainTabBar
width: parent.width
TabButton {
text: "Manage"
width: implicitWidth
}
TabButton {
text: "Search"
width: implicitWidth
}
}
StackLayout {
id: mainStackLayout
currentIndex: mainTabBar.currentIndex
ColumnLayout {
id: manageTab
RowLayout {
TextField {
placeholderText: "Filter packages..."
}
Button {
text: "Filter"
}
}
HorizontalHeaderView {
id: installedPackagesHeader
syncView: installedPackagesTable
}
TableView {
id: installedPackagesTable
columnSpacing: 1
rowSpacing: 1
clip: true
model: installedPackagesModel
Keys.onUpPressed: installedPackagesTableVerticalScrollBar.decrease()
Keys.onDownPressed: installedPackagesTableVerticalScrollBar.increase()
Keys.onLeftPressed: installedPackagesTableHorizontalScrollBar.decrease()
Keys.onRightPressed: installedPackagesTableHorizontalScrollBar.increase()
ScrollBar.vertical: ScrollBar {
id: installedPackagesTableVerticalScrollBar
parent: installedPackagesTable
}
ScrollBar.horizontal: ScrollBar {
id: installedPackagesTableHorizontalScrollBar
parent: installedPackagesTable
}
delegate: Rectangle {
implicitWidth: 300
implicitHeight: 25
Text {
text: display
anchors.left: parent.left
}
}
}
}
Rectangle {
id: searchTab
width: parent.parent.width
height: parent.parent.height
}
}
}
Here's my custom QSortFilterProxyModel and QAbstractItemModel:
class InstalledPackagesProxyModel(QSortFilterProxyModel):
def __init__(self, data: list[list]) -> None:
super().__init__()
self.realModel = InstalledPackagesModel(data)
self.setSourceModel(self.realModel)
def get_atom(self, index: QModelIndex) -> Atom:
"""
Returns a reference to the source index instead of proxy index, to handle the sorted view.
"""
mapped_index = self.mapToSource(index)
data = [tuple(pkg_data_list) for pkg_data_list in self.realModel._data]
return Atom(f"{data[mapped_index.row()][2]}/{data[mapped_index.row()][0]}")
class InstalledPackagesModel(QAbstractTableModel):
def __init__(self, data: list[list]):
super(InstalledPackagesModel, self).__init__()
self._data = data
self.header_labels = ["Name", "Use Flags", "Category", "Version"]
def data(self, index, role):
if role == Qt.DisplayRole: # type: ignore
value = self._data[index.row()][index.column()]
if isinstance(value, Atom):
return str(value)
return value
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
try:
return len(self._data[0])
# If there are no installed mods in the prefix
except IndexError:
return 1
def headerData(self, section, orientation, role=Qt.DisplayRole): # type: ignore
if role == Qt.DisplayRole and orientation == Qt.Horizontal: # type: ignore
return self.header_labels[section]
return QAbstractTableModel.headerData(self, section, orientation, role)
This is what it looks like:
You need to use anchors.fill: parent on the StackView so it fills out its parent and has a proper size. Furthermore the HorizontalHeaderView needs to get a implicitHeight and the TableView should set Layout.fillWidth and Layout.fillHeight to true.
You need to use Layout.fillWidth and Layout.fillHeight to span to the full width and height of layouts.
import QtQuick 6.0
import QtQuick.Controls.Basic 6.0
import QtQuick.Layouts 6.0
import Qt.labs.qmlmodels 1.0
ApplicationWindow {
title: "Portmod"
width: 640
height: 480
visible: true
header: TabBar {
id: mainTabBar
width: parent.width
TabButton { text: "Manage" }
TabButton { text: "Search" }
}
StackLayout {
id: mainStackLayout
currentIndex: mainTabBar.currentIndex
anchors.fill: parent
ColumnLayout {
id: manageTab
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
TextField {
Layout.fillWidth: true
placeholderText: "Filter packages..."
}
Button {
Layout.fillWidth: true
text: "Filter"
}
}
HorizontalHeaderView {
id: installedPackagesHeader
syncView: tableView
implicitHeight: 30
Layout.fillWidth: true
}
TableView {
id: tableView
Layout.fillWidth: true
Layout.fillHeight: true
columnSpacing: 1
rowSpacing: 1
clip: true
columnWidthProvider: function (column) {
return tableView.model ? tableView.width / tableView.model.columnCount : 0
}
rowHeightProvider: function (row) { return 30 }
onWidthChanged: tableView.forceLayout()
model: TableModel {
TableModelColumn { display: "name" }
TableModelColumn { display: "color" }
rows: [
{ "name": "cat", "color": "black" },
{ "name": "dog", "color": "brown" },
{ "name": "bird", "color": "white" }
]
}
delegate: Rectangle {
Text {
text: display
anchors.left: parent.left
}
}
}
}
Rectangle {
id: searchTab
Layout.fillWidth: true
Layout.fillHeight: true
color: "red"
}
}
}
I figured out that I needed to set the columnWidthProvider to this:
return Window.width / installedPackagesTableView.model.columnCount()
This is my full code for the TableView:
TableView {
id: installedPackagesTableView
property int selectedRow: 0
focus: true
columnSpacing: 1
rowSpacing: 1
clip: true
model: installedPackagesModel
columnWidthProvider: function () {
return Window.width / installedPackagesTableView.model.columnCount()
}
rowHeightProvider: function (row) {
return 30;
}
onWidthChanged: installedPackagesTableView.forceLayout()
Layout.fillWidth: true
Layout.fillHeight: true
Keys.onUpPressed: function () {
if (selectedRow != 0) {
selectedRow -= 1;
}
// Move scrollbar up if the selectedRow is going to be invisible
if (selectedRow == topRow) {
installedPackagesTableVerticalScrollBar.decrease()
}
}
Keys.onDownPressed: function () {
if (selectedRow != installedPackagesModel.rowCount() - 1) {
selectedRow += 1;
// Move scrollbar down if the selectedRow is going to be invisible
if (selectedRow == bottomRow) {
installedPackagesTableVerticalScrollBar.increase()
}
}
}
Keys.onReturnPressed: installedPackagesModel.getAtom(selectedRow)
ScrollBar.vertical: ScrollBar {
id: installedPackagesTableVerticalScrollBar
}
ScrollBar.horizontal: ScrollBar {
id: installedPackagesTableHorizontalScrollBar
}
delegate: ItemDelegate {
highlighted: row == installedPackagesTableView.selectedRow
onClicked: installedPackagesTableView.selectedRow = row
onDoubleClicked: installedPackagesModel.getAtom(row)
text: model.display
}
}
I have a ListView which is inside a ScrollView and I want to know or listen when the scroll bar is scrolled to the bottom,
ScrollView {
id: moderatorMessagesScrollViewID
anchors.fill: parent
function update()
{
console.debug("<< moderatorMessagesScrollViewID ScrollView::update ", flickableItem.atYEnd);
}
flickableItem.onAtYBeginningChanged: {
update();
}
flickableItem.onAtYEndChanged: {
update();
}
flickableItem.onContentYChanged: {
update();
}
ListView {
id: moderatorMessagesList
anchors.leftMargin: 15
anchors.rightMargin: 15
anchors.bottomMargin: 5
layoutDirection: Qt.LeftToRight
orientation: ListView.Vertical
verticalLayoutDirection: ListView.BottomToTop
cacheBuffer: (chatPanel.height <= 0) ? 1000 : (chatPanel.height * 1000)
spacing: 0
focus: true
Component.onCompleted: {
updateView();
}
delegate: messageListDelegate
}
style: ScrollViewStyle {
incrementControl: Rectangle {
visible: false
implicitWidth: 0
implicitHeight: 0
}
decrementControl: Rectangle {
visible: false
implicitWidth: 0
implicitHeight: 0
}
corner: Rectangle {
color: "white"
visible: true
rotation: 180
}
handle: Rectangle {
implicitWidth: 5 * MyStyle.props.scrollSizeFactor()
implicitHeight: 7 * MyStyle.props.scrollSizeFactor()
color: Qt.rgba(237/255, 237/255, 237/255, 1)
radius: 2
border.width: 1
border.color: "#C3C3C3"
}
scrollToClickedPosition: true
handleOverlap: 1
scrollBarBackground: Rectangle {
width: 5 * MyStyle.props.scrollSizeFactor()
height: 10 * MyStyle.props.scrollSizeFactor()
color: MyStyle.props.color("chatChatPanelBackground")
}
transientScrollBars: false
}
}
but my issues here are
flickableItem.atYEnd is always false
flickableItem.onAtYEndChanged is not triggered even if I scroll at the bottom
update() is triggered by onContentYChanged() instead when i try to scroll using scrollbar.
What is wrong with this and what areas I need to look into?
Here's a minimum working example depicting the detection of Vertical scroll bar at its bottom:
ListModel {
id: contactModel
ListElement {
name: "Bill Smith"
number: "555 3264"
}
ListElement {
name: "John Brown"
number: "555 8426"
}
ListElement {
name: "Sam Wise"
number: "555 0473"
}
ListElement {
name: "Bill Smith"
number: "555 3264"
}
ListElement {
name: "John Brown"
number: "555 8426"
}
ListElement {
name: "Sam Wise"
number: "555 0473"
}
}
Rectangle {
width: 180; height: 50
Component {
id: contactDelegate
Item {
width: 180; height: 40
Column {
Text { text: '<b>Name:</b> ' + name }
Text { text: '<b>Number:</b> ' + number }
}
}
}
}
ScrollView {
width: 200
height: 60
ListView {
model: contactModel
delegate: contactDelegate
onAtYEndChanged: console.log("Vertical Scroll Bar's bottom reached = ", atYEnd)
}
}
I have a QML application and problems with MouseAreas.
In a small test app, there is a red rectangle and when mouse enters this rect, a grey menu appears below (created with a Loader).
This grey menu must be open while mouse is over the red rectangle or the menu. For this purpose, I have 2 MouseAreas, 1 over the red rect and 1 over the menu.
Both are 'hoverEnabled' and with 'enter' and 'exit' I control 'hoverDialog' and 'hoverTopZone'.
When both are false, it means that the mouse is out, so I close the menu (using a signal, the Loader gets inactive).
The timer is required since when passing from 'mouseAreaTopZone' to 'mouseAreaDialog' there is just a moment with 'hoverDialog' and 'hoverTopZone' are both false.
Fixed with the timer.
In the middle of the menu there is a green rect, and (only) when mouse is over there, a yellow rect must be visible.
There is my problem. I have a MouseArea inside the green rect, but the yellow rect is not visible when required.
If I move 'rectGreen' below 'mouseAreaTopZone' and 'mouseAreaDialog' (that is, at the end of the file) I get the yellow rect visible when the mouse is over green rect, since its mouse area is then 'topmost'
BUT in this case, the menu dialog is closed, since when the mouse enters the MouseArea inside green rect, hoverDialog and hoverTopZone are false...
I hope U can understand my problem... Here is my code:
Test.qml
import QtQuick 2.5
import QtQuick.Controls 1.3
import QtQuick.Window 2.0
Item {
width: 800
height: 800
Rectangle{
id: rect
anchors { top: parent.top; topMargin: 100; horizontalCenter: parent.horizontalCenter }
height: 50; width: 50
color: "red"
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: loader_dialog.active = true
}
}
Loader {
id: loader_dialog
anchors { top: rect.bottom; horizontalCenter: rect.horizontalCenter}
active: false
sourceComponent: TestMenu {
onClose: loader_dialog.active = false;
}
}
}
TestMenu.qml
import QtQuick 2.0
Rectangle {
id: id_dialog
signal close()
width: 400
height: 600
color: "lightgrey"
property bool hoverDialog: false
property bool hoverTopZone: false
function update() {
if (!hoverDialog && !hoverTopZone)
timer.start();
}
function check() {
if (!hoverDialog && !hoverTopZone)
{
console.log("close");
id_dialog.close();
}
}
Timer {
id: timer
interval: 100
running: false
repeat: false
onTriggered: check();
}
Rectangle {
id: rectGreen
width: 200; height: 100
anchors.centerIn: parent
color: "green"
Rectangle {
id: rectYellow
anchors.centerIn: parent
width: 50; height: 50
color: "yellow"
visible: false
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: { rectYellow.visible = true; }
onExited: { rectYellow.visible = false }
}
}
MouseArea {
id: mouseAreaTopZone
anchors { bottom: parent.top; horizontalCenter: parent.horizontalCenter}
width: 50; height: 50
hoverEnabled: true
onEntered: { hoverTopZone = true; id_dialog.update(); }
onExited: { hoverTopZone = false; id_dialog.update(); }
}
MouseArea {
id: mouseAreaDialog
anchors.fill: parent
hoverEnabled: true
onEntered: { hoverDialog = true; id_dialog.update(); }
onExited: { hoverDialog = false; id_dialog.update(); }
}
}
Thanks in advance,
Diego
Thanks Mark Ch for your help.
I need to close the dialog when the mouse exits, so I think I can not use 'Popup' control...
I solved the problem. Using only one variable to know if the mouse is over my dialog ('m_iNumHovered'), I add a reference every time I enter in a Mouse Area, and I decrease it when I exit. The key was to add/remove a reference in the MouseArea over the green rectangle, to keep it 'm_iNumHovered=true' (dialog visible)
New code for TestMenu.qml:
import QtQuick 2.0
Rectangle {
id: id_dialog
signal close()
width: 400
height: 600
color: "lightgrey"
property int m_iNumHovered: 0
onM_iNumHoveredChanged: update();
function update() {
if (m_iNumHovered == 0)
timer.start();
}
function check() {
if (m_iNumHovered == 0)
id_dialog.close();
}
Timer {
id: timer
interval: 100
running: false
repeat: false
onTriggered: check();
}
MouseArea {
id: mouseAreaTopZone
anchors { bottom: parent.top; horizontalCenter: parent.horizontalCenter}
width: 50; height: 50
hoverEnabled: true
onEntered: m_iNumHovered++;
onExited: m_iNumHovered--;
}
MouseArea {
id: mouseAreaDialog
anchors.fill: parent
hoverEnabled: true
onEntered: m_iNumHovered++;
onExited: m_iNumHovered--;
}
Rectangle {
id: rectGreen
width: 200; height: 100
anchors.centerIn: parent
color: "green"
Rectangle {
id: rectYellow
anchors.centerIn: parent
width: 50; height: 50
color: "yellow"
visible: false
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: { m_iNumHovered++; rectYellow.visible = true; }
onExited: { m_iNumHovered--; rectYellow.visible = false }
}
}
}
We are working on a touchscreen device and we are facing an issue with the following code:
import QtQuick 2.1
import QtQuick.Window 2.0
Window {
visible: true
id: box
width: 360
height: 360
ListView {
id: list
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
model: 30
delegate: data
clip: true
focus: true
smooth: true
}
Component{
id: data
Item{
id: item
width: list.width
height: 30
clip: true; smooth: true; visible: true
property string cellColor: getCellColor(index)
Rectangle{
id: condData_item_line
width: parent.width
height: parent.height
color: cellColor
clip: true; smooth: true
Text{
text: index
anchors.centerIn: parent
}
MouseArea{
anchors.fill: parent
onClicked: {
console.log("click", index, mouse.x, mouse.y)
}
onPressed : {
console.log("press", index, mouse.x, mouse.y)
}
onReleased: {
console.log("release", index, mouse.x, mouse.y)
}
onCanceled: {
console.log("cancel")
}
}
}
}
}
function getCellColor(index){
var color;
if(index % 2 == 0){
color = "white";
}else{
color = "blue";
}
return color;
}
}
Whereas we defined handles for the clicked and released events they are never called. Is this normal? If yes, how should we code the event handling so that clicked and released events are fired on the Rectangle components?
Additional info: With this code, touching the first item (index:0), clicked and released events are fired properly. But touching the other items we only get canceled events.
import QtQuick 2.0
import Qt.labs.folderlistmodel 2.0
Item
{
Component {
id: highlight
Rectangle {
id: rooot
width: 180; height: 20
color: ListView.isCurrentItem ? "black" : "red"; radius: 5
y: list.currentItem.y
Behavior on y {
SpringAnimation {
spring: 3
damping: 0.2
}
}
}
}
ListView {
id: list
width: 480; height: 400
model: folderModel
delegate: Text { id: h; text: fileName }
highlight: highlight
highlightFollowsCurrentItem: false
focus: true
}
FolderListModel
{
id: folderModel
folder: "/home/anisha/"
nameFilters: ["*"]
}
}
This works only when I use keyboard. How to make it work on mouse clicks?
To react on mouse events you need to place MouseArea item.
In the sample below (being an expanded version of the code you provided) I have added a MouseArea to the delegate item that upon being clicked sets the ListView's currentIndex to the delegate's index (a special property visible in the ListView's delegate).
import QtQuick 2.0
import Qt.labs.folderlistmodel 2.0
Item
{
Component {
id: highlight
Rectangle {
id: rooot
width: 180; height: 20
color: ListView.isCurrentItem ? "black" : "red"; radius: 5
y: list.currentItem.y
Behavior on y {
SpringAnimation {
spring: 3
damping: 0.2
}
}
}
}
ListView {
id: list
width: 480; height: 400
model: folderModel
delegate:
Text {
id: h;
text: fileName
MouseArea {
anchors.fill: parent
onClicked: list.currentIndex = index
}
}
highlight: highlight
highlightFollowsCurrentItem: false
focus: true
}
FolderListModel
{
id: folderModel
folder: "/home/anisha/"
nameFilters: ["*"]
}
}
As an alternative approach you might try placing a single MouseArea filling the whole ListView and use ListView's indexAt(int x, int y) method to check which delegate was clicked. However, you would need to care about more edge-conditions in such case.