from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
from PySide6.QtCore import QObject, Signal, Slot, Property, Qt,QUrl, QTimer, QAbstractListModel
class CpuLoadModel(QAbstractListModel):
def __init__(self):
QAbstractListModel.__init__(self)
self.__update_timer = QTimer(self)
self.__update_timer.setInterval(1000)
self.__update_timer.timeout.connect(self.__update)
self.__update_timer.start()
self.todos = []
def __update(self):
self.todos.append(random.randint(0, 99))
self.layoutChanged.emit()
print(self.todos)
self.dataChanged.emit(self.index,self.index,[])
def data(self, index, role):
if role == Qt.DisplayRole:
text = self.todos[index.row()]
print(f"Text: {index.row()} role : {role}")
print("--------")
return text
def rowCount(self, index):
return len(self.todos)
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
qmlRegisterType(CpuLoadModel, 'PsUtils', 1, 0, 'CpuLoadModel')
engine.load(QUrl("main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
self.dataChange.emit method is not working at all, however self.LayoutChange.emit works but with those mistakes from qml
TypeError: Cannot read property 'width' of null
The layoutChanged signal must be emitted when something in the model has changed (for example it has been reordered), and dataChanged when any of the items change data but none of them is used to indicate that a row was added, in that case it must use the beginInsertRows and endInsertRows methods.
def __update(self):
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self.todos.append(random.randint(0, 99))
self.endInsertRows()
Also change to:
def rowCount(self, index=QModelIndex()):
if index.isValid():
return 0
return len(self.todos)
import QtQuick 2.0
import QtQuick.Window 2.0
import QtQuick.Controls 2.0
import PsUtils 1.0
Window {
id: root
width: 640
height: 480
visible: true
title: "CPU Load"
ListView {
id:listviewId
anchors.fill: parent
model: CpuLoadModel { }
delegate: Rectangle {
width: listviewId.width
height: 30;
color: "white"
Rectangle {
id: bar
width: listviewId.width * display / 100.0
height: 30
color: "green"
}
Text {
anchors.verticalCenter: parent.verticalCenter
x: Math.min(bar.x + bar.width + 5, parent.width-width)
text: display + "%"
}
}
}
}
Basically the mistake was the parent.width in QML we better off to use id to refer to element instead of using parent.width ( use elementId.width)
Related
I've been trying to make for example 10 different elements for example buttons and I could do that using the repeater but I've been having trouble with setting the text for each new element.
I'm getting the texts I want to set from a list in Python and I sent them to qml through QStringListModel. The text got to qml from the list as I wanted, but somehow the repeater set's the text of all the elements as the last string in the list given from Python.
I will provide the code later for extra explanation but for now I want to see if someone have idea how I can do it...(the thing is that the code is in another device and I'm waiting for it). Basically what I'm trying to do is, let's say for example, I have a list in python a_list = {buttonName, buttonAge, buttonHeight} and I want to make multiple buttons in qml as the size of the list (in this case 3) and change each text of the buttons i made in the repeater as the strings in the list).
this is main.py
import sys
from PySide2.QtCore import QUrl
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
from foo import FooController
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
fooController = FooController()
engine.rootContext().setContextProperty("fooController", fooController)
engine.load(QUrl.fromLocalFile('main.qml'))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
This is foo.py
from PySide2.QtCore import QObject, Property, Slot
class x:
def __init__(self, name, age):
self.name = name
self.age = age
class FooController(QObject):
def __init__(self, parent=None):
QObject.__init__(self, parent)
self.__text = "Foo"
self.__name = s1.name
self.__age = s1.age
#Property(str)
def text(self):
return self.__name
#Slot()
def clickListener(self):
print(self.__name)
This is foo.qml
import QtQuick 2.9
import QtQuick.Controls 2.2
Button {
text: fooController.text
onClicked: fooController.clickListener()
}
and here is the qml window that contains the repeater
import QtQuick 2.0
import "../components"
//import QtQuick.Timeline 1.0
import QtQuick.Controls 2.15
import QtQuick 2.15
import QtQuick.Window 2.15
import QtGraphicalEffects 1.15
import QtQuick.Layouts 1.15
import "../main.py"
window{
width: 1500
height: 920
minimumWidth: 1100
minimumHeight: 650
visible: true
color: "#00000000"
id: mainWindow
title: qsTr("--")
Rectangle{
id: rectangle
anchors.fill: parent
anchors.rightMargin: 0
anchors.bottomMargin: 0
anchors.leftMargin: 0
anchors.topMargin: 0
radius: 10
color: "#4642b6"
Flickable {
id: flickable
contentHeight: gridLayoutBottom.height
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: 96
anchors.rightMargin: 8
anchors.leftMargin: 8
anchors.bottomMargin: 8
clip: true
ListModel {
id: imageModel
ListElement { _id: "tile0" }
}
Repeater {
model: imageModel
delegate: CustomMenuType{
ListView{
model: s
delegate: Text {
text: model.display
font.family: "Segoe UI"
color: "#ffffff"
}
}
//text: ListView.delegate.Text.text
font.pointSize: 9
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
}
}
}
Disclaimer: The code you provide is useless since it does not show any attempt to solve your problem, besides that there are undefined elements that I will not take as a basis for my answer, and it is a shame because you always learn more by correcting errors.
If you want to handle the information that the Repeater uses from Python then you must use a model. The Repeater supports 3 types of models:
A number,
A list or
An object that inherits from QAbstractItemModel.
In this case the first does not provide important information since it only indicates the number of elements so it will not show it since it is a trivial example.
In the case of the list the logic is to create a Q_PROPERTY of type "QVariantList" (in PyQt5 list is enough) and that has an associated signal that is emitted every time the list changes so that the view is notified and updates the painting.
import os.path
import sys
from PySide2.QtCore import Property, QObject, QDateTime, QTimer, QUrl, Signal
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
CURRENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
class Controller(QObject):
modelChanged = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self._model = ["Text1", "Text2", "Text3"]
#Property("QVariantList", notify=modelChanged)
def model(self):
return self._model
def update_model(self, l):
self._model = l[:]
self.modelChanged.emit()
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
controller = Controller()
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty("controller", controller)
filename = os.path.join(CURRENT_DIRECTORY, "main.qml")
engine.load(QUrl.fromLocalFile(filename))
if not engine.rootObjects():
sys.exit(-1)
def on_timeout():
dt = QDateTime.currentDateTime()
l = [dt.addSecs(i).toString() for i in range(3)]
controller.update_model(l)
timer = QTimer(timeout=on_timeout, interval=1000)
timer.start()
sys.exit(app.exec_())
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
Window{
width: 640
height: 480
visible: true
Column {
Repeater {
model: controller.model
Button {
text: model.modelData
}
}
}
}
In the case of QAbstractItemModel the logic is to create a QProperty of type QObject and make it constant since the model itself does not change but the information it manages. And on the QML side, the property must be accessed using the associated role, for example in the case of QStringListModel the role is "display":
import os.path
import sys
from PySide2.QtCore import Property, QDateTime, QObject, QStringListModel, QTimer, QUrl
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
CURRENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
class Controller(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._model = QStringListModel()
self.model.setStringList(["Text1", "Text2", "Text3"])
def get_model(self):
return self._model
model = Property(QObject, fget=get_model, constant=True)
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
controller = Controller()
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty("controller", controller)
filename = os.path.join(CURRENT_DIRECTORY, "main.qml")
engine.load(QUrl.fromLocalFile(filename))
if not engine.rootObjects():
sys.exit(-1)
def on_timeout():
dt = QDateTime.currentDateTime()
l = [dt.addSecs(i).toString() for i in range(3)]
controller.model.setStringList(l)
timer = QTimer(timeout=on_timeout, interval=1000)
timer.start()
sys.exit(app.exec_())
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
Window{
width: 640
height: 480
visible: true
Column {
Repeater {
model: controller.model
Button {
text: model.display
}
}
}
}
You can also create a custom model but you have to declare a role associated with the data, a trivial example is to use the QStandardItemModel class.
import os.path
import sys
from PySide2.QtCore import (
Property,
QDateTime,
QObject,
Qt,
QTimer,
QUrl,
)
from PySide2.QtGui import QGuiApplication, QStandardItem, QStandardItemModel
from PySide2.QtQml import QQmlApplicationEngine
CURRENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
TEXT_ROLE = Qt.UserRole + 1000
DATA_ROLE = TEXT_ROLE + 1
class Controller(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._model = QStandardItemModel()
self.model.setItemRoleNames({TEXT_ROLE: b"text", DATA_ROLE: b"data"})
for i in range(3):
item = QStandardItem()
item.setData("Text{}".format(i), TEXT_ROLE)
item.setData(i, DATA_ROLE)
self.model.appendRow(item)
def get_model(self):
return self._model
model = Property(QObject, fget=get_model, constant=True)
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
controller = Controller()
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty("controller", controller)
filename = os.path.join(CURRENT_DIRECTORY, "main.qml")
engine.load(QUrl.fromLocalFile(filename))
if not engine.rootObjects():
sys.exit(-1)
def on_timeout():
dt = QDateTime.currentDateTime()
for i in range(controller.model.rowCount()):
item = controller.model.item(i)
item.setData(dt.addSecs(i).toString(), TEXT_ROLE)
item.setData(dt.addSecs(i), DATA_ROLE)
timer = QTimer(timeout=on_timeout, interval=1000)
timer.start()
sys.exit(app.exec_())
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
Window{
width: 640
height: 480
visible: true
Column {
Repeater {
model: controller.model
Button {
text: model.text
onClicked: console.log(model.data)
}
}
}
}
You could also create a class that inherits from QAbstractListModel and overrides the data, setData, roleNames, rowCount methods.
I need to add a header to a TableView that uses a custom model defined in Python.
I've tried overriding the headerData function in QAbstractTableModel with my custom headers. I'm following the same series of steps described in this C++ example of the same type of implementation: Header to a TableView
Unfortunately, the headers still don't show up at the top of the table. The table does however contain the data from overriding the data function in QAbstractTableModel.
Python:
class RouteTableModel(QAbstractTableModel):
def __init__(self, parent=None, *args):
super().__init__()
self._datatable = None
self._header = {
0 : 'X',
1 : 'Y',
2 : 'Z'
}
def data(self, index, role=Value):
i = index.row()
j = index.column()
if role == self.Value:
return '{0}'.format(self._datatable[i][j]['value'])
elif role == self.Selected:
return self._datatable[i][j]['selected']
else:
return None
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return self._header[section]
else:
return None
else:
return None
# create the application instance
app = QApplication(sys.argv)
# create a QML engine
engine = QQmlApplicationEngine()
# instantiate the TableModel class
xyztablemodel = RouteTableModel()
engine.rootContext().setContextProperty('XYZTableModel', xyztablemodel)
# load main QML file and start app engine
engine.load('view.qml')
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
QML:
import QtQuick 2.12
import QtQuick.Controls 2.12
GridLayout {
id: gridfpc2
flow: GridLayout.TopToBottom
rows: 4
columns: 2
rowSpacing: 20
columnSpacing: 35
TableView {
id: xyztable
Layout.rowSpan: 4
// Layout.alignment: Qt.AlignCenter
Layout.fillHeight: true
model: XYZTableModel
width: 350
delegate: CustomComp.XYZTableDelegate {
implicitWidth: parent.width / 3
implicitHeight: 20
}
}
}
No error messages occur in the Python or qml code. I'd expect the header to populate above the columns in the TableView, but they do not show up.
In the example you indicate in your question, it is from a QTableView that is very different from the TableView offered by QML. In your case you are using the TableView of QtQuick. This TableView has no headers so you must implement it, in my example I will use Repeaters. On the other hand headerData is not accessible from QML, so I will implement the Q_INVOKABLE of C++ using #Slot(), passing as an argument the result of the type of variable that returns the function:
main.py
from PySide2 import QtCore, QtGui, QtQml
class RouteTableModel(QtCore.QAbstractTableModel):
def __init__(self, parent=None):
super().__init__(parent)
self._header = {0: "X", 1: "Y", 2: "Z"}
def columnCount(self, parent=QtCore.QModelIndex()):
return 3
def rowCount(self, parent=QtCore.QModelIndex()):
return 100
def data(self, index, role=QtCore.Qt.DisplayRole):
i = index.row()
j = index.column()
if role == QtCore.Qt.DisplayRole:
return "{}-{}".format(i, j)
#QtCore.Slot(int, QtCore.Qt.Orientation, result="QVariant")
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self._header[section]
else:
return str(section)
if __name__ == "__main__":
import os
import sys
app = QtGui.QGuiApplication(sys.argv)
engine = QtQml.QQmlApplicationEngine()
xyztablemodel = RouteTableModel()
engine.rootContext().setContextProperty("XYZTableModel", xyztablemodel)
current_dir = os.path.dirname(os.path.realpath(__file__))
filename = os.path.join(current_dir, "view.qml")
engine.load(QtCore.QUrl.fromLocalFile(filename))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
view.qml
import QtQuick 2.12
import QtQuick.Controls 2.4
import QtQuick.Window 2.11
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
color: '#222222'
TableView {
id: tableView
columnWidthProvider: function (column) { return 100; }
rowHeightProvider: function (column) { return 60; }
anchors.fill: parent
leftMargin: rowsHeader.implicitWidth
topMargin: columnsHeader.implicitHeight
model: XYZTableModel
width: 350
delegate: Rectangle {
implicitWidth: 100
implicitHeight: 50
Text {
text: display
}
}
Rectangle { // mask the headers
z: 3
color: "#222222"
y: tableView.contentY
x: tableView.contentX
width: tableView.leftMargin
height: tableView.topMargin
}
Row {
id: columnsHeader
y: tableView.contentY
z: 2
Repeater {
model: tableView.columns > 0 ? tableView.columns : 1
Label {
width: tableView.columnWidthProvider(modelData)
height: 35
text: XYZTableModel.headerData(modelData, Qt.Horizontal)
color: '#aaaaaa'
font.pixelSize: 15
padding: 10
verticalAlignment: Text.AlignVCenter
background: Rectangle { color: "#333333" }
}
}
}
Column {
id: rowsHeader
x: tableView.contentX
z: 2
Repeater {
model: tableView.rows > 0 ? tableView.rows : 1
Label {
width: 40
height: tableView.rowHeightProvider(modelData)
text: XYZTableModel.headerData(modelData, Qt.Vertical)
color: '#aaaaaa'
font.pixelSize: 15
padding: 10
verticalAlignment: Text.AlignVCenter
background: Rectangle { color: "#333333" }
}
}
}
ScrollIndicator.horizontal: ScrollIndicator { }
ScrollIndicator.vertical: ScrollIndicator { }
}
}
I have 2 pandas dataframes, one with 6 rows and 7 columns and another one with 21 rows and 12 columns, i would like to display them using QML, while googling i got to know about TableView TableView QML Type. In that example, the data has been entered already in ListElement and it is being shown. But in my case the data is from python pandas dataframe, how can i fetch the columns and rows from pandas straight into QML, should i create those columns well before under listelement? Any help would be appreciated
Note: I have used QtWidgets to display dataframe, it was just a drag and drop tableview and use the model created and set them using tableView.setModel(model). But i am kind of clueless here in QML
You can create a class that inherits from QAbstractTableModel as implemented in this old answer and use the QTableView of QtQuick 2.12 that was released in Qt 5.12 since it supports the QAbstractTableModel type model, so you will have to use the latest versions of PyQt5.
main.py
from PyQt5 import QtCore, QtGui, QtQml
import numpy as np
import pandas as pd
class DataFrameModel(QtCore.QAbstractTableModel):
DtypeRole = QtCore.Qt.UserRole + 1000
ValueRole = QtCore.Qt.UserRole + 1001
def __init__(self, df=pd.DataFrame(), parent=None):
super(DataFrameModel, self).__init__(parent)
self._dataframe = df
def setDataFrame(self, dataframe):
self.beginResetModel()
self._dataframe = dataframe.copy()
self.endResetModel()
def dataFrame(self):
return self._dataframe
dataFrame = QtCore.pyqtProperty(pd.DataFrame, fget=dataFrame, fset=setDataFrame)
#QtCore.pyqtSlot(int, QtCore.Qt.Orientation, result=str)
def headerData(self, section: int, orientation: QtCore.Qt.Orientation, role: int = QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self._dataframe.columns[section]
else:
return str(self._dataframe.index[section])
return QtCore.QVariant()
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return len(self._dataframe.index)
def columnCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return self._dataframe.columns.size
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid() or not (0 <= index.row() < self.rowCount() \
and 0 <= index.column() < self.columnCount()):
return QtCore.QVariant()
row = self._dataframe.index[index.row()]
col = self._dataframe.columns[index.column()]
dt = self._dataframe[col].dtype
val = self._dataframe.iloc[row][col]
if role == QtCore.Qt.DisplayRole:
return str(val)
elif role == DataFrameModel.ValueRole:
return val
if role == DataFrameModel.DtypeRole:
return dt
return QtCore.QVariant()
def roleNames(self):
roles = {
QtCore.Qt.DisplayRole: b'display',
DataFrameModel.DtypeRole: b'dtype',
DataFrameModel.ValueRole: b'value'
}
return roles
if __name__ == "__main__":
import os
import sys
app = QtGui.QGuiApplication(sys.argv)
df = pd.DataFrame(np.random.randint(0, 100, size=(6, 7)), columns=list('ABCDEFG'))
print(df)
model = DataFrameModel(df)
engine = QtQml.QQmlApplicationEngine()
engine.rootContext().setContextProperty("table_model", model)
qml_path = os.path.join(os.path.dirname(__file__), "main.qml")
engine.load(QtCore.QUrl.fromLocalFile(qml_path))
if not engine.rootObjects():
sys.exit(-1)
engine.quit.connect(app.quit)
sys.exit(app.exec_())
main.qml
import QtQuick 2.12
import QtQuick.Controls 2.4
import QtQuick.Window 2.11
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
color: '#222222'
TableView {
id: tableView
columnWidthProvider: function (column) { return 100; }
rowHeightProvider: function (column) { return 60; }
anchors.fill: parent
leftMargin: rowsHeader.implicitWidth
topMargin: columnsHeader.implicitHeight
model: table_model
delegate: Rectangle {
color: parseFloat(display) > 50 ? 'green' : 'red'
Text {
text: display
anchors.fill: parent
anchors.margins: 10
color: 'white'
font.pixelSize: 15
verticalAlignment: Text.AlignVCenter
}
}
Rectangle { // mask the headers
z: 3
color: "#222222"
y: tableView.contentY
x: tableView.contentX
width: tableView.leftMargin
height: tableView.topMargin
}
Row {
id: columnsHeader
y: tableView.contentY
z: 2
Repeater {
model: tableView.columns > 0 ? tableView.columns : 1
Label {
width: tableView.columnWidthProvider(modelData)
height: 35
text: table_model.headerData(modelData, Qt.Horizontal)
color: '#aaaaaa'
font.pixelSize: 15
padding: 10
verticalAlignment: Text.AlignVCenter
background: Rectangle { color: "#333333" }
}
}
}
Column {
id: rowsHeader
x: tableView.contentX
z: 2
Repeater {
model: tableView.rows > 0 ? tableView.rows : 1
Label {
width: 40
height: tableView.rowHeightProvider(modelData)
text: table_model.headerData(modelData, Qt.Vertical)
color: '#aaaaaa'
font.pixelSize: 15
padding: 10
verticalAlignment: Text.AlignVCenter
background: Rectangle { color: "#333333" }
}
}
}
ScrollIndicator.horizontal: ScrollIndicator { }
ScrollIndicator.vertical: ScrollIndicator { }
}
}
I have used:
Python 3.7.2
PyQt5 5.12.1
Qt 5.12.2
pandas 0.24.1
Note: If you want to use the example with PySide2 you only have to change pyqtProperty with Property, pyqtSlot with Slot and PyQt5 to PySide2.
I'm trying to create some markers that will be moving dynamically on a QML map. Before that, however, I need to plot them all on the map using the mouse so that I can update them later. I have been using pyqtProperty to send the coordinates I need to QML but when I try to add them to a MapItemView, they are undefined. The following code demonstrates what I am hoping to accomplish. Is the problem that I am passing a pyqtProperty to QML from another python object that was not added with setContextProperty() like in main.py? Or am I using the MapItemView delegation incorrectly?
main.qml
import QtQuick 2.7
import QtQml 2.5
import QtQuick.Controls 1.3
import QtQuick.Controls.Styles 1.3
import QtQuick.Window 2.2
import QtQuick.Layouts 1.2
import QtPositioning 5.9
import QtLocation 5.3
import QtQuick.Dialogs 1.1
ApplicationWindow {
id: root
width: 640
height: 480
visible: true
ListModel {
id: markers
}
Plugin {
id: mapPlugin
name: "osm" //"mapboxgl" "osm" "esri"
}
Map {
id: map
anchors.fill: parent
plugin: mapPlugin
center: atc.location
zoomLevel: 14
MapItemView {
model: markers
delegate: MapCircle {
radius: 50
color: 'red'
center: markLocation //issue here?
}
}
MapCircle {
id: home
center: atc.location
radius: 40
color: 'white'
}
MouseArea {
id: mousearea
anchors.fill: map
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
property var coord: map.toCoordinate(Qt.point(mouseX, mouseY))
onDoubleClicked: {
if (mouse.button === Qt.LeftButton)
{
//Capture information for model here
atc.plot_mark(
"marker",
mousearea.coord.latitude,
mousearea.coord.longitude)
markers.append({
name: "markers",
markLocation: atc.get_marker_center("markers")
})
}
}
}
}
}
atc.py
import geocoder
from DC import DC
from PyQt5.QtPositioning import QGeoCoordinate
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
class ATC(QObject):
#pyqt Signals
locationChanged = pyqtSignal(QGeoCoordinate)
def __init__(self, parent=None):
super().__init__(parent)
self._location = QGeoCoordinate()
self.dcs = {}
g = geocoder.ip('me')
self.set_location(QGeoCoordinate(*g.latlng))
def set_location(self, coordinate):
if self._location != coordinate:
self._location = coordinate
self.locationChanged.emit(self._location)
def get_location(self):
return self._location
#pyqt Property
location = pyqtProperty(QGeoCoordinate,
fget=get_location,
fset=set_location,
notify=locationChanged)
#pyqtSlot(str, str, str)
def plot_mark(self, mark_name, lat, lng):
dc = DC(mark_name)
self.dcs[mark_name] = dc
self.dcs[mark_name].set_location(
QGeoCoordinate(float(lat), float(lng)))
#pyqtSlot(str)
def get_marker_center(self, mark_name):
return self.dcs[mark_name].location
DC.py
from PyQt5.QtPositioning import QGeoCoordinate
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
class DC(QObject):
#pyqt Signals
locationChanged = pyqtSignal(QGeoCoordinate)
def __init__(self, name, parent=None):
super().__init__(parent)
self.name = name
self._location = QGeoCoordinate()
def set_location(self, coordinate):
if self._location != coordinate:
self._location = coordinate
self.locationChanged.emit(self._location)
def get_location(self):
return self._location
#pyqt Property
location = pyqtProperty(QGeoCoordinate,
fget=get_location,
fset=set_location,
notify=locationChanged)
main.py
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine
from PyQt5.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty
from PyQt5.QtPositioning import QGeoCoordinate
from ATC import ATC
if __name__ == "__main__":
import os
import sys
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
atc = ATC()
engine.rootContext().setContextProperty("atc", atc)
qml_path = os.path.join(os.path.dirname(__file__), "main.qml")
engine.load(QUrl.fromLocalFile(qml_path))
if not engine.rootObjects():
sys.exit(-1)
engine.quit.connect(app.quit)
sys.exit(app.exec_())
Instead of creating a model in QML you must create it in python to be able to handle it directly for it you must inherit from QAbstractListModel. For a smooth movement you should use QxxxAnimation as QPropertyAnimation.
In the following example, each time a marker is inserted, the on_markersInserted function will be called, which will move the marker towards the center of the window.
main.py
from functools import partial
from PyQt5 import QtCore, QtGui, QtQml, QtPositioning
import geocoder
class Marker(QtCore.QObject):
locationChanged = QtCore.pyqtSignal(QtPositioning.QGeoCoordinate)
def __init__(self, location=QtPositioning.QGeoCoordinate(), parent=None):
super().__init__(parent)
self._location = location
def set_location(self, coordinate):
if self._location != coordinate:
self._location = coordinate
self.locationChanged.emit(self._location)
def get_location(self):
return self._location
location = QtCore.pyqtProperty(QtPositioning.QGeoCoordinate,
fget=get_location,
fset=set_location,
notify=locationChanged)
def move(self, location, duration=1000):
animation = QtCore.QPropertyAnimation(
targetObject=self,
propertyName=b'location',
startValue=self.get_location(),
endValue=location,
duration=duration,
parent=self
)
animation.start(QtCore.QAbstractAnimation.DeleteWhenStopped)
def moveFromTo(self, start, end, duration=1000):
self.set_location(start)
self.move(end, duration)
class MarkerModel(QtCore.QAbstractListModel):
markersInserted = QtCore.pyqtSignal(list)
PositionRole = QtCore.Qt.UserRole + 1000
def __init__(self, parent=None):
super().__init__(parent)
self._markers = []
self.rowsInserted.connect(self.on_rowsInserted)
def rowCount(self, parent=QtCore.QModelIndex()):
return 0 if parent.isValid() else len(self._markers)
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid() and 0 <= index.row() < self.rowCount():
if role == MarkerModel.PositionRole:
return self._markers[index.row()].get_location()
return QtCore.QVariant()
def roleNames(self):
roles = {}
roles[MarkerModel.PositionRole] = b'position'
return roles
#QtCore.pyqtSlot(QtPositioning.QGeoCoordinate)
def appendMarker(self, coordinate):
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
marker = Marker(coordinate)
self._markers.append(marker)
self.endInsertRows()
marker.locationChanged.connect(self.update_model)
def update_model(self):
marker = self.sender()
try:
row = self._markers.index(marker)
ix = self.index(row)
self.dataChanged.emit(ix, ix, (MarkerModel.PositionRole,))
except ValueError as e:
pass
#QtCore.pyqtSlot(QtCore.QModelIndex, int, int)
def on_rowsInserted(self, parent, first, end):
markers = []
for row in range(first, end+1):
markers.append(self.get_marker(row))
self.markersInserted.emit(markers)
def get_marker(self, row):
if 0 <= row < self.rowCount():
return self._markers[row]
class ManagerMarkers(QtCore.QObject):
locationChanged = QtCore.pyqtSignal(QtPositioning.QGeoCoordinate)
def __init__(self, parent=None):
super().__init__(parent)
self._center= Marker(parent=self)
self._model = MarkerModel(self)
g = geocoder.ip('me')
self.moveCenter(QtPositioning.QGeoCoordinate(*g.latlng))
def model(self):
return self._model
def center(self):
return self._center
def moveCenter(self, position):
self._center.set_location(position)
center = QtCore.pyqtProperty(QtCore.QObject, fget=center, constant=True)
model = QtCore.pyqtProperty(QtCore.QObject, fget=model, constant=True)
# testing
# When a marker is added
# it will begin to move toward
# the center of the window
def on_markersInserted(manager, markers):
end = manager.center.get_location()
for marker in markers:
marker.move(end, 5*1000)
if __name__ == "__main__":
import os
import sys
app = QtGui.QGuiApplication(sys.argv)
manager = ManagerMarkers()
engine = QtQml.QQmlApplicationEngine()
engine.rootContext().setContextProperty("manager", manager)
qml_path = os.path.join(os.path.dirname(__file__), "main.qml")
engine.load(QtCore.QUrl.fromLocalFile(qml_path))
if not engine.rootObjects():
sys.exit(-1)
manager.model.markersInserted.connect(partial(on_markersInserted, manager))
engine.quit.connect(app.quit)
sys.exit(app.exec_())
main.qml
import QtQuick 2.7
import QtQuick.Controls 2.2
import QtPositioning 5.9
import QtLocation 5.3
ApplicationWindow {
id: root
width: 640
height: 480
visible: true
Plugin {
id: mapPlugin
name: "osm" // "mapboxgl" "osm" "esri"
}
Map {
id: map
anchors.fill: parent
plugin: mapPlugin
center: manager.center.location
zoomLevel: 14
MapCircle {
id: home
center: manager.center.location
radius: 40
color: 'white'
}
MapItemView {
model: manager.model
delegate: MapCircle {
radius: 50
color: 'red'
center: model.position
}
}
MouseArea {
id: mousearea
anchors.fill: map
acceptedButtons: Qt.LeftButton | Qt.RightButton
onDoubleClicked: if (mouse.button === Qt.LeftButton)
manager.model.appendMarker(map.toCoordinate(Qt.point(mouseX, mouseY)))
}
}
}
I have two qml components sitting in one module.
components
|- Edge.qml
|- Class.qml
|- qmdir
main
|- main.qml
main.py
with main.qml
import urmelgraph.components 1.0
import CustomGeometry 1.0
ApplicationWindow {
visible: true
width: 640
height: 240
title: qsTr("Test")
color: "#2C3E50"
}
and qmdir
module urmelgraph.components
Class 1.0 Class.qml
Edge 1.0 Edge.qml
I am loading both inside my python main.py file.
from PyQt5.QtGui import QGuiApplication, QColor, QSurfaceFormat
from PyQt5.QtQml import QQmlApplicationEngine, QQmlComponent, QQmlContext, qmlRegisterType, QQmlProperty
from PyQt5.QtQuick import QQuickItem, QQuickView, QSGGeometryNode, QSGGeometry, QSGNode, QSGFlatColorMaterial
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QUrl, QPointF, QSizeF
from pathlib import Path
class StraightLine(QQuickItem):
p1_Changed = pyqtSignal()
p2_Changed = pyqtSignal()
segment_count_Changed = pyqtSignal()
def __init__(self, parent: QQuickItem, p1: QPointF = QPointF(0,0), p2: QPointF = QPointF(1,1), segment_count: int = 2):
super().__init__(parent)
self._p1 = p1
self._p2 = p2
self._segment_count = segment_count
self.setFlag(QQuickItem.ItemHasContents, True)
#pyqtProperty("QPointF", notify = p1_Changed)
def p1(self):
return self._p1
#p1.setter
def p1(self, p1: QPointF):
if p1 == self._p1:
return
self._p1 = p1
self.p1_Changed.emit()
self.update()
#pyqtProperty("QPointF", notify = p2_Changed)
def p2(self):
return self._p2
#p2.setter
def p2(self, p2: QPointF):
if p2 == self._p2:
return
self._p2 = p2
self.p2_Changed.emit()
self.update()
#pyqtProperty(int, notify = segment_count_Changed)
def segment_count(self):
return self._segment_count
#segment_count.setter
def segment_count(self, count: int):
if count == self._segment_count:
return
self._segment_count = count
self.segment_count_Changed.emit()
self.update()
def updatePaintNode(self, oldNode: QSGGeometryNode, _):
if oldNode == None:
node = QSGGeometryNode()
geometry = QSGGeometry(QSGGeometry.defaultAttributes_Point2D(), self._segment_count)
geometry.setLineWidth(3)
geometry.setDrawingMode(QSGGeometry.DrawLineStrip)
node.setGeometry(geometry)
node.setFlag(QSGNode.OwnsGeometry)
material = QSGFlatColorMaterial()
material.setColor(QColor(45, 100, 120))
node.setMaterial(material)
node.setFlag(QSGNode.OwnsMaterial)
else:
node = oldNode
geometry = node.geometry()
geometry.allocate(self._segment_count)
itemSize = self.size()
vertices = geometry.vertexDataAsPoint2D()
x1 = self._p1.x()
y1 = self._p1.y()
vertices[0].set(x1, y1)
x2 = self._p2.x()
y2 = self._p2.y()
vertices[1].set(x2, y2)
print(vertices[1].x)
node.markDirty(QSGNode.DirtyGeometry)
return node
if __name__ == "__main__":
import sys
path = Path("..")
resolved_path = path.resolve()
# Create an instance of the application
app = QGuiApplication(sys.argv)
# Set antialising 4 samples
format = QSurfaceFormat()
format.setSamples(4)
QSurfaceFormat.setDefaultFormat(format)
# register custom types
qmlRegisterType(StraightLine, "CustomGeometry", 1, 0, "StraightLine")
# Create QML engine
engine = QQmlApplicationEngine()
# Load the qml file into the engine
engine.addImportPath(str(resolved_path))
engine.load("main/main.qml")
# load the components
component = QQmlComponent(engine)
component.loadUrl(QUrl("components/Class.qml"))
line_component = QQmlComponent(engine)
line_component.loadUrl(QUrl("components/Edge.qml"))
# check for component creation errors
for error in component.errors():
print(error.toString())
# check for component creation errors
for error in line_component.errors():
print(error.toString())
classes = []
for index, class_name in enumerate(["Person", "Home"]):
# create a new instance of the component
cclass = component.create()
# set the class name property of the component
cclass.setProperty("className", class_name)
cclass.setX((cclass.width() + 50) * index)
cclass.setParentItem(engine.rootObjects()[0].findChild(QQuickItem))
classes.append(cclass)
line = line_component.beginCreate(engine.rootContext())
line.setProperty("anchor1", classes[0])
line.setProperty("anchor2", classes[1])
line_component.completeCreate()
# check for object creation errors
for error in line_component.errors():
print(error.toString())
for error in component.errors():
print(error.toString())
engine.quit.connect(app.quit)
sys.exit(app.exec_())
But now I want to connect the first point of the Edge E to a Class component A and the second point of the Edge E to a Class component B.
For that I have created properties in the Edge.qml.
import QtQuick 2.11
import CustomGeometry 1.0
import urmelgraph.components 1.0
StraightLine {
property Class anchor1
property Class anchor2
anchors.fill: parent
Component.onCompleted: {
console.log(anchor1)
console.log(anchor2)
}
p2: Qt.point(anchor2.x + (anchor2.width/2), anchor2.y + (anchor2.height/2))
p1: Qt.point(anchor1.x + (anchor1.width/2), anchor1.y + (anchor1.height/2))
}
This is my Class.qml
import QtQuick 2.11
import QtQuick.Layouts 1.11
Rectangle {
width: 50
height: 20
color: "#2980B9"
border.color: "#ECF0F1"
property string className
Drag.active: dragArea.drag.active
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
// disable delay when moved
drag.threshold: 0
}
Text {
text: className
}
}
In my main.py I have a classes list with all generated Class components but trying to connect the first class with the second class via Edge (line), for example, doesn't work:
line = line_component.beginCreate(engine.rootContext())
line.setProperty("anchor1", classes[0])
line.setProperty("anchor2", classes[1])
line_component.completeCreate()
However, if I create two Rectangles with id rect1 and rect2 within the main.qml file. using the QQuickItem StraightLine this code is working:
StraightLine {
anchors.fill: parent
p1: Qt.point(rect2.x + (rect2.width/2), rect2.y + (rect2.height/2))
p2: Qt.point(rect1.x + (rect1.width/2), rect1.y + (rect1.height/2))
}
Rectangle {
id: rect1
width: 10
height: 10
color: "red"
radius: width*0.5
Drag.active: dragArea.drag.active
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
// disable delay when moved
drag.threshold: 0
}
}
Rectangle {
id: rect2
width: 10
height: 10
color: "blue"
radius: width*0.5
Drag.active: dragArea2.drag.active
MouseArea {
id: dragArea2
anchors.fill: parent
drag.target: parent
// disable delay when moved
drag.threshold: 0
}
}
How can I pass the reference from those class components to my edge components to properly setup a binding for x,y,width,height?
The solution was to establish the data type of the property anchor1 and anchor2 to var.
Edge.qml
import QtQuick 2.11
import CustomGeometry 1.0
StraightLine {
property var anchor1
property var anchor2
anchors.fill: parent
Component.onCompleted: {
console.log(anchor1)
console.log(anchor2)
}
p2: Qt.point(anchor2.x + (anchor2.width/2), anchor2.y + (anchor2.height/2))
p1: Qt.point(anchor1.x + (anchor1.width/2), anchor1.y + (anchor1.height/2))
}
On the other hand, I have not included the QtQuick.Controls 1.4 import to recognize the ApplicationWindow in the main.qml:
import QtQuick.Controls 1.4
import urmelgraph.components 1.0
import CustomGeometry 1.0
ApplicationWindow {
visible: true
width: 640
height: 240
title: qsTr("Test")
color: "#2C3E50"
}
In the following link you will find the complete code