Texte in den Widgets kürzen

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo Leute,

im diesem lauffähigen Quelltext bzw. Programm geht es darum, dass bestimmte Texte (hier in diesem Beispiel) im QLabel und QLineEdit gekürzt werden soll, sobald das Fenster zu sehr verkleinert wird. Dieses Phänomen begegnet uns häufig dort, wenn der Pfad zu lang ist, wird dieser entweder in der Mitte oder am Ende gekürzt, damit der Inhalt weiterhin im Blick bleibt und nicht "verschwindet".

Was habe ich getan? Ich habe sowohl für QLabel als auch für QLineEdit jeweils eine Subclass geschrieben, um die Klassen der jeweiligen Widgets zu überschreiben. Das Kürzen der Texte klappt auch soweit. Allerdings habe ich beim QLineEdit ein kleines Problem. Sobald ich diese Anwendung starte, sieht mein QLineEdit aus wie ein Label und nicht eben wie ein QLineEdit. Was genau habe ich hier übersehen?

Code: Alles auswählen

import sys
from PyQt4.QtCore import Qt
from PyQt4.QtGui import QApplication,\
                        QLineEdit,\
                        QLabel,\
                        QFontMetrics,\
                        QHBoxLayout,\
                        QVBoxLayout,\
                        QWidget,\
                        QIcon,\
                        QPushButton,\
                        QToolTip,\
                        QBrush,\
                        QColor,\
                        QFont,\
                        QPainter

qt_app = QApplication(sys.argv)

class Example(QWidget):
    def __init__(self):
        QWidget.__init__(self)
        self.setMinimumWidth(100)
        
        self.init_ui()
        
    def init_ui(self):
        v_layout = QVBoxLayout()
        v_layout.addStretch(1)
       
        lbl = ExtendedTruncateTextLabel(self)
        lbl.setText("This is a really, long and poorly formatted runon sentence used to illustrate a point.")

        lbl_1 = ExtendedTruncateTextLabel(self)
        lbl_1.setText("Here you also see a long text, but isn't important.")

        l_text = ExtendedTruncateTextLineEdit()
        l_text.setText("In the widget namend QLineEdit is also a very long text.")
        
        v_layout.addWidget(lbl)
        v_layout.addWidget(lbl_1)
        v_layout.addWidget(l_text)
        
        self.setLayout(v_layout)

    def run(self):
        self.show()
        qt_app.exec_()
 
class ExtendedTruncateTextLineEdit(QLineEdit):
    def __init(self, parent):
        QLineEdit.__init__(self, parent)
       
    def paintEvent(self, event):
        painter = QPainter(self)

        metrics = QFontMetrics(self.font())
        elided  = metrics.elidedText(self.text(), Qt.ElideMiddle, self.width())

        painter.begin(self)
        painter.drawText(self.rect(), self.alignment(), elided)
        painter.end()

class ExtendedTruncateTextLabel(QLabel):
    def __init(self, parent):
        QLabel.__init__(self, parent)
        
    def paintEvent(self, event):
        painter = QPainter(self)

        metrics = QFontMetrics(self.font())
        elided  = metrics.elidedText(self.text(), Qt.ElideMiddle, self.width())

        painter.begin(self) 
        painter.drawText(self.rect(), self.alignment(), elided)
        painter.end()

if __name__ == '__main__':
    app = Example()
    app.run()
Benutzeravatar
Madmartigan
User
Beiträge: 200
Registriert: Donnerstag 18. Juli 2013, 07:59
Wohnort: Berlin

Das liegt daran, dass paintEvent(self, event) die Standard-Darstellung überschreibt. Du musst natürlich den Aufruf des Events an QLineEdit "weiterleiten".
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Madmartigan: Ich verstehe das nicht ganz. Den Event-Aufruf an QLineEdit weiterleiten? Ich bin gerade etwas verwirrt.
Benutzeravatar
Madmartigan
User
Beiträge: 200
Registriert: Donnerstag 18. Juli 2013, 07:59
Wohnort: Berlin

Schau dir einfach mal paintEvent() von ExtendedTruncateTextLineEdit an.

Was wird da gezeichnet? ... Nur der "gekürzte" Text. :wink:

Wenn du das Widget ebenfalls gezeichnet haben möchtest, musst du vorher paintEvent(...) der Parent-Klasse aufrufen. In deinem Falle fehlt also der Aufruf von paintEvent(self, event) der Basisklasse QLineEdit.

