PyQt LineEdit QAction change icon on checked/unchecked

569 Views Asked by At

I need to add a button to QLineEdit, set it checkable and change icon according to checked/unchecked state. I do this that way:

icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(os.path.join("Images", "L.png")), QtGui.QIcon.Normal, QtGui.QIcon.On)
icon.addPixmap(QtGui.QPixmap(os.path.join("Images", "Home.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
searchActionBttn = QtWidgets.QAction("None", self.searchIn)
searchActionBttn.triggered.connect(lambda: print(searchActionBttn.isChecked()))
searchActionBttn.setCheckable(True)
searchActionBttn.setIcon(icon)
self.searchIn.addAction(searchActionBttn, QtWidgets.QLineEdit.LeadingPosition)

But icon didn't change when I click on it.

2

There are 2 best solutions below

0
On

The problem is caused because the QToolButton associated with the QAction has its custom paintEvent method so it does not take into account the state of the checkbox

// https://code.qt.io/cgit/qt/qtbase.git/tree/src/widgets/widgets/qlineedit_p.cpp#n354
void QLineEditIconButton::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    QWindow *window = qt_widget_private(this)->windowHandle(QWidgetPrivate::WindowHandleMode::Closest);
    QIcon::Mode state = QIcon::Disabled;
    if (isEnabled())
        state = isDown() ? QIcon::Active : QIcon::Normal;
    const QLineEditPrivate *lep = lineEditPrivate();
    const int iconWidth = lep ? lep->sideWidgetParameters().iconSize : 16;
    const QSize iconSize(iconWidth, iconWidth);
    const QPixmap iconPixmap = icon().pixmap(window, iconSize, state, QIcon::Off);
    QRect pixmapRect = QRect(QPoint(0, 0), iconSize);
    pixmapRect.moveCenter(rect().center());
    painter.setOpacity(m_opacity);
    painter.drawPixmap(pixmapRect, iconPixmap);
}

(emphasis mine)

As you can see, the state only changes while the button is pressed, so the option of using a QAction should be discarded.

Another option is to set the QToolButton directly through a layout

import os
from functools import partial
from PyQt5 import QtCore, QtGui, QtWidgets


class LineEdit(QtWidgets.QLineEdit):
    @property
    def _internal_layout(self):
        if not hasattr(self, "_internal_layout_"):
            self._internal_layout_ = QtWidgets.QHBoxLayout(self)
        self._internal_layout_.addStretch()
        self._internal_layout_.setContentsMargins(2, 2, 2, 2)
        return self._internal_layout_

    def add_button(self, button):
        self._internal_layout.insertWidget(self._internal_layout.count() - 2, button)
        QtCore.QTimer.singleShot(0, partial(self._fix_cursor_position, button))
        button.setFocusProxy(self)

    def _fix_cursor_position(self, button):
        self.setTextMargins(button.geometry().right(), 0, 0, 0)


app = QtWidgets.QApplication([])

icon = QtGui.QIcon()
icon.addPixmap(
    QtGui.QPixmap(os.path.join("Images", "L.png")), QtGui.QIcon.Normal, QtGui.QIcon.On
)
icon.addPixmap(
    QtGui.QPixmap(os.path.join("Images", "Home.png")),
    QtGui.QIcon.Normal,
    QtGui.QIcon.Off,
)

button = QtWidgets.QToolButton()
button.setStyleSheet("border: none")
button.setCheckable(True)
button.setIcon(icon)

searchIn = LineEdit()
searchIn.add_button(button)

searchIn.show()

app.exec_()
0
On

You can have 2 actions and use removeAction/addAction to toggle between them

icon_on = QtGui.QIcon()
icon_on.addPixmap(QtGui.QPixmap(os.path.join("Images", "L.png")))
searchActionBttn_on = QtWidgets.QAction("None", self.searchIn)
searchActionBttn.setIcon(icon_on)

icon_off = QtGui.QIcon()
icon_off.addPixmap(QtGui.QPixmap(os.path.join("Images", "Home.png")))    
searchActionBttn_off = QtWidgets.QAction("None", self.searchIn)
searchActionBttn.setIcon(icon_off)  

self.searchIn.addAction(searchActionBttn, QtWidgets.QLineEdit.LeadingPosition)

def toggle(to_remove, to_add):
    self.searchIn.addRemove(to_remove)
    self.searchIn.addAction(to_add, QtWidgets.QLineEdit.LeadingPosition) 

searchActionBttn_on.triggered.connect(lambda: toggle(searchActionBttn_on, searchActionBttn_off)
searchActionBttn_off.triggered.connect(lambda: toggle(searchActionBttn_off, searchActionBttn_on)