Geokartenkacheln in QGraphicsView zusammenstellen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
kussji
User
Beiträge: 78
Registriert: Mittwoch 16. Mai 2018, 09:58

Hallo,
ich habe Geokartenkacheln als Graphikdateien (png). Diese möchte ich mit pyqt5 in QGraphicsView in die Scene laden. In meinem Beispiel mache ich das vorerst zum Testen mit 2 "Load-Button". Die einzelnen Bilder(Kacheln) sollen aber am Ende durch diverse Kriterien "automatisch" in die Scene geladen werden. In der Regel sollen dann immer 3x3 Kacheln geladen werden.

Mein "Versuchsaufbau" sieht wie unten aus. Das Problem: Die erste Kachel laden geht richtig. Sobald die zweite Kachel "dazu" geladen wird, ist die 2. Kachel mit dem richtigen Offset da, aber die erste Kachel wird nicht mehr angzeigt. (Da wo sie vorher war, ist jetzt leer (weiss). Warum das?

Wenn ich testweise mehrere Kacheln schon in "class Ui_MainWindow" in die Scene lade, dann geht es richtig (also nicht mit der Klasse "mapTiler()" geladen).

Und generell ist der Ansatz so vernünftig oder bin ich da auf dem Holzweg?

Danke schon mal für eure Inputs dazu.

Falls die GUI datei auch noch gebraucht wird einfach melden. Habe ich mit QT-Designer erstellt. (Tut ja aber nicht viel zur Sache)

Code: Alles auswählen

from PyQt5 import QtGui, QtWidgets, uic
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem

MAIN_FILEPATH = R'C:\Python37\projects''\\'
GUI_NAME = 'gui_v1.ui'

class mapTiler():
    def __init__(self):
        self._PIX_SIZE = 2500
        self._photo = QGraphicsPixmapItem()
        self.scene = QGraphicsScene()

    def add_tile(self, pixmap, tile_pos = 0):
        if tile_pos == 0:
            x = 0 * self._PIX_SIZE ; y = 0 * self._PIX_SIZE
        elif tile_pos == 1:
            x = 1 * self._PIX_SIZE ; y = 0 * self._PIX_SIZE
        elif tile_pos == 2:
            x = 0 * self._PIX_SIZE ; y = 1 * self._PIX_SIZE
        elif tile_pos == 3:
            x = 1 * self._PIX_SIZE ; y = 1 * self._PIX_SIZE
        else:
            print('tile pos error!')
            x = 0 ; y = 0
        self._photo.setPixmap(pixmap)
        self._photo.moveBy(x, y)
        self.scene.addItem(self._photo)

class Ui_MainWindow(QtWidgets.QMainWindow, QGraphicsView):
    def __init__(self, parent=None):
        super(Ui_MainWindow, self).__init__(parent)
        self.geoMap = mapTiler()
        self.ui = uic.loadUi(MAIN_FILEPATH + GUI_NAME, self)
        self.ui.graphicsView.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
        self.ui.graphicsView.setScene(self.geoMap.scene)
        self.btnLoad_1.clicked.connect(self.loadImage1)
        self.btnLoad_2.clicked.connect(self.loadImage2)

    def loadImage1(self):
        self.geoMap.add_tile(QtGui.QPixmap(MAIN_FILEPATH + 'IMG-1.png'), 1)

    def loadImage2(self):
        self.geoMap.add_tile(QtGui.QPixmap(MAIN_FILEPATH + 'IMG-2.png'), 2)

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = Ui_MainWindow()
    window.setGeometry(500, 300, 800, 600)
    window.show()
    sys.exit(app.exec_())
    
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du musst schon *per Kachel* ein PixmapItem anlegen. Eines für alle geht nicht, dann kickt der natürlich die alte Grafik raus.
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@kussji: Das die Definition von `MAIN_FILEPATH` so umständlich ist, könnte man als Anlass nehmen sowas nicht zu machen. Also das \ am Ende ist beispielsweise unnötig. Und falls da als Gegenargument käme „sonst geht das mit dem ``+`` nicht“ — ja das macht man ja auch nicht. Pfade sind nicht einfach nur Zeichenketten, da gelten Regeln und deshalb gibt es extra ein ganzes Modul, `pathlib`, dass sich damit beschäftigt.

Das `add_tile()` bei falschem `tile_pos`-Argument eine Meldung per `print()` ausgibt und dann einfach von Position 0 ausgeht ist überraschend. Ist das echt sinnvolles Verhalten?

Man schreibt nur eine Anweisung pro Zeile. Das ``;`` als Syntaxelement wird selten bis gar nicht verwendet. Dieser ganze Code könnte aber auch deutlich kürzer werden wenn man x und y einfach aus `tile_pos` *berechnet* statt da alle Kombinationen in den Quelltext zu schreiben.

Klassebnnamen schreibt man in PascalCase, also `MapTiler`. Konstanten definiert man nicht in Funktionen oder Methoden.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
kussji
User
Beiträge: 78
Registriert: Mittwoch 16. Mai 2018, 09:58

__deets__ hat geschrieben: Mittwoch 5. Januar 2022, 22:45 Du musst schon *per Kachel* ein PixmapItem anlegen. Eines für alle geht nicht, dann kickt der natürlich die alte Grafik raus.
Danke! Genau daran liegt es. Habe das jetzt mal "von Hand" versucht - geht. Lege wohl am Besten eine "pixmap"-Liste an und füttere die mit "append" ?!
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wieso? Die scene verwaltet die doch für dich. Und beinhaltet auch die Methoden, um zb welche die zu weit raus sind zu identifizieren & dann kannst du die abräumen. Und auch das Problem, rauszufinden, ob eine bestimmte Kachel schon da ist, löst man damit.
kussji
User
Beiträge: 78
Registriert: Mittwoch 16. Mai 2018, 09:58

Danke _blackjack_ für die Tipps. Einige waren mir bewusst (Baustellen und halt nur mal schnell ein Test). Nun im Wesentlichen habe ich mich erstmal mit Pathlib befasst. Habe ich mir einfacher vorgestellt.
Erste Unklarheit: Die erste Variante funktioniert - die Zweite nicht. Die Zweite meldet Fehler: "TypeError: QPixmap(): argument 1 has unexpected type 'WindowsPath".
Das ist mir ein Rätsel.
Zur Info: zur Zeit teste ich in Windows, soll aber nachher auch kompatibel in Linux (Raspi) sein...

Code: Alles auswählen

    def load_image_1(self):
        self.geo_map.add_tile(QtGui.QPixmap(R'gnss_navi''\\' + 'IMG-1.png'), 0)

Code: Alles auswählen

    def load_image_2(self):
        tile_path = Path('gnss_navi', 'IMG-2.png')
        self.geo_map.add_tile(QtGui.QPixmap(tile_path), 1)
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du musst die Objekte mit str() umwandeln, bevor du sie in Qt-Methoden stopfst. Das ist (noch) notwendig, weil die Qt APIs das noch nicht erkennen.
kussji
User
Beiträge: 78
Registriert: Mittwoch 16. Mai 2018, 09:58

__deets__ hat geschrieben: Donnerstag 6. Januar 2022, 21:49 Wieso? Die scene verwaltet die doch für dich. Und beinhaltet auch die Methoden, um zb welche die zu weit raus sind zu identifizieren & dann kannst du die abräumen. Und auch das Problem, rauszufinden, ob eine bestimmte Kachel schon da ist, löst man damit.
... hmm - weiss zwar etwa was Du sagen willst - und trotzdem muss ich pro Kachel eine Pixmapitem haben? Macht denn die Methode "add_tile" dies nicht? Anscheinend nicht sonst würde es ja funktionieren. Sehe den Lösungsansatz in dem Fall leider noch nicht. Kannst Du mir etwas auf die Sprünge helfen? Danke dafür!
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich weiß nicht, wie add_tile jetzt aussieht. Die alte war ja Murks. Mein Punkt ist: die scene verwaltet all ihre items. Wenn du 10 * 10 Kacheln hast, dann sind das 100 items, und die sind alle in der scene referenziert. Warum also willst du eine Liste davon erstellen? Was tut die? Warum glaubst du, du brauchst die?
kussji
User
Beiträge: 78
Registriert: Mittwoch 16. Mai 2018, 09:58

Hallo
Weiss nicht, ob wir aneinander vorbei reden? Der Murks in der MapTiler ist/war ja keine Konstanten deklarieren und die Positionsberechnung der Kachel bei add_tile, sowie die Fehlerbehandlung wenn die Position falsch ist - das habe ich begriffen. Die Frage deshalb ist das jetzt so wie unten immer noch "Murks" im Allgemeinen? Falls ja habe ich gar keinen Plan mehr für mein Projekt.
Dass die Scene die Items verwaltet ist mir klar. Mit addItem kann ich (jenste) zur Scene hinzufügen. Was aber anscheinend nicht geht, ist pro Kachel nur ein PixmapItem weil beim zweiten das erste rausgekickt wird. Davon will ich eine Liste erstellen, damit ich es zur Scene hinzufügen kann - da liegt irgendwie mein Verständnisproblem :shock:

Code: Alles auswählen

class MapTiler():
    def __init__(self):
        self._photo = QGraphicsPixmapItem()
        self.scene = QGraphicsScene()

    def add_tile(self, pixmap, tile_pos = 0):
	#position berechnen --> to do...
	self._photo.setPixmap(pixmap)
        self._photo.moveBy(x, y)
        self.scene.addItem(self._photo)
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Jedes Tile braucht ein QGraphicsPixmapItem. In add_tile musst du also ein neues erzeugen. Nicht eines für alle im Konstruktor.

Code: Alles auswählen

    def add_tile(self, pixmap, tile_pos = 0):
	#position berechnen --> to do...
        photo = QGraphicsPixmapItem(…)
	photo.setPixmap(pixmap)
        photo.moveBy(x, y)
        self.scene.addItem(photo)
Das war’s. Warum sollte da jetzt eine Liste ins Spiel kommen?
kussji
User
Beiträge: 78
Registriert: Mittwoch 16. Mai 2018, 09:58

__deets__ hat geschrieben: Freitag 7. Januar 2022, 08:46 Das war’s. Warum sollte da jetzt eine Liste ins Spiel kommen?
Tja - meine Schwäche - manchmal suche ich viel zu weit :?

Vielen Dank für die Hilfe
kussji
User
Beiträge: 78
Registriert: Mittwoch 16. Mai 2018, 09:58

Hallo
Man kann googeln oder man kann hier Fragen... ich mache mal beides ;-), ersteres habe ich schon mit nicht all zuviel Erfolg gemacht.
Kennt jemand gute - wenn möglich - deusche Doku, Tutorials, gute Beispiele zum PyQt QGraphicsView-Framework?