Existiert eigentlich ein spezieller Grund, warum du paintEvent überschreibst, anstatt einfach das Text Attribut des QLineEdit zu setzen?
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Madmartigan : Nur damit ich dich etwas verstehe. Dieses paintEvent() ist ein EventHandler, richtig? In diesem Falle wird es in der ExtendedTruncateTextLineEdit()-Klasse reimplementiert. Das heißt, beim Aufruf dieser Klasse wird das Event sofort mit ausgelöst. Daher bin ich verwirrt, wo ich es vorher aufrufen soll.

Hier mein Beispiel, ob ich dich richtig verstanden habe:

Code: Alles auswählen

class ExtendedTruncateTextLineEdit(QLineEdit):   
    def __init(self, parent):
        
        QLineEdit.__init__(self, parent)
       
    def paintEvent(self, event):

        """ Handle the paint event for the basic class of QLineEdit.
  
        This paint handler draws the text and the widget.
  
        """
        super(ExtendedTruncateTextLineEdit, self).paintEvent(event)
        
        painter = QPainter(self)
        
        metrics = QFontMetrics(self.font())
        elided  = metrics.elidedText(self.text(), Qt.ElideMiddle, self.width())

        painter.drawText(self.rect(), self.alignment(), elided)
       
Allerdings habe ich hier ein Problem. Ich sehe den Text total verschwommen.

Zu deiner Frage: Ich hatte nicht daran gedacht. Wie sähe das denn aus?
BlackJack

@Sophus: Was heisst ”verschwommen”? Schau mal genau hin, Du lässt rufst den ursprünglichen Code zum Zeichnen auf und danach zeichnest Du da noch deinen gekürzten Text drüber. Da der ursprüngliche Code zum Zeichnen alles zeichnet, also Rahmen, Hintergrund, *und Text*, macht das so nicht viel Sinn.

Interessant wäre auch wie sich Dein abgeleitet LineEdit eigentlich verhält wenn der Benutzer den gekürzt angezeigten Text bearbeiten will? Funktioniert das überhaupt sinnvoll?
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Black: Ich wurde darauf hingewiesen, dass ich das drawEvent-Ereignis an die Basisklasse der QLineEdit "weiterleiten" soll. Daher dachte ich, ich verbinde beim Aufruf der drawEvent-Funktion auf diesem Weg super(ExtendedTruncateTextLineEdit, self).paintEvent(event) an die Basis Klasse. Vermutlich und mit Sicherheit habe ich hier komplett falsch gedacht.

Aber selbst, wenn ich das wie folgt mache, erziele ich nicht den gewünschten Effekt: QLineEdit wird nicht gezeichnet, sondern nur der Text.

Code: Alles auswählen

class ExtendedTruncateTextLineEdit(QLineEdit):   
    def __init(self, parent):
        QLineEdit.__init__(self, parent).paintEvent(event)
       
    def paintEvent(self, event):
       
        painter = QPainter(self)
        
        metrics = QFontMetrics(self.font())
        elided  = metrics.elidedText(self.text(), Qt.ElideMiddle, self.width())

        painter.drawText(self.rect(), self.alignment(), elided)
BlackJack

@Sophus: Natürlich wird bei dem Quelltext nur der Text gezeichnet, denn der dort gezeigte Code zeichnet ja nur den Text.

Die Methode der Basisklasse aufzurufen funktioniert nicht, denn die zeichnet alles, inklusive dem ungekürzten Text.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Und wie wäre der Lösungsansatz? Irgendwie sehe ich den Wald vor lauter Bäumen nicht.
BlackJack

@Sophus: Mein Lösungsansatz wäre es erst einmal zu klären wie sich das denn überhaupt sinnvoll verhalten soll wenn der Benutzer versucht den Text zu bearbeiten. Denn dafür ist dieses Widget schliesslich da.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

In meinem Fall wird das Widget QLineEdit nur lesend freigegeben. Dort soll der Pfad zu einer bestimmten Datei ausgegeben werden. Rechts neben dem LineEdit-Widget ist dann ein pushButton. Wenn man also den Pfad ändern will, betätigt man einfach den pushButton und das entsprechende Dialog-Fenster öffnet sich. Es ist dem Anwender also nicht möglich direkt über das LineEdit-Widget den Pfad zu ändern. Aber du bringst dennoch eine Interessante Überlegung mit. Denn es soll ja nicht schaden, über den eigenen Tellerrand hinauszusehen.
BlackJack

