cannot add MapCircles to QML Map on MouseClick - python

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)))
}
}
}

Related

PySide Qt dataChanged.emit() not working but LayoutChanged.emit works

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)

manage QML Repeater from python

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.

Get delegate objectName from qml to python

I have an ListView, and inside of this i have delegate Rectangle, and inside Rectangle, i have Image object. So i want to get Image objectName into python PyQt5 file, and set for each image different sources!
ListView {
model: 12 /*that mean that i have 12 images!*/
delegate: Rectangle {
Image {
objectName: "img"
source: "file:///C:/LocalDir/Project/img/11.png"
}
}
}
def set_property():
self.context = QQuickWidget()
self.context.setSource(QUrl().fromLocalFile("QML-code.qml"))
self.rootObj = context.rootObject()
img = self.rootObj.findChild("img")
if img:
img.setProperty("source", path)
# and so on...but i don't know how to get img delegate
You are going down the wrong path, delegates can be deleted and created without notifying the GUI, so accessing them individually is not the right thing to do. The strategy in the views is to pass the information through the models, in this case as you are going to get the information of the path of the image in python it is advisable to create a model based on QAbstractListModel:
main.py
from PyQt5 import QtCore, QtGui, QtWidgets, QtQuickWidgets
class ImageModel(QtCore.QAbstractListModel):
PathRole = QtCore.Qt.UserRole + 1
def __init__(self, parent=None):
super(ImageModel, self).__init__(parent)
self._paths = []
def addPath(self, path):
self.beginInsertRows(
QtCore.QModelIndex(), self.rowCount(), self.rowCount()
)
self._paths.append(path)
self.endInsertRows()
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self._paths)
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == ImageModel.PathRole and 0 <= index.row() < self.rowCount():
return self._paths[index.row()]
return QtCore.QVariant()
def roleNames(self):
return {ImageModel.PathRole: b"path"}
if __name__ == "__main__":
import os
import sys
app = QtWidgets.QApplication(sys.argv)
model = ImageModel()
w = QtQuickWidgets.QQuickWidget(
resizeMode=QtQuickWidgets.QQuickWidget.SizeViewToRootObject
)
w.rootContext().setContextProperty("pathmodel", model)
filename = os.path.join(
os.path.dirname(os.path.realpath(__file__)), "main.qml"
)
w.setSource(QtCore.QUrl.fromLocalFile(filename))
pictures_path = QtCore.QStandardPaths.writableLocation(
QtCore.QStandardPaths.PicturesLocation
)
formats = (
"*{}".format(fmt.data().decode())
for fmt in QtGui.QImageReader.supportedImageFormats()
)
for finfo in QtCore.QDir(pictures_path).entryInfoList(formats):
model.addPath(finfo.absoluteFilePath())
w.show()
sys.exit(app.exec_())
main.qml
import QtQuick 2.12
Rectangle{
width: 640
height: 480
ListView {
anchors.fill: parent
model: pathmodel
delegate: Rectangle {
width: 100
height: 100
Image {
anchors.fill: parent
source: Qt.resolvedUrl(model.path)
}
}
}
}
Or more easily use a QStandardItemModel:
from PyQt5 import QtCore, QtGui, QtWidgets, QtQuickWidgets
PathRole = QtCore.Qt.UserRole + 1
if __name__ == "__main__":
import os
import sys
app = QtWidgets.QApplication(sys.argv)
model = QtGui.QStandardItemModel()
model.setItemRoleNames({PathRole: b"path"})
w = QtQuickWidgets.QQuickWidget(
resizeMode=QtQuickWidgets.QQuickWidget.SizeViewToRootObject
)
w.rootContext().setContextProperty("pathmodel", model)
filename = os.path.join(
os.path.dirname(os.path.realpath(__file__)), "main.qml"
)
w.setSource(QtCore.QUrl.fromLocalFile(filename))
pictures_path = QtCore.QStandardPaths.writableLocation(
QtCore.QStandardPaths.PicturesLocation
)
formats = (
"*{}".format(fmt.data().decode())
for fmt in QtGui.QImageReader.supportedImageFormats()
)
for finfo in QtCore.QDir(pictures_path).entryInfoList(formats):
it = QtGui.QStandardItem()
it.setData(finfo.absoluteFilePath(), PathRole)
model.appendRow(it)
w.show()
sys.exit(app.exec_())

Refreshing QQuickWidget in QML file After Button Click

