PyQT5 - QTableWidgetItems[farbe,text] updaten

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
Nion
User
Beiträge: 6
Registriert: Dienstag 3. Februar 2015, 15:55

Hallo,

vorab zur Info: Ich habe erst vor einer Weile mit Python und GUI-Programmierung angefangen, also bitte nicht ganz so streng sein, falls ich was verrissen habe... :roll:

Ich "arbeite" an einem Programm, welches die Farben und Inhalte(Text) der Zellen/Items des QTableWidget kontinuierlich ändert. Es werden dann Ein Wert (umgewandelt in einen String) und die dazugehörgen RGB-Werte als Hintergrundfarbe angezeigt. Das Ändern dieser Werte geschieht in einem Thread (treading.Thread oder QThread - Welches ist da besser?). Später sol das als Anzeige für ausgelesene Sensordaten dienen.

Zunächst habe ich Folgendes versucht: Das Fenster wird normal geöffnet und der Thread soll pausieren und nach 2 Sekunden, dann die 64 Felder (4 Zeilen * 16 Spalten) testweise mit "Hallo" beschreiben. Das Problem ist Folgendes: die Werte werden erst angezeigt, wenn ich in die entsprechenden Zellen/Items anklicke oder das Fenster in der Größe verändere. Diese Veränderungen sollen aber sofort angezeigt werden.
(Später soll der Thread dann kontinuierlich laufen und auf Variablen zugreifen, die dann als String in den Zellen angezeigt werden.)

Scheinbar muss ich dem Programm einen "Anreiz" geben, damit es die neuen Inhalte/Farben anzeigt, aber wie mache ich das? :?:

Danke schonmal im Voraus!

LG Nion :wink:

ich verwende: PyQt5, Python 3.4, Eclipse mit Pydev, Windows7

Code: Alles auswählen

class UpdateWindow(QThread):
    
    def run(self):
        time.sleep(2)
        i=0
        j=0
        while i <4:
            while j<16:
                item = screen.ui.MLX90620.item(i, j)
                item.setText(QtCore.QCoreApplication.translate("Temperaturmessung","hallo"))
                brush = QtGui.QBrush(QtGui.QColor(10,100, 10))
                brush.setStyle(QtCore.Qt.SolidPattern)
                item.setBackground(brush)
                j+=1
            i+=1  
            j=0
        screen.update()
        print('ok')
            
class StartQT5(QMainWindow):
    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.ui = Ui_Temperaturmessung()
        self.ui.setupUi(self)
        
if 1:
    app = QApplication(sys.argv)
    screen = StartQT5()
    
    UW=UpdateWindow()
    UW.start()
    
    screen.show()
    app.exec_()
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Hallo und willkommen im Forum!

Du überhebst dich da wahrscheinlich ein wenig. Mit GUIs zu beginnen ist als Programmieranfänger keine besonders gute Idee, da brauchst du zu viele Kenntnisse in OOP und ereignisorientierter Programmierung um halbwegs vernünftig arbeiten zu können. Das geht aber deutlich über Grundkenntnisse hinaus. Ähnliches gilt für Threads: Diese liefern viele Tücken und sind keine besonders gute Idee für Anfänger. GUIs in Kombination mit Threads sind dann noch einmal eine harte Kombination, bei der man wieder sehr viel falsch machen kann.

Ich würde dir dringend anraten zunächst vernünftig Python zu lernen. Da bieten sich das Tutorial aus der Dokumentation oder Learn Python the Hard Way (nicht vom Namen abschrecken lassen) an. Sonst bekommst du dein Programm vielleicht irgendwie zum laufen, über kurz oder lang schießt du dir aber mit deinem momentanen Wissen über Python, GUIs und Threads selbst in den Fuß. Oder eher beide.
Das Leben ist wie ein Tennisball.
BlackJack

@Nion: Zum konkreten Problem: Schätze Dich glücklich dass das Programm nicht einfach hart abstürzt. Man darf die GUI nicht einfach aus einem anderen Thread manipulieren als dem in dem die GUI-Hauptschleife läuft. Wenn man das doch macht, kann alles mögliche passieren — das Programmverhalten ist dann undefiniert.

Das Signal/Slot-Konzept von Qt ist thread-sicher, das heisst das kann man verwenden um Daten vom Arbeiter-Thread in den GUI-Thread zu schicken. Da braucht man dann `QThread` weil man nur auf `QObject`-Klassen eigene Signale definieren kann.

Anstelle der ``while``-Schleifen hätten sich ``for``-Schleifen angeboten.

``if 1:`` sollte man besser als ``if True:`` schreiben ­— oder komplett weglassen, denn das macht nicht wirklich Sinn.

Das Hauptprogramm steht üblicherweise in einer Funktion. Dann kann man auch nicht einfach so unsauber auf `screen` zugreifen.
Nion
User
Beiträge: 6
Registriert: Dienstag 3. Februar 2015, 15:55

Erstmal: Danke für die Antworten. :)

Gäbe es eine andere Möglichkeit das Oben genannte zu realisieren? Später würden dann noch ein paar Auswahlfelder und Buttons dazu kommen. Würde dafür auch die Standard-Bibliothek TKinter reichen?

