Get delegate objectName from qml to python - 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_())

Related

Is there a cleaner way for backend in PySide6 for QML?

Hello I am using Model class to supply items for lists and comboboxes. The problem is that I use the setContextProperty() function every time for each element. I'm looking for a solution where all elements(list and comboboxes) use the same ContextProperty. Furthermore with this way I guess JSON files can be loaded dynamically instead of loading all of them at the beginning.
main.py
class Model(QAbstractListModel, QObject):
""" it reads JSON file, that is given as argument,
and creates the model"""
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
model1 = Model("file1.json")
model2 = Model("file2.json")
model3 = Model("file3.json")
model4 = Model("file4.json")
model5 = Model("file5.json")
engine.rootContext().setContextProperty("model1", model1)
engine.rootContext().setContextProperty("model2", model2)
engine.rootContext().setContextProperty("model3", model3)
engine.rootContext().setContextProperty("model4", model4)
engine.rootContext().setContextProperty("model5", model5)
engine.rootContext().setContextProperty("applicationDirPath", os.path.dirname(__file__))
engine.load(os.path.join(os.path.dirname(__file__), "main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
You can create a QObject that exposes all the models as a property list and then use a Repeater to dynamically create the comboboxes.
The following is a demo:
import os
import sys
from pathlib import Path
from PySide6.QtCore import Property, QCoreApplication, QObject, Qt, QUrl, Signal
from PySide6.QtGui import QGuiApplication, QStandardItem, QStandardItemModel
from PySide6.QtQml import QQmlApplicationEngine
CURRENT_DIRECTORY = Path(__file__).resolve().parent
class Model(QStandardItemModel):
def __init__(self, values, parent=None):
super().__init__(parent)
for value in values:
item = QStandardItem(value)
self.appendRow(item)
class Manager(QObject):
models_changed = Signal(name="modelsChanged")
def __init__(self, parent=None):
super().__init__(parent)
self._models = []
#Property("QVariantList", notify=models_changed)
def models(self):
return self._models
def append_model(self, model):
self._models.append(model)
self.models_changed.emit()
def main():
app = QGuiApplication(sys.argv)
manager = Manager(app)
manager.append_model(Model(["item11", "item12", "item13"]))
manager.append_model(Model(["item21", "item22", "item23"]))
manager.append_model(Model(["item31", "item32", "item33"]))
manager.append_model(Model(["item41", "item42", "item43"]))
engine = QQmlApplicationEngine()
context = engine.rootContext()
context.setContextProperty("applicationDirPath", os.fspath(CURRENT_DIRECTORY))
context.setContextProperty("managerModel", manager)
filename = os.fspath(CURRENT_DIRECTORY / "main.qml")
url = QUrl.fromLocalFile(filename)
def handle_object_created(obj, obj_url):
if obj is None and url == obj_url:
QCoreApplication.exit(-1)
engine.objectCreated.connect(handle_object_created, Qt.QueuedConnection)
engine.load(url)
sys.exit(app.exec())
if __name__ == "__main__":
main()
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ApplicationWindow {
width: 640
height: 480
visible: true
ColumnLayout {
anchors.centerIn: parent
Repeater {
id: repeater
model: managerModel.models
ComboBox {
model: modelData
textRole: "display"
Layout.fillWidth: true
}
}
}
}
Side note: A QAbstractListModel is a QObject so the double inherence is useless so you should change it to: class Model(QAbstractListModel):

How to create a button on a field in QWizardPage

I am attempting to create a field in a QWizardPage that the user can either click on and a popup window opens up (this would be a window with a map on that allows the user to pick 2 coordinates) or allows the user to input the coordinates themselves. Think along the lines of a file picker line where the user can either open the popup file browser or manually type in the file path.
The original command is QLineEdit.
You have to create a custom widget that for example shows a QDoubleSpinBox for latitude and another for longitude, in addition to a button that allows to display a map (for example using QML). And then the widget is added to QWizardPage like any other widget.
import os
import sys
from pathlib import Path
from PySide2.QtCore import Property, Signal, Slot, Qt, QUrl
from PySide2.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
QHBoxLayout,
QLabel,
QToolButton,
QVBoxLayout,
QWidget,
QWizard,
QWizardPage,
)
from PyQt5.QtPositioning import QGeoCoordinate
from PyQt5.QtQuickWidgets import QQuickWidget
CURRENT_DIRECTORY = Path(__file__).resolve().parent
class MapDialog(QDialog):
def __init__(self, geo_widget):
super().__init__(geo_widget)
self.setWindowTitle("Map")
self.map_widget = QQuickWidget(resizeMode=QQuickWidget.SizeRootObjectToView)
self.map_widget.rootContext().setContextProperty("controller", geo_widget)
filename = os.fspath(CURRENT_DIRECTORY / "main.qml")
url = QUrl.fromLocalFile(filename)
self.map_widget.setSource(url)
button_box = QDialogButtonBox()
button_box.setOrientation(Qt.Horizontal)
button_box.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok)
lay = QVBoxLayout(self)
lay.addWidget(self.map_widget)
lay.addWidget(button_box)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
class GeoWidget(QWidget):
coordinate_changed = Signal(name="coordinateChanged")
def __init__(self, parent=None):
super().__init__(parent)
self._coordinate = QGeoCoordinate(0, 0)
self._lat_spinbox = QDoubleSpinBox(
minimum=-90.0, maximum=90.0, valueChanged=self.handle_value_changed
)
self._lng_spinbox = QDoubleSpinBox(
minimum=-180.0, maximum=180.0, valueChanged=self.handle_value_changed
)
self.btn = QToolButton(text="map", clicked=self.handle_clicked)
self.map_view = MapDialog(self)
lay = QHBoxLayout(self)
lay.addWidget(QLabel("Latitude:"))
lay.addWidget(self._lat_spinbox)
lay.addWidget(QLabel("Longitude:"))
lay.addWidget(self._lng_spinbox)
lay.addWidget(self.btn)
#Property(QGeoCoordinate, notify=coordinate_changed)
def coordinate(self):
return self._coordinate
#coordinate.setter
def coordinate(self, coordinate):
if self.coordinate == coordinate:
return
self._coordinate = coordinate
self.coordinate_changed.emit()
def handle_value_changed(self):
coordinate = QGeoCoordinate(
self._lat_spinbox.value(), self._lng_spinbox.value()
)
self.coordinate = coordinate
#Slot(QGeoCoordinate)
def update_from_map(self, coordinate):
self.coordinate = coordinate
self._lat_spinbox.setValue(self.coordinate.latitude())
self._lng_spinbox.setValue(self.coordinate.longitude())
def handle_clicked(self):
self.map_view.exec_()
class WizardPage(QWizardPage):
def __init__(self, parent=None):
super().__init__(parent)
self.geo_widget1 = GeoWidget()
self.geo_widget2 = GeoWidget()
self.registerField("coordinate1", self.geo_widget1, b"coordinate")
self.registerField("coordinate2", self.geo_widget2, b"coordinate")
lay = QVBoxLayout(self)
lay.addWidget(self.geo_widget1)
lay.addWidget(self.geo_widget2)
def main():
app = QApplication(sys.argv)
w = QWizard()
page = WizardPage()
w.addPage(page)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
import QtLocation 5.15
import QtQuick 2.15
Item {
width: 400
height: 400
Map {
id: map
anchors.fill: parent
zoomLevel: 14
MouseArea {
id: mouse
anchors.fill: parent
onClicked: controller.update_from_map(map.toCoordinate(Qt.point(mouse.x, mouse.y)))
}
plugin: Plugin {
name: "osm"
}
}
}

