Tabellenzugriff vom Workerthread

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
Bulli
User
Beiträge: 10
Registriert: Mittwoch 22. Februar 2017, 12:45

Hallo,
die nicht so schöne(falsche) Kommunikation vom Workerthread mit dem Hauptfenster soll nun verändert werden. Wie bekomme ich die Variable "test"(Funktion dictUebergabe vom Workerthread) in die "run" Funktion vom Workerthread?

hier noch mein Code:

Code: Alles auswählen

import os
import sys
import time

from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow, QTableWidgetItem
from PyQt5.QtCore import QThread, pyqtSignal

HAUPTFENSTER_UI = os.path.join(os.path.dirname(__file__), "Hauptfenster.ui")

# Klasse für den Loop
class WorkerThread(QThread):
   def __init__(self, gui):
      super().__init__()
      self.gui = gui
      self.running = False

   def dictUebergabe(self, dict):
      test = dict
      print(test)

   def run(self):
      while not self.gui.GUI_Shutdown:
         if self.running:
            while self.running:
               rowCount = self.gui.TABLE_Commands.rowCount()
               for i in range(rowCount):
                  if not self.running:
                     break

                  self.gui.TABLE_Commands.selectRow(i)
                  self.gui.diCt.connect(self.dictUebergabe)
                  row = self.gui.getRow(i)
                  task = row["task"]
                  value = row["value"]
                  note = row["note"]

                  if task == "Aufgabe 1":
                     print("Aufgabe 1 ist:", value, note)
                     time.sleep(1)

                  if task == "Aufgabe 2":
                     print("Aufgabe 2 ist:", value, note)
                     time.sleep(1)

                  if task == "Aufgabe 3":
                     print("Aufgabe 3 ist:", value, note)
                     time.sleep(1)

            self.gui.TABLE_Commands.clearSelection()
            self.gui.BTN_ProgramStop.setEnabled(False)
            self.gui.BTN_ProgramStart.setEnabled(True)