Created a MapWidget using QQuickWidget and a qml file to zoom into the given location coordinates. However, the map does not refresh whenever the coordinates changed. I am trying to connect a button that can be clicked to update the map but with no luck so far. Is there a way to obtain an id for the button and pass it on to the qml file to update or refresh the map whenever the coordinates are changed?
main.py
class MapWidget(QtQuickWidgets.QQuickWidget):
def __init__(self, parent=None):
super(MapWidget, self).__init__(parent,
resizeMode=QtQuickWidgets.QQuickWidget.SizeRootObjectToView)
model = MarkerModel(self)
self.rootContext().setContextProperty("markermodel", model)
self.rootContext().setContextProperty("lataddr", globals.latitude)
self.rootContext().setContextProperty("lonaddr", globals.longitude)
qml_path = os.path.join(os.path.dirname(__file__), "main.qml")
self.setSource(QtCore.QUrl.fromLocalFile(qml_path))
positions = [(globals.latitude, globals.longitude)]
urls = ["http://maps.gstatic.com/mapfiles/ridefinder-images/mm_20_red.png"]
for c, u in zip(positions, urls):
coord = QtPositioning.QGeoCoordinate(*c)
source = QtCore.QUrl(u)
model.appendMarker({"position": coord , "source": source})
class project_ui(QWidget):
def setup(self, window):
"omitted code"
self.btn = QPushButton('Search', self)
self.btn.setFixedWidth(200)
self.btn.clicked.connect("reload map in qml file")
main.qml
Rectangle {
id:rectangle
width: 640
height: 480
Plugin {
id: osmPlugin
name: "osm"
}
property variant locationTC: QtPositioning.coordinate(lataddr, lonaddr)
Map {
id: map
anchors.fill: parent
plugin: osmPlugin
center: locationTC
zoomLevel: 10
}
MapItemView{
model: markermodel
delegate: MapQuickItem {
coordinate: position_marker
anchorPoint.x: image.width
anchorPoint.y: image.height
sourceItem:
Image { id: image; source: source_marker }
}
}
}
Considering that you want to move only one marker you must create a QObject that has the position as q-properties, export the object and make a binding in QML.
main.py
import os
from PyQt5 import QtCore, QtGui, QtWidgets, QtQuickWidgets, QtPositioning
class MarkerObject(QtCore.QObject):
coordinateChanged = QtCore.pyqtSignal(QtPositioning.QGeoCoordinate)
sourceChanged = QtCore.pyqtSignal(QtCore.QUrl)
def __init__(self, parent=None):
super(MarkerObject, self).__init__(parent)
self._coordinate = QtPositioning.QGeoCoordinate()
self._source = QtCore.QUrl()
def getCoordinate(self):
return self._coordinate
def setCoordinate(self, coordinate):
if self._coordinate != coordinate:
self._coordinate = coordinate
self.coordinateChanged.emit(self._coordinate)
def getSource(self):
return self._source
def setSource(self, source):
if self._source != source:
self._source = source
self.sourceChanged.emit(self._source)
coordinate = QtCore.pyqtProperty(QtPositioning.QGeoCoordinate, fget=getCoordinate, fset=setCoordinate, notify=coordinateChanged)
source = QtCore.pyqtProperty(QtCore.QUrl, fget=getSource, fset=setSource, notify=sourceChanged)
class MapWidget(QtQuickWidgets.QQuickWidget):
def __init__(self, parent=None):
super(MapWidget, self).__init__(parent,
resizeMode=QtQuickWidgets.QQuickWidget.SizeRootObjectToView)
self._marker_object = MarkerObject(parent)
self._marker_object.setSource(QtCore.QUrl("http://maps.gstatic.com/mapfiles/ridefinder-images/mm_20_red.png"))
self.rootContext().setContextProperty("marker_object", self._marker_object)
qml_path = os.path.join(os.path.dirname(__file__), "main.qml")
self.setSource(QtCore.QUrl.fromLocalFile(qml_path))
#QtCore.pyqtSlot(QtPositioning.QGeoCoordinate)
def moveMarker(self, pos):
self._marker_object.setCoordinate(pos)
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._lat_spinbox = QtWidgets.QDoubleSpinBox(
minimum=-90,
maximum=90,
decimals=6,
value=59.91
)
self._lng_spinbox = QtWidgets.QDoubleSpinBox(
minimum=-180,
maximum=180,
decimals=6,
value=10.75
)
search_button = QtWidgets.QPushButton("Search", clicked=self.search)
self._map_widget = MapWidget()
flay = QtWidgets.QFormLayout(self)
flay.addRow("Latitude:", self._lat_spinbox)
flay.addRow("Longitude:", self._lng_spinbox)
flay.addRow(search_button)
flay.addRow(self._map_widget)
self.search()
#QtCore.pyqtSlot()
def search(self):
lat = self._lat_spinbox.value()
lng = self._lng_spinbox.value()
self._map_widget.moveMarker(QtPositioning.QGeoCoordinate(lat, lng))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
main.qml
import QtQuick 2.9
import QtLocation 5.12
import QtPositioning 5.12
Rectangle {
id:rectangle
width: 640
height: 480
Plugin {
id: osmPlugin
name: "osm"
}
Map {
id: map
anchors.fill: parent
plugin: osmPlugin
center: marker_object.coordinate
zoomLevel: 10
MapQuickItem {
coordinate: marker_object.coordinate
anchorPoint.x: image.width
anchorPoint.y: image.height
sourceItem: Image {
id: image
source: marker_object.source
}
}
}
}

How do I bind a dynamically created qmlcomponent object to the property of another dynamic created qmlcomponent object?

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

Categories