Und / Oder:
Speziell interessiert mich der Zusammenhang, wie hier schon geschreiben - mit Geokacheln und mit effektiven Koordinaten.
Meine effektiven Geo-Koordinaten liegen im Meter-Format bzw Milimeter vor (x,y). Die Kacheln haben 10Px/m. Ich will Items (Punkt, Linien etc.) Massstabgetreu in/auf die Geokacheln bringen kann.
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Auf deutsch nichts, und auch nicht in Python. Das ist bei Qt aber immer so. Da muss man die C++-Dokumentation hernehmen. Die ist meistens sehr einfach zu transferieren, und nur Besonderheiten sind dann in der PyQt Doku aufgeführt.

https://doc.qt.io/qt-5.15/graphicsview. ... ate-system
kussji
User
Beiträge: 78
Registriert: Mittwoch 16. Mai 2018, 09:58

Danke @ _deets_
Werde ich mal danach gucken....
kussji
User
Beiträge: 78
Registriert: Mittwoch 16. Mai 2018, 09:58

So habe mal etwas Zeit gefunden mein Vorhaben weiter zu machen.
Aktuell will ich bei einem Mausklick in die GraphicsView die aktuellen 'absoluten' Koordinaten der Items in der Scene anzeigen. Nun wahrscheinlich suche ich schon wieder zuweit :D .
Der erste Code habe ich irgendwo gefunden. Der macht mal aus meiner Sicht das was ich möchte. Was ich nicht verstehe, wer ruft die Methode 'def mousePressEvent' auf?
Wie kann ich das jetzt auf mein Code anwenden, da ich meine GUI per QTDesigner erstelle sprich ui-File. Die Idee wäre in der 'Ui_MainWindow' ein entsprechender Slot einzurichten aber da scheint für graphicsView nichts passendes da zu sein.