@Sophus: Na wenn das eh nur zur Anzeige dienen soll, dann kannst Du doch einfach wie schon vorgeschlagen den Text selber kürzen und den normalen Code zur Anzeige verwenden.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

In meinem Fall sieht aber die QLineEdit nicht aus wie ein Widget als solches, sondern wie ein Label, und genau da stecke ich buchstäblich fest.
Benutzeravatar
Madmartigan
User
Beiträge: 200
Registriert: Donnerstag 18. Juli 2013, 07:59
Wohnort: Berlin

Ein Widget vom Typ QLineEdit macht aber auch nur wirklich Sinn, wenn der Nutzer da eine Eingabe machen kann. Wenn es lediglich der Anzeige dient, dann reicht doch ein QLabel völlig aus. Dafür müsstest du ja nicht einmal eine eigene Klasse aberben.

Verschwommen siehst du den Text in deinem Beispiel, da du ihn zwei mal zeichnest. Das erste Mal zeichnet das QLineEdit seinen Content für dich und dann zeichnest du mit dem QPainter nochmals drüber.
Du müsstest also effektiv das innere Rechteck des Eingabefeldes mit einer undurchlässigen Farbe (weiß) füllen, bevor du den Text zeichnest. Das hört sich zwar komisch, ist aber zudem auch noch völlig unnötig. Meine Frage hinsichtlich des Grundes für das Überschreiben von paintEvent() war ja absichtlich gestellt.

Wenn du unbedingt den Look eines QLineEdit haben willst, dann musst du das Widget eben auch selbst zeichnen. Eine Hälfte davon und die andere Hälfte von was anderem ... das geht eben nicht. ;-)
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Madmartigan : Ich bin noch einmal auf deinem Text-Attribut-Hinweis zurückgekommen. Was habe ich gemacht? Ich habe im Resize-Ereignis angesetzt, und mir gedacht: Sobald das Fenster in seiner Größe verändert wird, werden auch die darin befindlichen Widgets verändert. Und der Text wird im setText()-Attribut gekürzt, bzw. wieder hergestellt.

Allerdings habe ich hierbei ein kleines kosmetisches Problem. Sobald das Fenster verkleinert und der Text im LineEdit verkürzt wird, verschwindet ein kleiner Bruchteil des Textes in der linken Ecke. Links im LineEdit verschwinden die ersten 2-3 Buchstaben.

Code: Alles auswählen

import sys
from PyQt4.QtCore import Qt
from PyQt4 import QtCore
from PyQt4.QtGui import QApplication,\
                        QStyleOptionFrameV2,\
                        QLineEdit,\
                        QLabel,\
                        QFontMetrics,\
                        QHBoxLayout,\
                        QVBoxLayout,\
                        QWidget,\
                        QIcon,\
                        QPushButton,\
                        QToolTip,\
                        QBrush,\
                        QColor,\
                        QFont,\
                        QPalette,\
                        QStyle,\
                        QPainter

qt_app = QApplication(sys.argv)

class Example(QWidget):
    def __init__(self):
        QWidget.__init__(self)
        self.setMinimumWidth(100)

        self.fm = QFontMetrics(self.font())
        self.mText = QtCore.QString("This is a really, long and poorly formatted runon sentence used to illustrate a point")
        self.line_edit_text = QLineEdit() 
        
        self.initUI()
                    
    def initUI(self):
    
        v_layout = QVBoxLayout()
        v_layout.addStretch(1)       
        v_layout.addWidget(self.line_edit_text )
        self.setLayout(v_layout)

    def resizeEvent( self, event ) :
        self.line_edit_text .setText(self.fm.elidedText( self.mText, Qt.ElideMiddle, event.size().width() ) )

    def run(self):
        self.show()
        qt_app.exec_()
        
if __name__ == '__main__':
    app = Example()
    app.run()
Benutzeravatar
Madmartigan
User
Beiträge: 200
Registriert: Donnerstag 18. Juli 2013, 07:59
Wohnort: Berlin

Das Problem ist hier:

Code: Alles auswählen