Ich hab mir schon einiges angesehen zu Signal/Slot. Solange das Signal von der GUI gesendet wird habe ich damit keine Probleme. Woher weiß ich, welche Slots ich explizit mit Signalen ansprechen soll? Muss ich auch Methoden, wie "setText" als signale implementieren?
BlackJack

@Nion: Grundsätzlich kann man das wohl auch in Tkinter lösen, allerdings ist da immer die Frage ab wo man anfangen muss Sachen selber zu schreiben die andere GUI-Rahmenwerke schon fertig liefern. Zum Beispiel wieviel funktionalität Du von einem `QTableWidget` benötigst, denn das hat Tkinter in der Form nicht. Da würde man um Dein Beispiel umzusetzen einen `Frame` erstellen und dort mit dem `grid()`-Layout 4×16 `Label`\s drin platzieren. Die müsste man sich selbst in einer geeigneten Datenstruktur merken um später über Zeilen- und Spaltennummer darauf zugreifen zu können um Text und Hintergrundfarbe ändern zu können. Und auch Tk ist nicht thread-sicher, hier müsste man sich die Kommunikation über eine `Queue` zwischen den Threads selber basteln.

In Python kannst Du bei Qt alles was aufrufbar ist einfach so als Slot verwenden ohne es als Slot registrieren zu müssen. Es gibt einen Dekorator den man zum registrieren verwenden kann, dann sind die Aufrufe geringfügig schneller als die vollständig dynamischen Aufrufe. Ich persönlich spare mir das eigentlich immer, denn ich hatte noch nie den Fall wo das ein messbares Problem gewesen wäre.

Die Frage nach `setText()` als *Signal* zu implementieren macht IMHO keinen Sinn, jedenfalls nicht wenn `setText()` das macht was der Name suggeriert. Apropos Namen: Klassen bekommen üblicherweise Namen die „Dinge” beschreiben, im Gegensatz zu Funktionen und Methoden die in der Regel Namen bekommen die Tätigkeiten beschreiben. Dein `UpdateWindow` würde beispielsweise besser `TemperatureReader` heissen. Der würde vielleicht dann ein Signal `new_temperature` haben das irgendeine ID für den Temperatursensor und die Temperatur als Daten hat. Und das Signal würde man dann mit einem entsprechenden Slot im GUI-Objekt verbinden das *dort* erst die Sensor-ID auf eine Zeilen- und Spalteninformation abbildet, und die entsprechende Anzeige veranlasst. Was mit der Temperatur passiert, also zum Beispie wie und wo die angezeigt wird, sollte den Code der die Temperaturen ausliest nämlich nichts angehen. Das gehört in die GUI und nicht in die Programmlogik.
Nion
User
Beiträge: 6
Registriert: Dienstag 3. Februar 2015, 15:55

Ich benötige nur ein 4+16 Feld in dem ich die Hintergrundfarbe und den Inhalt ändern kann. Da ich jetzt einmal angefangen habe mit QT würde ich schon gern dabei bleiben und nicht auf TKinter umsteigen. Ich würde in Zukunft auch gerne weiter mit PyQt arbeiten. :)

Also muss ich in der Gui (z.B. QTableWigdet) eine Slot definieren, der auf die Temperaturdaten zugreift, Farben zuordnet und dann in die Zellen schreibt. Von dem QThread muss dann nur ein Signal gesendet werden, was diesen Slot auslößt. Richtig so :?:
Und in der der GUI kann ich dann die Methoden des Widgets z.b. setText() u.ä. verwenden?
BlackJack

@Nion: Klingt fast richtig, nur dass die Daten, also welcher Sensor und welche Temperatur am besten als Daten mit dem Signal mitgeschickt werden, statt das sich das GUI-Objekt die dann aufgrund des Signals erst noch irgendwo holen muss.

In der Methode die mit dem Signal verbunden wird kannst Du ganz normal auf die GUI zugreifen, denn diese Methode wird von der GUI-Hauptschleife im GUI-Thread ausgeführt.
Nion
User
Beiträge: 6
Registriert: Dienstag 3. Februar 2015, 15:55

Ah Okay!
Gibt es eine maximale Anzahl an Argumenten für ein Signal bzw. welche Anzahl ist sinnvoll? Alle 64 Werte mit einem Signal zu übertragen scheint mir doch etwas viel. Wäre es dann sinnvoll mehrere Signale zu verwenden, die dann zum Beispiel, jeweils die Daten einer Spalte enthalten? Oder muss ich sogar ein Signal pro Messwert erzeugen?

Das hat mich jetzt verwirrt. Wird die Methode auch dann vom GUI-Thread ausgeführt, wenn diese Methode außerhalb des GUI-Threads steht?
BlackJack

@Nion: Die Anzahl der Argumente hat doch nicht wirklich etwas mit der Anzahl der Messwerte zu tun. Du kannst doch problemlos eine Liste mit allen Messwerten als *ein* Argument übergeben.