# Klasse fuers Hauptfenster
class Hauptfenster(QMainWindow):
    diCt             = pyqtSignal(dict)

    def __init__(self):
        super().__init__()
        ui = uic.loadUi(HAUPTFENSTER_UI, self)
        self.GUI_Shutdown = False
        self.Worker = WorkerThread(self)
        self.Worker.start()

        self.TABLE_Commands = ui.CommandTable
        self.BTN_ProgramStart = ui.ProgramStart
        self.BTN_ProgramStop = ui.ProgramStop
        self.BTN_Line_Insert = ui.Line_Insert
        self.BTN_ResetInput = ui.ResetInput
        self.BTN_Line_DeleteAll = ui.Table_Reset
        self.LINE_ValueInput = ui.Line_ValueInput
        self.LINE_NoteInput = ui.Line_NoteInput
        self.COMBOBOX_Commands = ui.CommandBox

        self.COMBOBOX_Commands.currentTextChanged.connect(self.controlInputLines)
        self.COMBOBOX_Commands.currentTextChanged.connect(self.__removeEmptyItem)
        self.LINE_ValueInput.setPlaceholderText("Wert")
        self.LINE_NoteInput.setPlaceholderText("Notiz")
        self.BTN_Line_Insert.clicked.connect(self.BTN_Line_Insert_clicked)
        self.BTN_ResetInput.clicked.connect(self.BTN_ResetInput_clicked)
        self.BTN_ProgramStart.clicked.connect(self.BTN_ProgramStart_clicked)
        self.BTN_ProgramStop.clicked.connect(self.BTN_ProgramStop_clicked)
        self.BTN_ResetInput.clicked.connect(self.BTN_ResetInput_clicked)
        self.BTN_Line_DeleteAll.clicked.connect(self.BTN_Line_DeleteAll_clicked)
        self.BTN_Line_DeleteAll.clicked.connect(self.controlInputLines)
        self.LINE_ValueInput.textChanged.connect(self.controlInputLines)
        self.LINE_NoteInput.textChanged.connect(self.controlInputLines)

    def controlInputLines(self):
        taskNotEmpty = False
        valueNotEmpty = False
        noteNotEmpty = False
        tableNotEmpty = False

        if not self.COMBOBOX_Commands.currentText().strip() == "":
            taskNotEmpty = True

        if not self.LINE_ValueInput.text().strip() == "":
            valueNotEmpty = True

        if not self.LINE_NoteInput.text().strip() == "":
            noteNotEmpty = True

        if not self.TABLE_Commands.rowCount() == 0:
            tableNotEmpty = True

        self.BTN_ResetInput.setEnabled(True if taskNotEmpty or valueNotEmpty or noteNotEmpty else False)
        self.BTN_Line_Insert.setEnabled(True if taskNotEmpty and valueNotEmpty else False)
        self.BTN_Line_DeleteAll.setEnabled(True if tableNotEmpty else False)
        self.BTN_ProgramStart.setEnabled(True if tableNotEmpty else False)

    def BTN_ProgramStart_clicked(self) -> None:
        if not self.Worker.running:
            self.BTN_ProgramStop.setEnabled(True)
            self.BTN_ProgramStart.setEnabled(False)
            self.Worker.running = True

    def BTN_ProgramStop_clicked(self):
        self.Worker.running = False
        self.BTN_ProgramStop.setEnabled(False)
        self.BTN_ProgramStart.setEnabled(True)

    def BTN_Line_Insert_clicked(self) -> None:
        self.__insertRow(task=self.COMBOBOX_Commands.currentText(),
                            value=self.LINE_ValueInput.text(),
                            note=self.LINE_NoteInput.text())
        self.__resizeTable()
        self.TABLE_Commands.clearSelection()
        self.BTN_ResetInput_clicked()

    def BTN_ResetInput_clicked(self) -> None:
        self.LINE_ValueInput.clear()
        self.LINE_NoteInput.clear()
        self.COMBOBOX_Commands.insertItem(0, "")
        self.COMBOBOX_Commands.setCurrentIndex(self.COMBOBOX_Commands.findText(""))

    def BTN_Line_Delete_clicked(self) -> None:
        indexes = self.TABLE_Commands.selectionModel().selectedRows()
        if not len(indexes) == 0:
            removedRows = 0
            for index in indexes:
                row = index.row() - removedRows
                self.TABLE_Commands.removeRow(row)
                removedRows += 1
            self.TABLE_Commands.clearSelection()

    def BTN_Line_DeleteAll_clicked(self) -> None:
        self.TABLE_Commands.selectAll()
        self.BTN_Line_Delete_clicked()

    def getRow(self, row: int) -> dict:
        task = self.TABLE_Commands.item(row, 0).text()
        value = self.TABLE_Commands.item(row, 1).text()
        note = self.TABLE_Commands.item(row, 2).text()
        dict = {"task" : task, "value" : value, "note" : note}
        self.diCt.emit(dict)
        return(dict)

    def __changeRow(self, row: int, task: str, value: str, note: str) -> None:
        self.TABLE_Commands.setItem(row, 0, QTableWidgetItem(task))
        self.TABLE_Commands.setItem(row, 1, QTableWidgetItem(value))
        self.TABLE_Commands.setItem(row, 2, QTableWidgetItem(note))

    def __insertRow(self, task: str, value: str, note: str) -> None:
        currentRowCount = self.TABLE_Commands.rowCount()
        self.TABLE_Commands.insertRow(currentRowCount)
        self.__changeRow(row=currentRowCount, task=task, value=value, note=note)

    def __resizeTable(self):
        self.TABLE_Commands.resizeColumnsToContents()
        self.TABLE_Commands.resizeRowsToContents()

    def __moveRow(self, moveUp: bool) -> None:
        indexes = self.TABLE_Commands.selectionModel().selectedRows()
        if len(indexes) == 1:
            row = indexes[0].row()
            if (not row == 0 and moveUp) or (not row == self.TABLE_Commands.rowCount()-1 and not moveUp):
                rowMoveUp   = self.getRow(row if moveUp else row + 1)
                rowMoveDown = self.getRow(row - 1 if moveUp else row)
                self.__changeRow(row=row - 1 if moveUp else row, task=rowMoveUp["task"],
                                    value=rowMoveUp["value"], note=rowMoveUp["note"])
                self.__changeRow(row=row if moveUp else row + 1, task=rowMoveDown["task"],
                                    value=rowMoveDown["value"], note=rowMoveDown["note"])
                self.TABLE_Commands.selectRow(row-1 if moveUp else row+1)

    def __removeEmptyItem(self):
        if not self.COMBOBOX_Commands.currentText() == "":
            self.COMBOBOX_Commands.removeItem(self.COMBOBOX_Commands.findText(""))

    # Hier wird aufgeraeumt
    def closeEvent(self, event) -> None:
       self.Worker.running = False
       event.accept()
       self.GUI_Shutdown = True