self.line_edit_text .setText(self.fm.elidedText( self.mText, Qt.ElideMiddle, event.size().width() ) )
event.size().width() ist nicht die korrekte Breite gegen die du prüfen willst. Was du brauchst ist doch eher die Breite des QLineEdits.

Code: Alles auswählen

self.line_edit_text .setText(self.fm.elidedText( self.mText, Qt.ElideMiddle, self.line_edit_text.width() ) )
Du kannst da aus kosmetischen Gründen (für pixel-Perfektionismus :wink: ) noch ein Offset hinzugeben.

Code: Alles auswählen

self.line_edit_text .setText(self.fm.elidedText( self.mText, Qt.ElideMiddle, self.line_edit_text.width() - 10 ) )
Dann wird der Text rechtzeitig gekürzt und es "verschwindet" kein Text mehr.

Nur am Rande:

Code: Alles auswählen

if __name__ == '__main__':
    app = Example()
    app.run()
Die Variable nennst du app, aber Example ist ein QWidget, keine QApplication. Run() ist da etwas verwegen...
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Madmartigan Besten Dank für die Hilfestellung. Eine Frage: Inwiefern ist run() etwas verwegen? Gut, es sieht nicht standardisiert aus, wenn man in der Example()-Klasse eine run()-Funktion schreibt, um daraus die eigentliche QApplication()-Klasse aufzurufen. Aber falsch ist diese Art doch nicht oder? Oder erkaufe ich mir dabei Nachteile?

Und dann habe ich zu deinem Offset noch eine Frage. Was besagt die Zahl -10? Heißt das, sobald -10 Pixel erreicht wird, wird der Text gekürzt?
Benutzeravatar
Madmartigan
User
Beiträge: 200
Registriert: Donnerstag 18. Juli 2013, 07:59
Wohnort: Berlin

Sophus hat geschrieben:Aber falsch ist diese Art doch nicht oder? Oder erkaufe ich mir dabei Nachteile?
Falsch ... nicht zwingend, aber nicht zu empfehlen. (1) qt_app ist global deklariert. (2) Warum muss deine Example-Klasse Kenntnis der QApplication-Instanz besitzen?
Sophus hat geschrieben:Und dann habe ich zu deinem Offset noch eine Frage. Was besagt die Zahl -10? Heißt das, sobald -10 Pixel erreicht wird, wird der Text gekürzt?
Nein, die -10 ist nur ein Beispiel. Ich würde dafür auch keine magic-number nehmen, sondern das Offset an entsprechender Stelle deklarieren. Wenn das nur an der Stelle im Code verwendet wird, ist die -10 aber auch ok. Man fragt sich eben nur nach ein paar Wochen/Monaten mal schnell nach deren Herkunft. Daher ist es besser dafür eine gescheite Variable zu vergeben, deren Name aussagekräftig ist.

In dem Beispiel wird also geprüft, ob die Textbreite > (Eingabefeldbreite - 10px) ist. Dadurch wird der Text bereits gekürzt, bevor er die maximale Breite des QLineEdit erreicht. Probier es einfach aus, du siehst den Effekt sofort.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Verdammte Axt! Es war einfach zu schön um wahr zu sein. Ich habe meinen vorherigen Quelltext dahingehend modifiziert, dass ich einen Text nach dem Start des Programmes in das vorgesehene QLineEdit-Widget eintippe, und dann die Fenstergröße verändere. Was habe ich getan? Durch das textChanged()-Attribut der QLineEdit wird die Eingabe mittels der own_textChanged()-Funktion in die self.mText-Variable gespeichert. Anschließend verändere ich die Fenstergröße, der Text wird auch gekürzt, aber beim vergrößern der Fenstergröße wird der Text nicht mehr vollständig angezeigt - der Text behält seine gekürzte Version bei.

Meine naive Erklärung wäre: Durch die elidedText()-Methode von der QFontMetrics()-Klasse überschreibt beim Kürzen des Textes die self.mText-Variable. Und beim Vergrößern des QLineEdit-Widgets ist kein "originaler" Text vorhanden, um vom gekürzten Text zum ursprünglichen Text zu gelangen. In Anbetracht meiner naiven Erklärung weiß ich leider nicht, wie ich dieses Problem umgehen kann. Eine zweite Variable anlegen, in der der originale unangetastete Text hinterlegen wurde, erscheint mir sinnlose.

