QML ListView place new list item below last list item - qt

I'm trying to create a simple chat bubble layout where the height of each message's rectangle can be variable while the width is fixed. Now we need to place the new message right below the last message present in the listview. Since height is variable, ListView.spacing can't be decided between any two elements. So how can I achieve this?
Listview code:
ListView{
id: listView
anchors.fill: parent
anchors.top: parent.top
anchors.topMargin: 10
delegate: ChatMessageItem{}
model: listModel
spacing: 10
}
ChatMessageItem's code
Item{
Rectangle {
color: "#6BB9F0"
height: 50
width: (childrenRect.width < 200? childrenRect.width : 200)
radius: 7
Text {
id: name
text: username
}
Text {
id: message
text: msg
anchors.top: name.bottom
wrapMode: Text.WordWrap
Component.onCompleted: {
if(width > 200)
width = 200;
else if(width < 50)
width = 50;
}
}
}
}

Your issue is that your Item does not has a height. Your ListView therefore does not know what height your delegates have. This has nothing to do with your ListView.spacing. Try the following change:
Item{
height: rect.height
Rectangle {
id: rect
color: "#6BB9F0"
height: name.paintedHeight + message.paintedHeight
width: (childrenRect.width < 200 ? childrenRect.width : 200)
radius: 7
Text {
id: name
text: username
}
Text {
id: message
text: msg
anchors.top: name.bottom
wrapMode: Text.WordWrap
Component.onCompleted: {
if(width > 200)
width = 200;
else if(width < 50)
width = 50;
}
}
}
}

Related

Calculate TableView height according to Image in Delegate

I have a TableView with the following output:
Item {
property string picture
//...
id: root
Rectangle {
id: rect
anchors.fill: parent
border.width: 1
border.color: "black"
}
Image {
id: image
anchors.centerIn: parent
source: root.picture.length > 0 ? "data:image/png;base64," + root.picture : ""
fillMode: Image.PreserveAspectFit
sourceSize.width: rect.width - rect.border.width * 2
sourceSize.height: rect.height - rect.border.height * 2
}
//...
}
So I would have expected the Image fits in the Rectangle but it does not.
The TableView looks like this:
Item {
id: root
//...
HorizontalHeaderView {
id: horizontalHeaderView
syncView: tableView
anchors.left: tableView.left
model: [qsTr("Id"), qsTr("Question"), qsTr("Answer 1"), qsTr(
"Answer 2"), qsTr("Answer 3"), qsTr("Answer 4"), qsTr(
"Correct Answer"), qsTr("Picture")]
}
TableView {
id: tableView
width: parent.width
height: parent.height - horizontalHeaderView.height
anchors.top: horizontalHeaderView.bottom
boundsBehavior: Flickable.StopAtBounds
reuseItems: true
clip: true
property var columnWidths: [60, 220, 220, 220, 220, 220, 100, 140]
columnWidthProvider: function (column) {
return columnWidths[column]
}
model: questionsProxyModel
delegate: DelegateChooser {
id: chooser
//... More DelegateChoices here for the other columns but not
//.. interesting for the issue here
DelegateChoice {
column: 7
delegate: PictureDelegate {
id: pictureDelegate
width: tableView.columnWidthProvider(column)
picture: model.picture
}
}
}
ScrollBar.vertical: ScrollBar {}
}
}
So in the TableView I provide the width to each delegate via columnWidthProvider which works fine.
The height is calculated automatically according to the content of the childs. This for example makes sure all the text always fits:
Now how can I take the heigth the Image in the delegate needs to proper scale into account?
The Issue could be fixed by making the Rectangle the root of Image and using anchors.fill instead of anchors.centerIn
Rectangle {
property string picture
//...
color: "transparent"
border.width: 1
border.color: "black"
id: root
Image {
id: image
anchors.fill: parent
anchors.margins: root.border.width
source: root.picture.length > 0 ? "data:image/png;base64," + root.picture : ""
fillMode: Image.PreserveAspectFit
}
//...
}

Line numbers/ line height for a Qml TextArea