Live video from GigE Cameras

I have a problem with live video stream from 2 GigE cameras in QML. I tried it before with QLabels and QPixmap and it worked without any problem. The QML Labels don't have pixmap property to send images using signal slots.
Here is my Python code:
import sys
import os
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtGui import QImage, QPixmap
from PySide2.QtCore import Slot, QThread, Signal, Qt, QObject
import cv2
from pypylon import pylon
tlFactory = pylon.TlFactory.GetInstance()
devices = tlFactory.EnumerateDevices()
if len(devices) == 0:
raise pylon.RuntimeException("No camera present.")
cameras = pylon.InstantCameraArray(min(len(devices), 2))
for i, cam in enumerate(cameras):
cam.Attach(tlFactory.CreateDevice(devices[i]))
class CamThread(QThread):
cam1 = Signal(QImage)
cam2 = Signal(QImage)
def run(self):
cameras.StartGrabbing(pylon.GrabStrategy_LatestImageOnly)
try:
while cameras.IsGrabbing():
grabResult1 = cameras[0].RetrieveResult(
5000, pylon.TimeoutHandling_ThrowException
)
grabResult2 = cameras[1].RetrieveResult(
5000, pylon.TimeoutHandling_ThrowException
)
if grabResult1.GrabSucceeded() and grabResult2.GrabSucceeded():
img1 = grabResult1.GetArray()
img2 = grabResult2.GetArray()
rgb1 = cv2.cvtColor(img1, cv2.COLOR_YUV2RGB_Y422)
rgb2 = cv2.cvtColor(img2, cv2.COLOR_YUV2RGB_Y422)
h1, w1, ch1 = rgb1.shape
h2, w2, ch2 = rgb2.shape
bytesPerLine1 = ch1 * w1
bytesPerLine2 = ch2 * w1
convertToQtFormat1 = QImage(
img1.data, w1, h1, bytesPerLine1, QImage.Format_RGB888
)
convertToQtFormat2 = QImage(
img2.data, w2, h2, bytesPerLine2, QImage.Format_RGB888
)
p = convertToQtFormat1.scaled(800, 746, Qt.KeepAspectRatio)
q = convertToQtFormat2.scaled(800, 746, Qt.KeepAspectRatio)
self.cam1.emit(p)
self.cam2.emit(q)
except Exception as error:
print(error)
class MainWindow(QObject):
def __init__(self):
QObject.__init__(self)
self.CamThread = CamThread()
self.CamThread.cam1.connect(self.camera1)
self.CamThread.cam2.connect(self.camera2)
self.CamThread.start()
#Slot(QImage)
def camera1(self, image):
pass
#Slot(QImage)
def camera2(self, image):
pass
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
backend = MainWindow()
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty("backend", backend)
engine.load(os.path.join(os.path.dirname(__file__), "main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
So how to show live video stream using QML/PySide2?
I am using QT Design Studio.
Although the QQuickImageProvider option can be a good one but the drawback is that you have to generate different urls, instead a better option is to use VideoOutput, for example in your case the following implementation should work (not tested):
from functools import cached_property
import os
import random
import sys
import threading
import cv2
from PySide2.QtCore import Property, QObject, Qt, QSize, QTimer, Signal, Slot
from PySide2.QtGui import QColor, QGuiApplication, QImage
from PySide2.QtMultimedia import QAbstractVideoSurface, QVideoFrame, QVideoSurfaceFormat
from PySide2.QtQml import QQmlApplicationEngine
import shiboken2
from pypylon import pylon
class CameraProvider(QObject):
imageChanged = Signal(int, QImage)
def start(self, cameras):
threading.Thread(target=self._execute, args=(cameras,), daemon=True).start()
def _execute(self, cameras):
while cameras.IsGrabbing():
for i, camera in enumerate(cameras):
try:
grab_result = cameras[i].RetrieveResult(
5000, pylon.TimeoutHandling_ThrowException
)
if grab_result.GrabSucceeded():
img = grab_result.GetArray()
# FIXME
# convert img to qimage
qimage = QImage(800, 746, QImage.Format_RGB888)
qimage.fill(QColor(*random.sample(range(0, 255), 3)))
if shiboken2.isValid(self):
self.imageChanged.emit(i, qimage.copy())
except Exception as error:
print(error)
class CameraService(QObject):
surfaceChanged = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self._surface = None
self._format = QVideoSurfaceFormat()
self._format_is_valid = False
def get_surface(self):
return self._surface
def set_surface(self, surface):
if self._surface is surface:
return
if (
self._surface is not None
and self._surface is not surface
and self._surface.isActive()
):
self._surface.stop()
self._surface = surface
self.surfaceChanged.emit()
if self._surface is not None:
self._format = self._surface.nearestFormat(self._format)
self._surface.start(self._format)
videoSurface = Property(
QAbstractVideoSurface,
fget=get_surface,
fset=set_surface,
notify=surfaceChanged,
)
#Slot(QImage)
def update_frame(self, qimage):
if self.videoSurface is None or qimage.isNull():
return
if not self._format_is_valid:
self._set_format(qimage.width(), qimage.height(), QVideoFrame.Format_RGB32)
self._format_is_valid = True
qimage.convertTo(
QVideoFrame.imageFormatFromPixelFormat(QVideoFrame.Format_RGB32)
)
self._surface.present(QVideoFrame(qimage))
def _set_format(self, width, height, pixel_format):
size = QSize(width, height)
video_format = QVideoSurfaceFormat(size, pixel_format)
self._format = video_format
if self._surface is not None:
if self._surface.isActive():
self._surface.stop()
self._format = self._surface.nearestFormat(self._format)
self._surface.start(self._format)
class CameraManager(QObject):
def __init__(self, cameras, parent=None):
super().__init__(parent)
self._services = []
self.provider.imageChanged.connect(self.handle_image_changed)
self.provider.start(cameras)
for _ in cameras:
self._services.append(CameraService())
#cached_property
def provider(self):
return CameraProvider()
#Slot(int, QImage)
def handle_image_changed(self, index, qimage):
self._services[index].update_frame(qimage)
def get_services(self):
return self._services
services = Property("QVariantList", fget=get_services, constant=True)
def main():
app = QGuiApplication(sys.argv)
tlFactory = pylon.TlFactory.GetInstance()
devices = tlFactory.EnumerateDevices()
if len(devices) == 0:
raise pylon.RuntimeException("No camera present.")
cameras = pylon.InstantCameraArray(min(len(devices), 2))
for i, cam in enumerate(cameras):
cam.Attach(tlFactory.CreateDevice(devices[i]))
manager = CameraManager(cameras)
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty("manager", manager)
engine.load(os.path.join(os.path.dirname(__file__), "main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
if __name__ == "__main__":
main()
import QtQuick 2.14
import QtQuick.Window 2.14
import QtMultimedia 5.14
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
GridView {
width: 300; height: 200
model: manager !== null ? manager.services : []
delegate: VideoOutput {
width: 100
height: 100
fillMode: VideoOutput.PreserveAspectCrop
source: model.modelData
}
}
}
Qt provides different methods to pass images/video streams to QML:
1. Converting pixmap to base64 encoding
QByteArray byteArray;
QBuffer buffer(&byteArray);
buffer.open(QIODevice::WriteOnly);
pixmap.save(&buffer,"PNG");
QString data("data:image/png;base64,");
data.append(QString::fromLatin1(byteArray.toBase64().data()));
This base64 encoded image may be passed to Image::source
2. Use QQuickImageProvider
This allows connecting a custom image://... url to a QPixmap or QImage directly. Check the docs for more information.
3. Use QtMultimedia
Especially VideoOutput may be useful.

cannot add MapCircles to QML Map on MouseClick

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

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

Categories