Geometrische Objekte werden generiert, aber nicht angezeigt.

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

Jetzt kannst Du keine Elemente mehr entfernen, da `[thing for thing in self._things]` ohne Bedingung alle alten Elemente weiterverwendet. Wie schon geschrieben, der Teil muß weg, und update wenn nötig sich selbst zurückgeben.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@Sirius3: Du meinst anscheinend folgenden Tipp an __deets__.
Sirius3 hat geschrieben: Freitag 4. Januar 2019, 22:03 @__deets__: statt ein total_to_add und ein to_remove wäre doch eine einzelne Liste, die bei not remove things und immer to_add hinzufügt, einfacher umzusetzen. Dann könnte man statt ein remove und ein to_add nur eine Liste zurückgeben, die normalerweise [self] enthält, oder nichts, oder irgendwelche Objekte, die neu erstellt wurden.
Was meinst du mit "normalerweise [self] enthält, oder nichts, oder irgendwelche Objekte" genau?

@__deets__ & Sirius3: Ich habe den Code nun so kommentiert wie ich ihn mit Debugger und eingefügten Statusmeldungen verstanden habe. Ist das so richtig?

Code: Alles auswählen

import random
import time
import sys

from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene
from PyQt5.QtGui import QPainterPath


SPEED = 5000
MIN_DISTANCE = 30000
DISTANCE_VARIANCE = 20000


class Star:
    D = 1000

    def __init__(self, scene):
        path = QPainterPath()
        path.addRect(-10, -10, 20, 20)
        self._x, self._y = random.random() * 20000 - 10000, random.random() * 20000 - 10000
        self._z = random.random() * 20000 + MIN_DISTANCE
        self._speed = random.random() * SPEED
        self._path = scene.addPath(path)
        self._path.setPen(QtCore.Qt.red)
        self.update(0)  # force placement

    def update(self, elapsed):
        """
        Rückgabe:
        'False', damit Star-Objekt nicht entfernt wird.
        Leere Liste, weil hier nix generiert wird.
        """

        self._z -= elapsed * self._speed
        if self._z < 0:
            self._z = random.random() * DISTANCE_VARIANCE + MIN_DISTANCE
        self._path.setX(self._x * self.D / self._z) # ?
        self._path.setY(self._y * self.D / self._z) # ?
        self._path.setScale(1 - self._z / (MIN_DISTANCE + DISTANCE_VARIANCE))
        self._path.setRotation(self._path.rotation() + elapsed * 360)
        return False, []


class StarSpawner:
    STARS_PER_SECOND = 10
    TOTAL_STARS = 100
    def __init__(self, scene):
        self._star_spawn_period = 1.0 / self.STARS_PER_SECOND
        self._accu = 0.0
        self._count = self.TOTAL_STARS
        self._scene = scene


    def update(self, elapsed):
        """
        Bis self._accu kleiner/gleich der self._star_spawn_period ist
        werden neue Star-Objekte generiert.
        Sobald 100 Sterne generiert wurden, wird 'False' zurückgegeben, damit der
        StarSpawner aus self._things entfernt wird.
        """

        self._accu += elapsed
        spawned_stars = []
        while self._accu > self._star_spawn_period:
            spawned_stars.append(Star(self._scene))
            self._accu -= self._star_spawn_period

        self._count -= len(spawned_stars)
        return self._count <= 0, spawned_stars #