We want to implement an embedded code editor in our QtQuick based application. For highlighting we use a QSyntaxHighlighter based on KSyntaxHighlighting. We found no way to determine the line height and line spacing that would allow us to display line numbers next to the code. Supporting dynamic line-wrap would also be a great addition.
Flickable {
id: flickable
flickableDirection: Flickable.VerticalFlick
Layout.preferredWidth: parent.width
Layout.maximumWidth: parent.width
Layout.minimumHeight: 200
Layout.fillHeight: true
Layout.fillWidth: true
boundsBehavior: Flickable.StopAtBounds
clip: true
ScrollBar.vertical: ScrollBar {
width: 15
active: true
policy: ScrollBar.AlwaysOn
}
property int rowHeight: textArea.font.pixelSize+3
property int marginsTop: 10
property int marginsLeft: 4
property int lineCountWidth: 40
Column {
id: lineNumbers
anchors.left: parent.left
anchors.leftMargin: flickable.marginsLeft
anchors.topMargin: flickable.marginsTop
y: flickable.marginsTop
width: flickable.lineCountWidth
function range(start, end) {
var rangeArray = new Array(end-start);
for(var i = 0; i < rangeArray.length; i++){
rangeArray[i] = start+i;
}
return rangeArray;
}
Repeater {
model: textArea.lineCount
delegate:
Label {
color: (!visualization.urdfPreviewIsOK && (index+1) === visualization.urdfPreviewErrorLine) ? "white" : "#666"
font: textArea.font
width: parent.width
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
height: flickable.rowHeight
renderType: Text.NativeRendering
text: index+1
background: Rectangle {
color: (!visualization.urdfPreviewIsOK && (index+1) === visualization.urdfPreviewErrorLine) ? "red" : "white"
}
}
}
}
Rectangle {
y: 4
height: parent.height
anchors.left: parent.left
anchors.leftMargin: flickable.lineCountWidth + flickable.marginsLeft
width: 1
color: "#ddd"
}
TextArea.flickable: TextArea {
id: textArea
property bool differentFromSavedState: fileManager.textDifferentFromSaved
text: fileManager.textTmpState
textFormat: Qt.PlainText
//dont wrap to allow for easy line annotation wrapMode: TextArea.Wrap
focus: false
selectByMouse: true
leftPadding: flickable.marginsLeft+flickable.lineCountWidth
rightPadding: flickable.marginsLeft
topPadding: flickable.marginsTop
bottomPadding: flickable.marginsTop
background: Rectangle {
color: "white"
border.color: "green"
border.width: 1.5
}
Component.onCompleted: {
fileManager.textEdit = textArea.textDocument
}
onTextChanged: {
fileManager.textTmpState = text
}
function update()
{
text = fileManager.textTmpState
}
}
}
As you can see we use property int rowHeight: textArea.font.pixelSize+3 to guess the line height and line spacing but that of course breaks as soon as DPI or other properties of the system change.
The TextArea type has two properties contentWidth and contentHeight which contains the size of the text content.
So, if you divide the height by the number of lines (which you can get with the property lineCount), you will get the height of a line:
property int rowHeight: textArea.contentHeight / textArea.lineCount
But, if you plan to have multiple line spacing in the same document, you will have to handle each line by manipulating the QTextDocument:
class LineManager: public QObject
{
Q_OBJECT
Q_PROPERTY(int lineCount READ lineCount NOTIFY lineCountChanged)
public:
LineManager(): QObject(), document(nullptr)
{}
Q_INVOKABLE void setDocument(QQuickTextDocument* qdoc)
{
document = qdoc->textDocument();
connect(document, &QTextDocument::blockCountChanged, this, &LineManager::lineCountChanged);
}
Q_INVOKABLE int lineCount() const
{
if (!document)
return 0;
return document->blockCount();
}
Q_INVOKABLE int height(int lineNumber) const
{
return int(document->documentLayout()->blockBoundingRect(document->findBlockByNumber(lineNumber)).height());
}
signals:
void lineCountChanged();
private:
QTextDocument* document;
};
LineManager* mgr = new LineManager();
QQuickView *view = new QQuickView;
view->rootContext()->setContextProperty("lineCounter", mgr);
view->setSource(QUrl("qrc:/main.qml"));
view->show();
Repeater {
model: lineCounter.lineCount
delegate:
Label {
color: "#666"
font: textArea.font
width: parent.width
height: lineCounter.height(index)
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
text: index+1
background: Rectangle {
border.color: "black"
}
}
}
I found a QML only solution:
Use TextEdit instead of TextArea to avoid alignment issues between line numbers and text
Use a 'ListView' to generate the line numbers for the text edit:
Here is an initial solution:
RowLayout {
anchors.fill: parent
ListView {
Layout.preferredWidth: 30
Layout.fillHeight: true
model: textEdit.text.split(/\n/g)
delegate: Text { text: index + 1 }
}
TextEdit {
id: textEdit
Layout.fillWidth: true
Layout.fillHeight: true
}
}
The ListView has a complete copy of each row of text. We can use this copy to compute the line height (taking into account of word wrap). We do this by creating an invisible Text. We can improve the answer further by adding a Flickable to the TextEdit and synchronize the scroll between the ListView and the TextEdit:
Here is a more complete solution:
// NumberedTextEdit.qml
import QtQuick 2.12
import QtQuick.Controls 2.5
Item {
property alias lineNumberFont: lineNumbers.textMetrics.font
property color lineNumberBackground: "#e0e0e0"
property color lineNumberColor: "black"
property alias font: textEdit.font
property alias text: textEdit.text
property color textBackground: "white"
property color textColor: "black"
Rectangle {
anchors.fill: parent
color: textBackground
ListView {
id: lineNumbers
property TextMetrics textMetrics: TextMetrics { text: "99999"; font: textEdit.font }
model: textEdit.text.split(/\n/g)
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: 10
width: textMetrics.boundingRect.width
clip: true
delegate: Rectangle {
width: lineNumbers.width
height: lineText.height
color: lineNumberBackground
Text {
id: lineNumber
anchors.horizontalCenter: parent.horizontalCenter
text: index + 1
color: lineNumberColor
font: textMetrics.font
}
Text {
id: lineText
width: flickable.width
text: modelData
font: textEdit.font
visible: false
wrapMode: Text.WordWrap
}
}
onContentYChanged: {
if (!moving) return
flickable.contentY = contentY
}
}
Item {
anchors.left: lineNumbers.right
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: 10
Flickable {
id: flickable
anchors.fill: parent
clip: true
contentWidth: textEdit.width
contentHeight: textEdit.height
TextEdit {
id: textEdit
width: flickable.width
color: textColor
wrapMode: Text.WordWrap
}
onContentYChanged: {
if (lineNumbers.moving) return
lineNumbers.contentY = contentY
}
}
}
}
}
I've found that you can query the line height using FontMetrics and then getting the true height by Math.ceil(fontMetrics.lineSpacing) for example:
TextEdit {
id: textArea
FontMetrics {
id: fontMetricsId
font: textArea.font
}
Component.onCompleted: {
console.log("Line spacing:" + Math.ceil(fontMetricsId.lineSpacing)
}
}

Long TabBar - adding a size and position indicator to reflect the presence of off-screen tabs

I have a tab bar with a stacklayout like the following:
Rectangle {
id: rect
height: 190
anchors.right: parent.right
anchors.left: parent.left
color: "transparent"
anchors.top: uniqueHandleText.bottom
anchors.topMargin: 100
TabBar {
id: frame
anchors.right: parent.right
anchors.left: parent.left
background: Rectangle {
color: "#737373"
}
x: -hbar.position * width
Repeater {
model: wizard.categories
TabButton {
id: tabData
property bool selected: false
text: modelData.name
width: 200
font.pixelSize: 18
contentItem: Text {
text: tabData.text
font: tabData.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
wrapMode: Text.WordWrap
color: "#FFFFFF"
}
background: Rectangle {
implicitWidth: frame.width
implicitHeight: 180
opacity: enabled ? 1 : 0.3
color: tabData.checked ? "#BD9CBE": "#737373"
}
}
}
}
ScrollBar {
id: hbar
hoverEnabled: true
active: hovered || pressed
orientation: Qt.Horizontal
size: rect.width / frame.width
anchors.left: parent.left
anchors.right: parent.right
anchors.top: frame.bottom
}
Text {
font.pixelSize: 18
text: "Next"
anchors.right: parent.right
visible: frame.x != frame.width ? true: false
}
StackLayout {
id: stack1
anchors.left: parent.left
anchors.right: parent.right
anchors.top: frame.bottom
currentIndex: frame.currentIndex
Repeater {
model: wizard.categories
Item {
id: homeTab
TabBar {
id: homeTabTab
anchors.right: parent.right
anchors.left: parent.left
anchors.top: parent.top
height: 180
background: Rectangle {
color: "#958096"
}
Repeater {
model: modelData.sub_categories
TabButton {
property bool selected: false
id: currentTab
text: modelData.name
width: 200
font.pixelSize: 18
background: Rectangle {
implicitWidth: frame.width
implicitHeight: 180
opacity: enabled ? 1 : 0.3
color: currentTab.checked ? "#958096": "#8D758E"
}
contentItem: Text {
text: currentTab.text
font: currentTab.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
wrapMode: Text.WordWrap
color: "#FFFFFF"
MouseArea {
anchors.fill: parent
onClicked: {
if(currentTab.checked){
currentTab.checked = false
} else {
currentTab.checked = true
}
}
onDoubleClicked: {
currentTab.selected = true
var found = false;
var someText = frame.itemAt(stack1.currentIndex).text;
print(someText)
for(var i = 0; i<wizard.selectedSkills.count; i++){
if(wizard.selectedSkills.get(i).name === someText){
wizard.selectedSkills.get(i).sub_categories.append({"name":currentTab.text});
wizard.skills.push({"name": someText})
found = true;
}
}
if(!found){
print(currentTab.text)
wizard.selectedSkills.append({"name":someText, "sub_categories":[{"name":currentTab.text}]})
}
print(window.selectedSkills)
}
}
}
}
}
}
}
}
}
}
I've tried many different things to add a scrollbar or to figure out how to use the flickable functionality that TabBar has. However, the documentation doesn't specify how it works, it just does. Therefore, they are not accessible (or even rewritteable, to use those properties). I want to add a small indicator like an arrow to specify that there is more elements for ease of navigation on desktop on top of the TabBar functionality.
It doesn't seem like the necessary properties are exposed in order to make this happen the easy way.
However, since this is QML, it means the whole object tree is gaping wide open to introspection, allowing us to establish that the item that does the flicking is the contentItem of a ListView inside the Container the ToolBar inherits. The view happens to be the second visible child, although this is technically "private implementation" that one should not rely on. So it is better to take some extra care to establish whether or not you have the correct object.
ApplicationWindow {
id: main
width: 640
height: 480
visible: true
TabBar {
id: toolbar
width: parent.width
height: 50
Repeater {
model: 10
TabButton {
text: "test " + index
width: 100
}
}
}
Rectangle {
height: 5
width: main.width * (view ? view.visibleArea.widthRatio : toolbar.width / toolbar.contentWidth)
color: "red"
anchors.top: toolbar.bottom
x: view ? (main.width - width) * (view.contentX / (view.contentWidth - view.width)) : 0
}
property ListView view: {
var l = toolbar.visibleChildren.length
while (--l) if ("cacheBuffer" in toolbar.visibleChildren[l]) return toolbar.visibleChildren[l]
return null
}
}
And there you have it. We iterate the tabview children until we find one that has a property cacheBuffer which is fairly unique to ListView, and once we have that, we can access the needed properties. As you see, for the indicator width we can do even without the list view, as the toolbar exposes a contentWidth property, but for the indicator position there is no workaround.
And it works:

How to drag an item outside a ListView in QML

I am developing a QML application which basically contains two ListView. I would like to copy a QML item from one ListView to another. I tried to handle this by setting Drag property in the delegate but the item cannot go outside the view when I drag the item, I think the Flickable container handles mouse events.
So, I want to try the following:
create a mousearea which overlaps the to ListView
create a new object by calling **createComponent() / createObject()**
reparent this object to the mousearea
handle mouse events in the mousearea till drop
This solution seems to me a little complicated, so do you have a better way to achieve this ?
This was a bad idea and too much complicated. I think I got a way to achieve this:
each delegate of the ListView has a hidden Item which can be dragged,
as my ListView are in a reusable component, I use a property to pass a higher item (a Rectangle here and NOT a **MouseArea**) which can be used as parent for dragged items,
the higher item contains the two ListView (and maybe more in the future),
when the drag begins, the item is set to visible and reparented using a **State**
So, I missed the point that set the parent should solve my problem.
Next code is just an idea, but the key is to have a MouseArea inside a delegate for the first ListView so the user can drag the items and drop them into a DropArea which belongs to the second ListView.
In this example, model is very simple, just a number. And when the item is dropped, it is removed from the first ListView:
listView.model.remove(listView.dragItemIndex)
Just remove that line of code to copy the item instead of removing.
main.qml
import QtQuick 2.5
import QtQuick.Window 2.2
Window {
visible: true
width: 600
height: 600
Rectangle {
id: root
width: 400
height: 400
ListView {
id: listView
width: parent.width / 2
height: parent.height
property int dragItemIndex: -1
model: ListModel {
Component.onCompleted: {
for (var i = 0; i < 10; ++i) {
append({value: i});
}
}
}
delegate: Item {
id: delegateItem
width: listView.width
height: 50
Rectangle {
id: dragRect
width: listView.width
height: 50
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
color: "salmon"
border.color: Qt.darker(color)
Text {
anchors.centerIn: parent
text: modelData
}
MouseArea {
id: mouseArea
anchors.fill: parent
drag.target: dragRect
drag.onActiveChanged: {
if (mouseArea.drag.active) {
listView.dragItemIndex = index;
}
dragRect.Drag.drop();
}
}
states: [
State {
when: dragRect.Drag.active
ParentChange {
target: dragRect
parent: root
}
AnchorChanges {
target: dragRect
anchors.horizontalCenter: undefined
anchors.verticalCenter: undefined
}
}
]
Drag.active: mouseArea.drag.active
Drag.hotSpot.x: dragRect.width / 2
Drag.hotSpot.y: dragRect.height / 2
}
}
}
ListView {
id: listView2
width: parent.width / 2
height: parent.height
anchors.right: parent.right
property int dragItemIndex: -1
DropArea {
id: dropArea
anchors.fill: parent
onDropped: {
listView2.model.append(listView.model.get(listView.dragItemIndex))
listView.model.remove(listView.dragItemIndex)
listView.dragItemIndex = -1;
}
}
model: ListModel {
Component.onCompleted: {
for (var i = 0; i < 1; ++i) {
append({value: i});
}
}
}
delegate: Item {
id: delegateItem2
width: listView2.width
height: 50
Rectangle {
id: dragRect2
width: listView2.width
height: 50
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
color: "salmon"
border.color: Qt.darker(color)
Text {
anchors.centerIn: parent
text: modelData
}
}
}
}
}
}

Qt, QML ListView and Desktop App

My question is kind of a two part conditional question. I have a desktop application I'm writing in C++/Qt. In the app I have a couple lists that I want to decorate and add list items with icons and rich text.
I first attempted to do this with the QWidget world but the more I looked into it, the more I thought QML might be a better option. But now I'm wondering about that as well since it seems that QML Is more geared toward touch screen devices. Not to mention that my progress with QML has been frusating. Give them QML below, I cannot figure out how to: (1) get an item to highlight when I click it and (2) add a scroll bar:
import QtQuick 1.0
Item
{
width: 300
height: 200
ListModel
{
id: myModel2
ListElement { text: "List Item 1" }
ListElement { text: "List Item 2" }
ListElement { text: "List Item 3" }
ListElement { text: "List Item 4" }
ListElement { text: "List Item 5" }
ListElement { text: "List Item 6" }
}
Component
{
id: beerDelegate
Rectangle
{
id: beerDelegateRectangle
height: beerDelegateText.height * 1.5
width: parent.width
Text
{
id: beerDelegateText
text: "<b>" + modelData + "</b> <i>(" + modelData + ")</i>"
}
MouseArea
{
anchors.fill: parent
onClicked:
{
console.log("clicked: " + modelData + " at index: " + index);
beerList.currentIndex = index;
}
}
}
}
ListView
{
id: beerList
anchors.fill: parent
model: myModel2
delegate: beerDelegate
highlightFollowsCurrentItem: true
highlight: Rectangle
{
width: parent.width
color: "red"
}
focus: true
}
}
How can I accomplish what I'm looking for given this QML? Or is using QML in a QWidget desktop app just a bad idea all around?
For the first question (highlight):
Your list actually draws the highlight, however, your item delegate overpaints this with a white rectangle! Just replace the rectangle with an item and it works:
Component
{
id: beerDelegate
Item
{
...
}
}
For the second question (scroll bars):
As far as I know, QML doesn't provide scroll bars out of the box. There is however the Qt Desktop Components project (git repository) which gives you access to most of the widgets in the QML world. Among them, there is a ScrollArea.
It is no longer necessary to implement the Scrollbars yourself. There is the ScrollView-Item since Qt 5.1. Simply surround a Flickable-Item (e.g. the ListView-Item you use, is also "Flickable") with the ScrollView-Item and you'll be fine:
ScrollView {
ListView {
id: beerList
anchors.fill: parent
model: myModel2
delegate: beerDelegate
highlightFollowsCurrentItem: true
highlight: Rectangle
{
width: parent.width
color: "red"
}
focus: true
}
}
For the second question. i.e Scroll-bar on ListView:
I have created code for scroll bar on ListView. It also can work on the GridView
ScrollBar.qml
import Qt 4.7
Item {
property variant target
width: 8
anchors.top: target.top
anchors.bottom: target.bottom
anchors.margins: 1
anchors.rightMargin: 2
anchors.bottomMargin: 2
anchors.right: target.right
visible: (track.height == slider.height) ? false : true
Image {
id: scrollPath
width: 2
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
source: "qrc:/resources/buttons/slider2.png"
}
Item {
anchors.fill: parent
Timer {
property int scrollAmount
id: timer
repeat: true
interval: 20
onTriggered: {
target.contentY = Math.max(0, Math.min(target.contentY + scrollAmount,
target.contentHeight - target.height));
}
}
Item {
id: track
anchors.top: parent.top
anchors.topMargin: 1
anchors.bottom: parent.bottom
width: parent.width
MouseArea {
anchors.fill: parent
onPressed: {
timer.scrollAmount = target.height * (mouseY < slider.y ? -1 : 1)
timer.running = true;
}
onReleased: {
timer.running = false;
}
}
Image {
id:slider
anchors.horizontalCenter: parent.horizontalCenter
source: "qrc:/resources/buttons/slider.png"
width: parent.width
height: Math.min(target.height / target.contentHeight * track.height, track.height) < 20 ? 20 : Math.min(target.height / target.contentHeight * track.height, track.height)
y: target.visibleArea.yPosition * track.height
MouseArea {
anchors.fill: parent
drag.target: parent
drag.axis: Drag.YAxis
drag.minimumY: 0
drag.maximumY: track.height - height
onPositionChanged: {
if (pressedButtons == Qt.LeftButton) {
target.contentY = slider.y * target.contentHeight / track.height;
}
}
}
}
}
}
}
And I used scroll bar item with ListView in MyListView.qml as:
MyListView.qml
ListView {
id: list
clip: true
anchors.margins: 5
anchors.fill: parent
model: 10
delegate: trackRowDelegate
interactive: contentHeight > height
}
ScrollBar {
id: verticalScrollBar
target: list
clip: true
}
This ScrollBar item can be used with GridView as
GridView {
id: grid
clip: true
anchors.margins: 5
anchors.fill: parent
cellWidth:100
cellHeight: 100
model: items
interactive: contentHeight > height
snapMode: GridView.SnapToRow
delegate: myDelegate
}
ScrollBar {
id: verticalScrollBar
target: grid
clip: true
visible: grid.interactive
}

Resources