# =====================================================================================================================
#  Hier gehts LOS, das Hauptprogramm
# =====================================================================================================================


if __name__ == "__main__":
    app = QApplication(sys.argv)
    gui = Hauptfenster()
    gui.show()
    sys.exit(app.exec_())
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Per Queued Signal Slot Connection.
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Hier ein Beispiel, wie das geht: viewtopic.php?f=1&t=53299#p395440
Bulli
User
Beiträge: 10
Registriert: Mittwoch 22. Februar 2017, 12:45

Vielen Dank für die Info werd mir das mal anschauen.
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Die Info ist ehrlich gesagt nicht neu, das alles hast du auch schon beim ersten mal erzaehlt bekommen. Der Code hier ist auch nicht dem ueblichen Muster entsprechend, QThread leitet man nicht mehr ab. Und auch lesender Zugriff auf GUI-Elemente ist kritisch, du kannst nicht einfach rowCount zB lesen.
Bulli
User
Beiträge: 10
Registriert: Mittwoch 22. Februar 2017, 12:45

So habe das alles mal neu überhohlt.
Mit deiner Info hats dann auch funktioniert und ich habs endlich verstanden (Ich hoffe es zumindest).
Vielen Dank!

Code: Alles auswählen

import os
import sys
import time
import queue

from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot

HAUPTFENSTER_UI = os.path.join(os.path.dirname(__file__), "Hauptfenster.ui")

# Klasse für den Loop
class WorkerThread(QObject):
    def __init__(self, gui):
        super().__init__()
        self.gui = gui
        self.gui.loopMode.connect(self.loopMode)
        self.gui.diCt.connect(self.dictUebergabe)
        self._loopSignal = queue.Queue()
        self._diCtSignal = queue.Queue()

    def dictUebergabe(self, dict):
        test = dict
        self._diCtSignal.put(test)

    def loopMode(self, int):
        test = int
        self._loopSignal.put(test)

    def run(self):
        while True:
            print("Start run ")
            loopMode = self._loopSignal.get()
            self._loopSignal.task_done()
            row = self._diCtSignal.get()

            while loopMode:
                print("Start LoopMode")

                task = row["task"]
                value = row["value"]
                note = row["note"]

                if task == "Aufgabe 1":
                    print("Aufgabe 1 ist:", value, note)
                    time.sleep(1)

                if task == "Aufgabe 2":
                    print("Aufgabe 2 ist:", value, note)
                    time.sleep(1)

                if task == "Aufgabe 3":
                    print("Aufgabe 3 ist:", value, note)
                    time.sleep(1)

                # Hier wird die Schleife loopMode mit StopButton aus dem Mainthread gestoppt
                if not self._loopSignal.empty():
                    self._loopSignal.get()
                    break

            print("Stop loopMode")


# Klasse fuers Hauptfenster
class Hauptfenster(QMainWindow):
    diCt = pyqtSignal(dict)
    loopMode = pyqtSignal(int)

    def __init__(self):
        super().__init__()
        ui = uic.loadUi(HAUPTFENSTER_UI, self)
        self.GUI_Shutdown = False

        self.BTN_ProgramStart = ui.ProgramStart
        self.BTN_ProgramStop = ui.ProgramStop
        self.BTN_ProgramStart.clicked.connect(self.BTN_ProgramStart_clicked)
        self.BTN_ProgramStop.clicked.connect(self.BTN_ProgramStop_clicked)
        self.BTN_ProgramStart.setEnabled(True)

        self.thread = QThread()
        self.worker = WorkerThread(self)
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        self.thread.start()

    def BTN_ProgramStart_clicked(self):
        self.loopMode.emit(1)
        # diCt1 Dictionary soll zur Auswertung an den Thread gesendet werden
        diCt1 = {"task": "Aufgabe 2", "value": 1, "note": 504}
        self.diCt.emit(diCt1)
        self.BTN_ProgramStart.setEnabled(False)
        self.BTN_ProgramStop.setEnabled(True)

    def BTN_ProgramStop_clicked(self):
        self.loopMode.emit(0)
        self.BTN_ProgramStop.setEnabled(False)
        self.BTN_ProgramStart.setEnabled(True)

    # Hier wird aufgeraeumt
    def closeEvent(self, event) -> None:
        self.thread.exit()
        print("Ablaufprogramm beendet")
        event.accept()
        self.GUI_Shutdown = True