Code: Alles auswählen

import sys
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene

class MainWindow(QtWidgets.QMainWindow):
    central_widget = None
    layout_container = None

    def __init__(self):
        super(MainWindow, self).__init__()
        self.central_widget = QtWidgets.QWidget()
        self.layout_container = QtWidgets.QVBoxLayout()
        self.central_widget.setLayout(self.layout_container)
        self.setCentralWidget(self.central_widget)
        self.layout_container.addWidget(MyGraphicsView())

class MyGraphicsView(QGraphicsView):
    def __init__(self):
        super(MyGraphicsView, self).__init__()
        self.setScene(QGraphicsScene())
 
    def mousePressEvent(self, event):
        print('view', event.pos())
        print('scene', self.mapToScene(event.pos()))

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    mw = MainWindow()
    mw.show()
    sys.exit(app.exec_())

Code: Alles auswählen

from os import path
import sys
from pathlib import Path
from PyQt5 import QtGui, QtWidgets, QtCore, uic
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsPixmapItem, QGraphicsLineItem

MAIN_PATH = Path('g_nav')
IMAGE_PATH = Path('g_nav', 'image')
ORTHO_PATH = Path('g_nav', 'ortho', 'ortho_full')
GUI_NAME = 'g_nav_gui.ui'

class MapDraw():
    def __init__(self):
        self.scene = QGraphicsScene()

    def add_tile(self, pixmap, x, y):
        tile = QGraphicsPixmapItem()
        tile.setPixmap(pixmap)
        tile.moveBy(x, y)
        self.scene.addItem(tile)