class GraphicsWindow(QGraphicsView):

    def __init__(self, parent=None):

        super(GraphicsWindow, self).__init__(parent)
        self._last = time.monotonic()
        scene = QGraphicsScene(self)
        self.setScene(scene)
        self._things = [StarSpawner(scene)]

        scene.setSceneRect(-400, -400, 800, 800)

        self._timer = QtCore.QTimer(self)
        self._timer.setSingleShot(False)
        self._timer.setInterval(1000 / 60)
        self._timer.timeout.connect(self._update_all)
        self._timer.start()


    def _update_all(self):
        """
        remove: True => Objekt entfernen (trifft nur bei StarSpawner zu); False => Objekt behalten
        to_add: StarSpawner => Liste mit erzeugten Star-Objekten; Star => leere Liste
        to_remove: nur der StarSpawner kommt darin einmal vor

        'self._things' werden die Elemente aus 'total_to_add' und alle eigenen Elemente,
        die nicht in 'to_remove' vorkommen (list comprehension), neu zugewiesen.
        In 'to_remove' vorkommende Elemente (nur StarSpawner) werden so entfernt.
        """
        now = time.monotonic()
        elapsed = now - self._last
        self._last = now

        to_remove = []
        total_to_add = []

        for thing in self._things:

            length = len(self._things) # Statusmeldung
            print(f"self._things (Länge: {length}): {self._things}") # Statusmeldung

            remove, to_add = thing.update(elapsed)

            if to_add: # Statusmeldung
                print(f"to_add        (Zeit: {time.monotonic()}): {to_add}")

            if remove:
                to_remove.append(thing)

                print(f"to_remove     (Zeit: {time.monotonic()}): {to_remove}") # Statusmeldung

            total_to_add.extend(to_add)

            if total_to_add: # Statusmeldung
                print(f"total_to_add: (Zeit: {time.monotonic()}): {total_to_add}")

        self._things = total_to_add + \
            [thing for thing in self._things if thing not in to_remove]


def main():
    app = QApplication(sys.argv)
    graphics_window = GraphicsWindow()
    graphics_window.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
Gruß
Atalanttore
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Die Kommentare kommen halbwegs hin.

Was Sirius3 meinte: Statt (True, []) etc zurück zu geben, einfach immer eine Liste zurück geben. Diese Liste ist beim Star zb immer [self]. Wie sie beim StarSpawner aussieht, kannst du ja mal überlegen.

In update_all wird dann immer die neue Liste aus den Rückgaben der alten berechnet. Ohne abfragen etc.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__deets__: Halbwegs habe ich es jetzt auch verstanden.

Der StarSpawner gibt schon jetzt eine Liste (`spawned_stars`) mit den neu von ihm erstellten Star-Objekten zurück. Neben dem booleschen Wert, ob der StarSpawner noch gebraucht wird, bräuchte man das eigentlich auch weiterhin oder man verzichtet auf den booleschen Wert und fügt den StarSpawner als `self` auch zur Liste der von ihm erstellten Star-Objekte hinzu. Wenn der StarSpawner merkt, dass er seine Arbeit erledigt hat (100 Sterne erzeugt), könnte er sich nicht mehr selbst zur Liste hinzufügen. Man müsste aber irgendwie sicherstellen, dass `self._things` am Ende nicht mehrere StarSpawner-Objekte enthält, sondern keine.

Weitere Fragen:
  1. Wieso heißt die Variable, wo die vergangene Zeit (`elapsed`) aufaddiert wird `self._accu`?
  2. Braucht man für weitere Objekte, die man zur Liste `self._things` hinzufügen möchte, damit sie auf dem Bildschirm erscheinen, auch jeweils einen Spawner?
Gruß
Atalanttore
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Man muss nicht sicherstellen, dass mehrere entstehen, weil die Liste _things immer NEU gebaut wird aus allen Rueckgaben der update-Methoden-Aufrufe. Deshalb muss ein Star eben [self] zurueckgeben.

Code: Alles auswählen

things_for_this_frame = self._things
self._things = [] # neu!
for thing in things_for_this_frame:
       self._things.extend(thing.update())
Zu deinen Fragen:

1) weil mir kein besserer Name eingefallen ist. Akkumulator (accu) ist ein Register-Name in alten Prozessoren, und manchmal benutze ich den Namen fuer Dinge, die etwas "aufsammeln", also akkumulieren. Da kommt der Name urspruenglich denke ich her.
2) Das ist eine Moeglichkeit. Das kommt halt drauf an, was da jetzt so an Drehbuch geplant ist. Man kann sich da beliebig viel denken, aber dann muss man ggf. auch noch andere Massnahmen ergreifen. Dadurch aendert sich dann halt das Vorgehen. Dieses Muster hier ist fuer den gegebenen Zweck gut, weil es eine Ablaufsteuerung mit klar definierten Verantwortlichkeiten herstellt, aber mehr Logik mag dann andere Dinge notwendig machen.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__deets__:

Zu 2. Kann man auch einen generischen Spawner bauen, der verschiedene Objekte erstellt?


Danke für den Codeabschnitt.
Ich habe den Code nun eingebaut, `elapsed` als Parameter für die `update()`-Methoden ergänzt (weil sich Python über ein fehlendes Argument beschwerte) und die Rückgabewerte so angepasst wie ich es verstanden habe. Die Liste `self._things` bleibt jetzt allerdings leer und es kommt zu folgendem Fehler:

Code: Alles auswählen

Traceback (most recent call last):
  File "/home/ata/source/stars-qt/main.py", line 91, in _update_all
    self._things.extend(thing.update(elapsed))
TypeError: 'NoneType' object is not iterable
Stimmt mit den angepassten Rückgaben etwas nicht?

Code:

Code: Alles auswählen

import random
import time
import sys

from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene
from PyQt5.QtGui import QPainterPath


SPEED = 5000
MIN_DISTANCE = 30000
DISTANCE_VARIANCE = 20000


class Star:
    D = 1000

    def __init__(self, scene):
        path = QPainterPath()
        path.addRect(-10, -10, 20, 20)
        self._x, self._y = random.random() * 20000 - 10000, random.random() * 20000 - 10000
        self._z = random.random() * 20000 + MIN_DISTANCE
        self._speed = random.random() * SPEED
        self._path = scene.addPath(path)
        self._path.setPen(QtCore.Qt.red)
        self.update(0)  # force placement

    def update(self, elapsed):

        self._z -= elapsed * self._speed
        if self._z < 0:
            self._z = random.random() * DISTANCE_VARIANCE + MIN_DISTANCE
        self._path.setX(self._x * self.D / self._z)
        self._path.setY(self._y * self.D / self._z)
        self._path.setScale(1 - self._z / (MIN_DISTANCE + DISTANCE_VARIANCE))
        self._path.setRotation(self._path.rotation() + elapsed * 360)
        #return False, []
        return self

class StarSpawner:
    STARS_PER_SECOND = 10
    TOTAL_STARS = 100
    def __init__(self, scene):
        self._star_spawn_period = 1.0 / self.STARS_PER_SECOND
        self._accu = 0.0
        self._count = self.TOTAL_STARS
        self._scene = scene


    def update(self, elapsed):

        self._accu += elapsed
        spawned_stars = []
        while self._accu > self._star_spawn_period:
            spawned_stars.append(Star(self._scene))
            self._accu -= self._star_spawn_period

        self._count -= len(spawned_stars)
        #return self._count <= 0, spawned_stars
        return spawned_stars.append(self)


class GraphicsWindow(QGraphicsView):

    def __init__(self, parent=None):

        super(GraphicsWindow, self).__init__(parent)
        self._last = time.monotonic()
        scene = QGraphicsScene(self)
        self.setScene(scene)
        self._things = [StarSpawner(scene)]

        scene.setSceneRect(-400, -400, 800, 800)

        self._timer = QtCore.QTimer(self)
        self._timer.setSingleShot(False)
        self._timer.setInterval(1000 / 60)
        self._timer.timeout.connect(self._update_all)
        self._timer.start()


    def _update_all(self):

        now = time.monotonic()
        elapsed = now - self._last
        self._last = now

        things_for_this_frame = self._things
        self._things = [] # neu!
        for thing in things_for_this_frame:
               self._things.extend(thing.update(elapsed))


def main():
    app = QApplication(sys.argv)
    graphics_window = GraphicsWindow()
    graphics_window.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
Gruß
Atalanttore
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Schau mal genau hin. Was habe ich geschrieben, was gibt dein Stern zurück? und was gibt “append” in deinem spawner zurück.

Und natürlich kann man einen anderen spawner bauen. Jedes Objekt, das diese Schnittstelle implementiert, geht.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

`Star` gibt sich selbst, also ein `Star`-Objekt zurück. Im `StarSpawner` wird an die Liste `spawned_stars` mit den von ihm erzeugten `Star`-Objekten noch das `StarSpawner`-Objekt angehängt und dann zurückgegeben.

Gruß
Atalanttore
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und wenn ein Star sich selbst zurueck gibt - wie verhaelt sich das zu der Erwartung an den Rueckgabewert an der aufrufenden Stelle?

Und zu der zweiten Antwort: das ist das, was du *denkst* was passiert. Nochmal die Frage: was ist der Rueckgabewert von liste.append()? Bau dir ein Skript, pack ne Liste rein, und mach print(liste.append(10)). Dann sollte der Fehler hoffentlich klar werden.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__deets__: `return self` im `Star`-Objekt liefert sich selbst zurück und `spawned_stars.append(Star(self._scene))` im `StarSpawner`-Objekt erwartet ein `Star`-Objekt.


