Search code examples
pythonuser-interfacepyqt6qlineedit

How can I create a QLineEdit in PyQt6 that displays truncated text similar to how an Excel cell displays truncated text?


I created a custom QLineEdit with a fixed size so that when the user types in text larger than the QLineEdit, the text gets truncated. When the user clicks out of the QLineEdit, the cursor jumps back to the beginning of the QLineEdit. Below is my code:

from PyQt6.QtWidgets import QLineEdit

class CustomLineEdit(QLineEdit):
    def focusOutEvent(self, event):
        super().focusOutEvent(event)
        self.setCursorPosition(0)  # Move cursor to the beginning of the text

If the text is larger than the QLineEdit, I want to make it so that when the user clicks on the QLineEdit again, the text will overflow and display its full text, similar to how in Excel, when you click on a cell with a large chunk of text, the full text is displayed without affecting the cell size. Is there a way to do this without changing QLineEdit to QTextEdit?

To clarify this is what I have:
QLineEdit with truncated text

This is what I want:
This is what I want my QlineEdit to do

I know I can set a sizePolicy so that the QLineEdit expands to show its content, but this is not what I want. I have other widgets next to the QLineEdit that need to remain in their respective positions.


Solution

  • The only way to achieve this is by manually changing the geometry of the line edit.

    Unfortunately, it's not that simple, especially when layout managers are used (which, by the way, should be mandatory).

    Most importantly, we need to ensure that the geometry change is:

    • applied when the "something else" is trying to resize the widget (for instance, the parent resizing) and the widget is still focused;
    • restored back when focus is left;

    We also need to ensure that the manual resizing doesn't go beyond the parent geometry, meaning that we also need to check that the widget does have a parent. While this may seem trivial and unnecessary, it's actually quite important.

    All this can be achieved with a basic boolean flag that only alters the geometry when required, and eventually tries to restore it back when needed.

    A possible implementation will then override the following methods:

    • resizeEvent(), by possibly resizing the widget a further time (but also avoiding recursion, which is the reason for the boolean flag above);
    • focusInEvent() to change the geometry so that all required (and available, depending on the parent) horizontal space will be used;
    • focusOutEvent() to eventually restore the size previously externally set (usually, from the layout manager);

    Here is a possible implementation of the above:

    class OverflowLineEdit(QLineEdit):
        _recursionGuard = False
        _layoutGeo = QRect()
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.textChanged.connect(self._updateGeometry)
            self.setCursorPosition(0)
    
        def _setGeometry(self, geo):
            if geo.isValid():
                self._recursionGuard = True
                super().setGeometry(geo)
                self._recursionGuard = False
    
        def _updateGeometry(self):
            if self.isWindow():
                return
            self.ensurePolished()
    
            style = self.style()
            opt = QStyleOptionFrame()
            self.initStyleOption(opt)
    
            fm = QFontMetrics(self.font())
            cm = self.contentsMargins()
            minWidth = (
                fm.horizontalAdvance(self.text()) 
                + cm.left() + cm.right()
                # horizontal margin is hardcoded to 2
                + 4
                # add the cursor width to avoid scrolling if possible
                + style.pixelMetric(style.PixelMetrics.PM_TextCursorWidth, opt, self)
            )
    
            # adjust the minWidth to what the style wants
            minWidth = style.sizeFromContents(
                style.ContentsType.CT_LineEdit, opt, QSize(minWidth, fm.height()), self
            ).width()
    
            if self._layoutGeo.isValid():
                refWidth = self._layoutGeo.width()
            else:
                refWidth = self.width()
    
            if minWidth > refWidth:
                parent = self.parentWidget()
                # remove the margins of the parent widget and its layout; 
                # note that this doesn't consider nested layouts, and if we just 
                # want to extend to the maximum available width (which is that 
                # of the parent), "available" should just be "parent.rect()"
                margins = parent.contentsMargins()
                if parent.layout() is not None:
                    margins += parent.layout().contentsMargins()
                available = parent.rect().marginsRemoved(margins)
    
                if self._layoutGeo.isValid():
                    geo = QRect(self._layoutGeo)
                else:
                    geo = self.geometry()
    
                if available.width() < minWidth:
                    geo.setWidth(available.width())
                else:
                    geo.setWidth(minWidth)
    
                if geo.x() < available.x():
                    geo.moveLeft(available.x())
                if geo.right() > available.right():
                    geo.moveRight(available.right())
    
                self._setGeometry(geo)
                self.raise_()
    
            elif (
                self._layoutGeo.isValid()
                and minWidth < refWidth 
            ):
                # restore the default size (probably set by the layout)
                self._setGeometry(self._layoutGeo)
    
        def focusInEvent(self, event):
            super().focusInEvent(event)
            self._layoutGeo = self.geometry()
            self._updateGeometry()
    
        def focusOutEvent(self, event):
            super().focusOutEvent(event)
            self.setCursorPosition(0)
            self._setGeometry(self._layoutGeo)
    
        def resizeEvent(self, event):
            super().resizeEvent(event)
            if not self._recursionGuard:
                self._layoutGeo = self.geometry()
                if self.hasFocus() and self.isVisible():
                    self._updateGeometry()