Was meinst Du mit „die Methode steht ausserhalb des GUI-Threads”? In welchem Thread eine Methode/Code ausgeführt wird hat nichts damit zu tun wo diese Methode im Quelltext steht, sondern von welchem Thread die Methode zur Laufzeit aufgerufen wird. Der `emit()`-Aufruf, egal in welchem Thread ausgeführt, sorgt dafür dass das Signal in die Ereignisverarbeitung von Qt gelangt. Und die Slots werden dann in dem Thread aufgerufen in dem die Schleife läuft in der die Hauptschleife läuft (zumindest in dem Fall der hier für Dich jetzt interessant ist.)
BlackJack

Beispiel (allerdings für Python 2.7 und PyQt4):

Code: Alles auswählen

import sys
from random import random
from time import sleep
from PyQt4.QtCore import pyqtSignal, QThread
from PyQt4.QtGui import (
    QApplication, QBrush, QColor, QMainWindow, QTableWidget, QTableWidgetItem
)


class TemperatureReader(QThread):

    new_temperatures = pyqtSignal(list)

    def __init__(self, sensor_count):
        QThread.__init__(self)
        self.sensor_count = sensor_count

    def run(self):
        while True:
            sleep(1)
            self.new_temperatures.emit(
                [random() * 100 for _ in xrange(self.sensor_count)]
            )


class TemperatureTable(QTableWidget):
    def __init__(self, rows, columns, parent):
        QTableWidget.__init__(self, rows, columns, parent)
        for i in xrange(rows):
            for j in xrange(columns):
                self.setItem(i, j, QTableWidgetItem())


    def set_temperatures(self, temperatures):
        for i, temperature in enumerate(temperatures):
            row, column = divmod(i, self.columnCount())
            item = self.item(row, column)
            item.setText(format(temperature, '.2f'))
            red_value = int(min(max(temperature, 0), 100) * 256 / 100)
            item.setBackground(
                QBrush(QColor(red_value, 255 - red_value, 255 - red_value))
            )


class MainWindow(QMainWindow):
    def __init__(self, temperature_reader, temperature_count, rows=4):
        QMainWindow.__init__(self)
        self.table = TemperatureTable(
            rows, int(temperature_count // rows + 0.5), self
        )
        self.setCentralWidget(self.table)
        temperature_reader.new_temperatures.connect(self.table.set_temperatures)


def main():
    application = QApplication(sys.argv)

    sensor_count = 64
    temperature_reader = TemperatureReader(sensor_count)

    window = MainWindow(temperature_reader, sensor_count)
    window.show()

    temperature_reader.start()
    sys.exit(application.exec_())


if __name__ == '__main__':
    main()
Nion
User
Beiträge: 6
Registriert: Dienstag 3. Februar 2015, 15:55

Daran hatte ich gar nicht gedacht - stimmt.

Entschuldige, da habe ich mich verschrieben. ich meinte File und nicht Thread. Also muss der pyqtSlot() nicht in dem vom QtDesigner erzeugtem File definiert werden?

Ich weiß leider noch nicht so genau wo ich die Slots hinpacke und ich es genau. Ich hab das Stückchen Code noch etwas bearbeitet. Es zeigt jetzt in einer Zelle im Sekundentakt die werte einer Liste an, die ich mit dem Signal übergeben habe.

Code: Alles auswählen

Daten=[0,1,2,3,4,5,6,7,8,9]

class UpdateSignal(QObject):
    NewText=pyqtSignal(list)
    i=0
    def __init__(self):
        QObject.__init__(self)
        i=0
    @pyqtSlot(list)
    def refresh(self, Liste):
        item = screen.ui.MLX90620.item(3, 0)
        item.setText(QtCore.QCoreApplication.translate("Temperaturmessung", str(Liste[self.i])))
        self.i+=1
class UpdateWindow(QThread):
    def run(self):
        while True:
            time.sleep(1)
            new.NewText.emit(Daten)
            print ('ok')

class StartQT5(QMainWindow):
    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.ui = Ui_Temperaturmessung()
        self.ui.setupUi(self)

app = QApplication(sys.argv)
screen = StartQT5()
    
screen.setUpdatesEnabled(True)
UW=UpdateWindow()
    
new=UpdateSignal()
new.NewText.connect(new.refresh)
    
UW.start()
screen.show()
app.exec_()

Kannst du da mal drüber sehen bitte?
Nion
User
Beiträge: 6
Registriert: Dienstag 3. Februar 2015, 15:55

OH, Wow danke, dass du dir die Mühe gemacht hast einen Beispielcode zu schreiben!! Das hilft mir sehr, danke! :!: :)

Wenn ich mir den so anschaue habe ich ja ein paar sachen ähnlich gemacht. (:

Danke, es funktioniert! (:

EDIT:
Für diejenigen, die der Beispielcode in Python 3 und PyQt5 haben wollen, müssen xrange() durch range() ersetzen und die Imports ändern:

Code: Alles auswählen

import sys
from random import random
from time import sleep
from PyQt5.QtCore import pyqtSignal, QThread
from PyQt5.QtGui import QBrush, QColor
from PyQt5.QtWidgets import QApplication, QMainWindow, QTableWidget, QTableWidgetItem
Antworten