# =====================================================================================================================
#  Hier gehts LOS, das Hauptprogramm
# =====================================================================================================================


if __name__ == "__main__":
    app = QApplication(sys.argv)
    gui = Hauptfenster()
    gui.show()
    sys.exit(app.exec_())
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das ist ein Fortschritt, aber immer noch Verbesserungswuerdig.

Ein deutlicher Code-Smell ist die Verbindungen, die der Worker selbst zur GUI herstellt. Sowas macht man grundsaetzlich nicht. Die Signal/Slots sind ein wunderbarer Mechanismus, um Entkopplung zu erreichen. Und das machst du kaputt, indem du dem Worker implizit Wissen mitgibst, wie die aufrufende Seite aussieht.

Stattdessen wuerde man diese connects nach der Erzeugung des workers, und vor allem *NACH* dem moveToThread, machen.

Und dann hast du gleich die Gelegenheit, den Worker auf einen Bruchteil seiner Groesse einzudampfen. Das ganze rum-ge-queue ist unnoetig. Durch den Worker in einem anderen Thread gibt es eine *implizite* Queue von Signalen aus dem Main-Thread (also _loopSignal etc), und der Ereignisschleife. Damit braucht du also gar keine extra queues mehr, stattdessen arbeitet der Worker ein Signal einfach im Thread ab.

Ohne Garantie, weil nur bearbeitet, aber nicht getestet:

Code: Alles auswählen

import os
import sys
import time
import queue

from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot

HAUPTFENSTER_UI = os.path.join(os.path.dirname(__file__), "Hauptfenster.ui")

# Klasse für den Loop
class WorkerThread(QObject):

    def dictUebergabe(self, dict):
        print("dictUebergabe")

    def loopMode(self, int):
        print("loopMode")

# Klasse fuers Hauptfenster
class Hauptfenster(QMainWindow):
    diCt = pyqtSignal(dict)
    loopMode = pyqtSignal(int)

    def __init__(self):
        super().__init__()
        ui = uic.loadUi(HAUPTFENSTER_UI, self)
        self.GUI_Shutdown = False

        self.BTN_ProgramStart = ui.ProgramStart
        self.BTN_ProgramStop = ui.ProgramStop
        self.BTN_ProgramStart.clicked.connect(self.BTN_ProgramStart_clicked)
        self.BTN_ProgramStop.clicked.connect(self.BTN_ProgramStop_clicked)
        self.BTN_ProgramStart.setEnabled(True)

        self.thread = QThread()
        self.worker = WorkerThread(self)
        self.worker.moveToThread(self.thread)
        self.loopMode.connect(worker.loopMode)
        self.diCt.connect(worker.dictUebergabe)
        # ab hier laeuft eine Event-Queue im QThread, die zum ausfuehren von slots genutzt werden kann.
        self.thread.start()

    def BTN_ProgramStart_clicked(self):
        self.loopMode.emit(1)
        # diCt1 Dictionary soll zur Auswertung an den Thread gesendet werden
        diCt1 = {"task": "Aufgabe 2", "value": 1, "note": 504}
        self.diCt.emit(diCt1)
        self.BTN_ProgramStart.setEnabled(False)
        self.BTN_ProgramStop.setEnabled(True)

    def BTN_ProgramStop_clicked(self):
        self.loopMode.emit(0)
        self.BTN_ProgramStop.setEnabled(False)
        self.BTN_ProgramStart.setEnabled(True)

    # Hier wird aufgeraeumt
    def closeEvent(self, event) -> None:
        self.thread.exit()
        print("Ablaufprogramm beendet")
        event.accept()
        self.GUI_Shutdown = True

# =====================================================================================================================
#  Hier gehts LOS, das Hauptprogramm
# =====================================================================================================================


if __name__ == "__main__":
    app = QApplication(sys.argv)
    gui = Hauptfenster()
    gui.show()
    sys.exit(app.exec_())
Antworten