Bevor die Frage auftaucht, weshalb ich einen Text zur Laufzeit kürzen will. Mir geht es darum, dass bestimmte Informationen erst zur Laufzeit in die QLineEdit geladen werden, zum Beispiel aus einer Datenbank oder einer Textdatei. Das heißt, der nachträglich geladene Text muss kürzbar und wieder herstellbar sein. Um das zu simulieren wollte ich auf Datenbankanbindungen verzichten, und auf einfacher Weise eine Eingabe in die QLineEdit vornehmen.

Code: Alles auswählen

import sys
from PyQt4.QtCore import Qt
from PyQt4 import QtCore
from PyQt4.QtGui import QApplication,\
                        QStyleOptionFrameV2,\
                        QLineEdit,\
                        QLabel,\
                        QFontMetrics,\
                        QHBoxLayout,\
                        QVBoxLayout,\
                        QWidget,\
                        QIcon,\
                        QPushButton,\
                        QToolTip,\
                        QBrush,\
                        QColor,\
                        QFont,\
                        QPalette,\
                        QStyle,\
                        QPainter

qt_app = QApplication(sys.argv)

class Example(QWidget):
    def __init__(self):
        QWidget.__init__(self)
        
        self.setMinimumWidth(100)

        self.fm = QFontMetrics(self.font())
        self.line_edit_text = QLineEdit()

        self.line_edit_text.textChanged.connect(self.own_textChanged)
                         
        self.mText = QtCore.QString()
        
        self.initUI()

    def own_textChanged(self, prompt_text):
        self.mText = prompt_text
    
    def initUI(self):
    
        v_layout = QVBoxLayout()
        v_layout.addStretch(1)       
        v_layout.addWidget(self.line_edit_text)
        self.setLayout(v_layout)

    def resizeEvent(self, event) :
        if self.mText == "" or None:
            pass
        else:
            try:
                self.line_edit_text.setText(self.fm.elidedText(self.mText, Qt.ElideMiddle, self.line_edit_text.width() -10))
            except: pass

    def run(self):
        self.show()
        qt_app.exec_()
        
if __name__ == '__main__':
    app = Example()
    app.run()
Benutzeravatar
Madmartigan
User
Beiträge: 200
Registriert: Donnerstag 18. Juli 2013, 07:59
Wohnort: Berlin

Es war eigentlich abzusehen, dass das passiert, oder? ;-)
Blackjack hatte die Frage ja bereits gestellt - soll der Nutzer in der Lage sein den Text zu editieren? Wenn ja, was hast du dir dafür überlegt?

Eine zusätzliche Variable zur Speicherung des Originaltextes wäre nicht unüblich, allerdings bist du die Probleme damit nicht automatisch los. Du müsstest in dem Falle immer noch herausfinden, ob der Nutzer den Text gerade ändert, damit du den Originaltext in der Eingabe zur Verfügung stellen kannst.

*Pseudo*
- Setze Text programmatisch
- Prüfe Textbreite > Eingabefeldbreite -> JA? Speichere Originaltext in Variable, kürze angezeigten Text

Bis hierhin kein Problem, oder?

- will Nutzer Text ändern -> JA? ersetze gekürzten Text mit Originaltext

Die Stelle ist bereits nicht mehr trivial. Du müsstest eventuell auf die Fokus-Events hören, denn der Text muss ja ersetzt werden, BEVOR der Nutzer Änderungen vornehmen darf.

-------

Eine mögliche Idee wäre z.B. ein QLineEdit mit zwei Modi.

(1) Read-Only: der Originaltext wird gespeichert und gekürzt angezeigt.
(2) Edit-Modus: Der Nutzer "aktiviert" per Click/DoppelClick den Edit-Modus, der gekürzte Text wird durch den Originaltext ersetzt.

Im Modus 2 kann also auf dem Originaltext gearbeitet werden. Wichtig ist nun, beim Verlust des Fokus den Text wieder in der Variable zu speichern und ggf. in der Anzeige zu kürzen. Damit befindet sich das Eingabefeld quasi wieder im Zustand 1.

Ich würde ggf. das Konzept überdenken, für den simplen Anwendungsfall scheint mir der Aufwand einfach nicht gerechtfertigt. Generell würde ich auch überlegen, ob der Nutzer diese Texte wirklich ändern können muss. Wenn nicht, dann verwende ein QLabel zur Anzeige, oder setze alle QLineEdit einfach auf Read-Only.
Antworten