class Ui_MainWindow(QtWidgets.QMainWindow, QGraphicsView):
    def __init__(self, parent=None):
        super().__init__(parent)

        ui_path = Path(MAIN_PATH, GUI_NAME)
        self.ui = uic.loadUi(ui_path, self)
  
        self.ui.graphicsView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.ui.graphicsView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.ui.graphicsView.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)

        self.geo_map = MapDraw()
        self.ui.graphicsView.setScene(self.geo_map.scene)

        self.pixmap = QtGui.QPixmap(str(Path(ORTHO_PATH, 'tile27.png')))
        self.geo_map.add_tile(self.pixmap, 0, 0)

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = Ui_MainWindow()
    MainWindow.show()
    sys.exit(app.exec_())
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das ist Mist. Stattdessen nimmt man die protected methods der scene here: https://doc.qt.io/qt-5/qgraphicsscene.h ... PressEvent - uns die kannst du ja problemlos ableiten & bekommst dann sogar schon spezifische Events: https://doc.qt.io/qt-5/qgraphicsscenemouseevent.html
kussji
User
Beiträge: 78
Registriert: Mittwoch 16. Mai 2018, 09:58

Hallo
Ein neue Frage aber in Zusammenhang mit dem gleichen Thema, weswegen ich keinen neuen Thread starte - okey?
Bin da ein rechtes Stück weiter mit meiner Sache. Ich lade mehrere Geokacheln in die Szene, weiter werden verschiedene Items (Linien, Pixmap, etc) in die Szene gelanden. Diese Items werden dann programmgesteuert verschoben und rotiert, soweit so gut. Nun soll aber auch die View rotiert werden, da die Karten inkl. Items durchaus Kopf stehen sollen. Das erreiche ich mit "GraphicsView.rotate(10)" für z.B. 10° rechts.
Mein Problem besteht darin, dass rotate immer relativ um angegebenen Wert vor oder zurück (+/-) rotiert. Ich gäbe aber gerne den Drehwert absolut rein, weil er mir in dieser Form vorliegt (also 0-359° (Kompass)).

Gibt es das bereits und ich habe es nicht gefunden oder sollte ich mein geschildertes Vorhaben überdenken. Irgendwelche coole Ideen? Ich gerade nicht :roll:

Danke
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Na klar, mit https://doc.qt.io/qt-5/qgraphicsview.html#setTransform und https://doc.qt.io/qt-5/qtransform.html#rotate kannst du beliebige affine Transformationen auf deinen View anwenden.
kussji
User
Beiträge: 78
Registriert: Mittwoch 16. Mai 2018, 09:58

Hallo,
vorweg: für mich ist absolute Rotation und nicht relative die Knacknuss.

Danke, habe mir mal die Transformationen angesehen. Auf folgendes bin ich gekommen:

Code: Alles auswählen

    def map_rotate(self, angle):
        transform = QtGui.QTransform()
        transform.rotate(-angle)
        self.ui.graphicsView.setTransform(self.transform)
 
in der Tat macht es zwar das was ich will, aber ist das "gut" so? Bastel?
Die Funktion "map_rotate" wird zyklisch (500ms) aufgerufen (GUI-update). Woran ich mich störe ist die erste Zeile "transform = QtGui.QTransform ()". Das kann ich doch im init der GUI machen ala: "self.transform=QtGuiTransform()" und dann in meiner Funktion die entsprechende erste Zeile weg lassen? Wenn ich das tue, dann habe ich keine absolute Rotation mehr, sondern die View wird zyklisch um den entsprechenden Winkel gedreht. Warum das so ist kann ich mir vorstellen. Muss ich am Ende meiner Funktion etwas "zurücksetzen"?
Antworten