`append()` scheint nicht innerhalb einer `print`-Anweisung zu funktionieren. Warum auch immer ...

Skript:

Code: Alles auswählen

liste = list(range(0,10))

liste2 = liste.copy()
liste2.append(10)

print("Liste 1:", liste.append(10))
print("Liste 2:", liste2)
Ausgabe:

Code: Alles auswählen

Liste 1: None
Liste 2: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Gruß
Atalanttore
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Seufz. Gut, mit den Hinweisen zum selbst-verstehen hoere ich jetzt mal auf.

Die update-Methode eines things muss IMMER eine Liste zurueckgeben. Wenn du also ` return self` in Star.update machst, dann ist das *trommelwirbel* keine Liste. Du musst [self] zurueck geben.

Und wenn du `return spawned_stars.append(self)` machst im Spawner, dann gibt das nicht magisch die Liste der erzeugten Sterne erweitert um das self des Spawners zurueck, sondern was auch immer list.append zurueckgibt. Und list.append gib *trommelwirbel* None zurueck.

Du musst also

Code: Alles auswählen

spawned_stars.append(self)
return spawned_stars
daraus machen, oder wegen mir

Code: Alles auswählen

return spawned_stars + [self]
Wobei durch das unbedingte zurueckgeben natuerlich der Spawner niemals aufhoert, Sterne zu erzeugen. Und irgendwann deine Framerate einbricht. Aber das ist dann das naechste Problem.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nachtrag: natuerlich funktioniert append innerhalb einer print-Anweisung. Es gibt aber eben None zurueck, und das wird dann eben ausgedruckt. Du hast halt *geglaubt*, dass es die Liste erweitert um das angehangene Element zurueckgibt. Genau auf diesen Denkfehler wollte ich dich hinweisen.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__deets__: Mit deinen Änderungen funktioniert es dann auch.

Ich habe die `update()`-Methode im `StarSpawner` angepasst, damit nicht mehr als 100 Sterne erzeugt werden.

Code: Alles auswählen

    def update(self, elapsed):

        self._accu += elapsed
        spawned_stars = []
        while self._accu > self._star_spawn_period:
            spawned_stars.append(Star(self._scene))
            self._accu -= self._star_spawn_period

        self._count -= len(spawned_stars)

        if self._count > 0:
            spawned_stars.append(self)

        #return self._count <= 0, spawned_stars
        return spawned_stars
Gruß
Atalanttore
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

Für eine Erweiterung des Programms bin ich auf der Suche nach einer einfachen Möglichkeiten zum Erkennen, wenn ein Objekt in der `scene` für den Nutzer nicht mehr sichtbar ist. Laut Doku bietet `QGraphicsScene` eine solche Möglichkeit an:
QGraphicsScene also provides functionality that lets you efficiently determine both the location of items, and for determining what items are visible within an arbitrary area on the scene.
Eine passende Methode und/oder eine genau Erklärung gibt es dort aber leider nicht.

Gruß
Atalanttore
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich sehe da diverse. Zb https://doc.qt.io/qt-5/qgraphicsscene.h ... ectionArea - die ausgewählten items sind diejenigen die in dem Pfad liegen.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

Wenn `path` ein `QPainterPath`-Objekt und `scene` ein `QGraphicsScene`-Objekt ist, schreibt man das dann so:

Code: Alles auswählen

scene.setSelectionArea(path)
selected = scene.selectedItems()
Es wäre toll, wenn `selected` dann eine Liste mit den noch sichtbaren Objekten enthält. Ist das der Fall?

Gruß
Atalanttore
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich wuerde eher mit der zweiten Option operieren, die Selektion ist sicher moeglich, aber mir persoenlich zu viel Seiteneffekt - die Selektion hat ja ggf. auch noch andere Einsatzzwecke.

Und hast du das mal probiert? Du hast doch inzwischen ausreichend viel Code, um damit mal rumzuspielen.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

Also eher so:

Code: Alles auswählen

scene.setSelectionArea(path)
selected = scene.items()
Gruß
Atalanttore
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Hast du dir die items-calls mal angeschaut? Das sind jede Menge. Mit Argumenten für eine Zone (als rect oder Pfad und so)

Also präzise wonach du gefragt hast.
Antworten