Можно ли настроить расстояние между значками QlineEdit?

Я планирую использовать QLineEdit с тремя действиями, добавленными через addAction().
Достаточно просто и выглядит это так: (квадратики в качестве значков для примера)

Но небольшое неудобство заключается в том, что расстояние между значками кажется мне слишком большим.
Можно ли отрегулировать это расстояние? QLineEdit, похоже, не имеет доступного макета, в котором можно было бы установить интервал.

На самом деле это QToolButtons. Вы можете попробовать создать подкласс QLineEdit и обновить их геометрию на resizeEvent()

musicamante 26.08.2024 16:57

@musicamante, я как бы предполагал, что изменение размера кнопок инструментов не изменит интервал. Как вы думаете, так и будет?

mahkitah 26.08.2024 17:11

Вам нужно будет вызвать setGeometry(). Я бы вызвал базу resizeEvent(), затем получил геометрию всех кнопок, перевел их, используя крайнюю правую в качестве ссылки, и вызвал setGeometry() с новыми прямоугольниками.

musicamante 26.08.2024 17:24

@musicamante, мне удалось заставить его работать достаточно хорошо, вызвав setGeometry для кнопок из переопределенного события resizeEvent. Но я вижу, что вы были заняты поиском действительно правильного решения.

mahkitah 27.08.2024 11:20

Да, на самом деле я понял, что этот интервал меня часто раздражал, и решил более тщательно продумать его реализацию. setGeometry() одних может быть недостаточно, поскольку они не учитываются для эффективного расчета полей текста.

musicamante 27.08.2024 18:14
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
5
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Действия, показанные в QLineEdit (включая действия, используемые для «кнопки очистки»), реализованы конфиденциально.

addAction(<action|icon>, position) — это перегрузка QWidget::addAction(), которая после вызова базовой реализации также создает экземпляр частного подкласса QToolButton (QLineEditIconButton).
Причина использования QToolButton заключается в том, что он обеспечивает немедленное взаимодействие с мышью, а также имеет тесную связь с QActions (см. QToolButton::setDefaultAction()).

Затем каждая кнопка соответствует общим «параметрам боковых виджетов», которые используют жестко запрограммированные значения:

QLineEditPrivate::SideWidgetParameters QLineEditPrivate::sideWidgetParameters() const
{
    Q_Q(const QLineEdit);
    SideWidgetParameters result;
    result.iconSize = q->style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, q);
    result.margin = result.iconSize / 4;
    result.widgetWidth = result.iconSize + 6;
    result.widgetHeight = result.iconSize + 2;
    return result;
}

Это много пробелов и полей; это может подойти для редактирования относительно широких строк или для систем, использующих большие значки и/или огромные шрифты, но также является значительной тратой места в большинстве случаев при использовании более одного действия (возможно, включая кнопку «Очистить»): даже при использовании базового значка размером 16 пикселей. размера, это означает, что на каждые 2 значка возможно хватит места для еще одного.

В любом случае, как только добавляется новое действие, QLineEdit размещает эти «фальшивые кнопки» на основе приведенных выше значений, располагая их с помощью result.margin и учитывая увеличенную ширину.

Обратите внимание, что эти кнопки даже не отображаются как настоящие QToolButtons, поскольку частный подкласс QLineEditIconButton также переопределяет свой собственный paintEvent(). Фактически, он просто рисует растровое изображение значка (используя включенное и нажатое состояние действия/кнопки) внутри QToolButton rect().

К счастью, рисование и взаимодействие с мышью всегда основаны на геометрии кнопок, а это означает, что мы можем программно изменить их геометрию после того, как они установлены (и при этом сохранять полную функциональность), что происходит в следующих случаях:

  • добавляется новое действие (которое автоматически создает соответствующую кнопку);
  • существующее действие удалено/удалено;
  • размер виджета изменен;
  • layoutDirection меняется;

Для самых простых приложений, которые учитывают только «следящие действия», мы могли бы просто рассмотреть первые три случая выше (и текст слева направо), а затем произвольно обновить геометрию кнопок инструментов на основе предполагаемых размеров и положений; Однако этого может быть недостаточно, поскольку действия могут быть с обеих сторон, а направление макета также инвертирует положение (и порядок) этих действий.

Кроме того, QLineEdit учитывает эффективные поля текста («ограничивающий прямоугольник», в котором текст фактически отображается и с которым можно взаимодействовать) на основе приведенных выше «параметров бокового виджета», что означает, что после того, как мы уменьшили размер и/или расстояние между кнопок, у нас по-прежнему остается то же, возможно, небольшое горизонтальное пространство для отображаемого текста и перемещения курсора.

Это означает, что нам нужно отслеживать положение каждого действия (ведущего или завершающего) и, наконец, обновлять текстовые поля с помощью дополнительной «частной» реализации.

В следующем примере я показываю возможную реализацию всего описанного выше, которая в большинстве случаев должна работать нормально. Могут возникнуть некоторые проблемы с действиями, которые явно скрыты (но я проверю это позже, так как у меня сложилось впечатление, что это может быть ошибка, см. примечание ниже[1]).

Он также должен нормально работать как для PyQt6, так и для PySide6, а также для PyQt5 и PySide2 (за исключением пространств имен перечислений для старых версий PyQt5 и PySide2).

class CustomLineEdit(QLineEdit):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__textMargins = QMargins()
        self.__iconMargins = QMargins()

        self.__leadingActions = set()
        self.__trailingActions = set()
        self.__actions = {
            QLineEdit.ActionPosition.LeadingPosition: self.__leadingActions, 
            QLineEdit.ActionPosition.TrailingPosition: self.__trailingActions
        }

    def __updateGeometries(self):
        buttons = self.findChildren(QToolButton)
        if len(buttons) <= 1:
            return

        iconSize = self.style().pixelMetric(
            QStyle.PixelMetric.PM_SmallIconSize, None, self)
        iconMargin = max(1, iconSize // 8)
        btnWidth = iconSize + 2

        leading = []
        trailing = []
        for button in buttons:
            if button.defaultAction() in self.__leadingActions:
                leading.append(button)
            else:
                trailing.append(button)

        if self.layoutDirection() == Qt.LayoutDirection.RightToLeft:
            leading, trailing = trailing, leading

        if leading:
            if len(leading) > 1:
                leading.sort(key=lambda b: b.x())
                lastGeo = leading[-1].geometry()
                it = iter(leading)
                prev = None
                while True:
                    button = next(it, None)
                    if not button:
                        break
                    geo = button.geometry()
                    geo.setWidth(btnWidth)
                    if prev:
                        geo.moveLeft(prev.x() + prev.width() + iconMargin)
                    button.setGeometry(geo)
                    prev = button
            else:
                button = leading[0]
                lastGeo = button.geometry()
                geo = button.geometry()
                geo.setWidth(btnWidth)
                button.setGeometry(geo)
            left = geo.right() - lastGeo.right()
        else:
            left = 0

        if trailing:
            if len(trailing) > 1:
                trailing.sort(key=lambda b: -b.x())
                lastGeo = trailing[-1].geometry()
                it = iter(trailing)
                prev = None
                while True:
                    button = next(it, None)
                    if not button:
                        break
                    geo = button.geometry()
                    if prev:
                        geo.setWidth(btnWidth)
                        geo.moveRight(prev.x() - iconMargin)
                    else:
                        geo.setLeft(geo.right() - btnWidth + 1)
                    button.setGeometry(geo)
                    prev = button
            else:
                button = trailing[0]
                lastGeo = button.geometry()
                geo = button.geometry()
                geo.setLeft(geo.right() - btnWidth + 1)
                button.setGeometry(geo)
            right = lastGeo.x() - geo.x()
        else:
            right = 0

        self.__iconMargins = QMargins(left, 0, right, 0)
        super().setTextMargins(self.__textMargins + self.__iconMargins)

    # Note that these are NOT "real overrides"
    def addAction(self, *args):
        if len(args) != 2:
            # possibly, the default QWidget::addAction()
            super().addAction(*args)
            return

        arg, position = args
        if isinstance(arg, QAction):
            action = arg
            # check if the action already exists in a different position, and 
            # eventually remove it from the related set
            if (
                position == QLineEdit.ActionPosition.LeadingPosition
                and action in self.__trailingActions
            ):
                self.__trailingActions.discard(action)
            elif action in self.__leadingActions:
                self.__leadingActions.discard(action)
            super().addAction(action, position)
            self.__actions[position].add(action)
            # addAction(action, position) is "void" and returns None
            return

        action = super().addAction(arg, position)
        self.__actions[position].add(action)
        # for compliance with the default addAction() behavior
        return action

    def textMargins(self):
        return QMargins(self.__textMargins)

    def setTextMargins(self, *args):
        self.__textMargins = QMargins(*args)
        super().setTextMargins(self.__textMargins + self.__iconMargins)

    # Common event overrides
    def actionEvent(self, event):
        super().actionEvent(event)
        if event.type() == event.Type.ActionRemoved:
            # this should take care of actions that are being deleted, even
            # indirectly (eg. their parent is being destroyed)
            action = event.action()
            if action in self.__leadingActions:
                self.__leadingActions.discard(action)
            elif action in self.__trailingActions:
                self.__trailingActions.discard(action)
            else:
                return
            self.__updateGeometries()

    def changeEvent(self, event):
        super().changeEvent(event)
        if event.type() == event.Type.LayoutDirectionChange:
            self.__updateGeometries()

    def childEvent(self, event):
        super().childEvent(event)
        if (
            event.polished() 
            and isinstance(event.child(), QToolButton)
            # the following is optional and the name may change in the future
            and event.child().metaObject().className() == 'QLineEditIconButton'
        ):
            self.__updateGeometries()

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.__updateGeometries()

А вот пример кода, который создает две строки редактирования со схожими функциями (два действия отмены/повтора и кнопка очистки), чтобы сравнить различия в макетах.

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)

    test = QWidget()
    layout = QFormLayout(test)

    undoIcon = QIcon.fromTheme('edit-undo')
    if undoIcon.isNull():
        undoIcon = app.style().standardIcon(
            QStyle.StandardPixmap.SP_ArrowLeft)
    redoIcon = QIcon.fromTheme('edit-redo')
    if redoIcon.isNull():
        redoIcon = app.style().standardIcon(
            QStyle.StandardPixmap.SP_ArrowRight)

    def makeLineEdit(cls):
        def checkUndoRedo():
            undoAction.setEnabled(widget.isUndoAvailable())
            redoAction.setEnabled(widget.isRedoAvailable())

        widget = cls()
        widget.setClearButtonEnabled(True)
        undoAction = QAction(undoIcon, 'Undo', widget, enabled=False)
        redoAction = QAction(redoIcon, 'Redo', widget, enabled=False)
        widget.addAction(redoAction, QLineEdit.ActionPosition.TrailingPosition)
        widget.addAction(undoAction, QLineEdit.ActionPosition.TrailingPosition)

        undoAction.triggered.connect(widget.undo)
        redoAction.triggered.connect(widget.redo)
        widget.textChanged.connect(checkUndoRedo)
        return widget

    layout.addRow('Standard QLineEdit:', makeLineEdit(QLineEdit))
    layout.addRow('Custom QLineEdit:', makeLineEdit(CustomLineEdit))

    test.show()
    sys.exit(app.exec())

Вот результат:

[1]: there could be some inconsistencies with QEvent.Type.ActionChanged for actionEvent() (which is received first by the private QLineEditIconButton) when the visibility of an action changes at runtime and after__updateGeometries() has been already called following the other events, which may reset the text margins. I will do further investigation, as I believe it as a symptom of a bug, other than a possible inconsistency in the behavior of the latest Qt versions.
[2]: I've not tested this on many styles, nor with complex selections or drag&drop; feel free to leave a comment about these aspects or related unexpected behavior.

Можете ли вы объяснить, почему переопределения не являются «настоящими»?

mahkitah 27.08.2024 11:25

@mahkitah Под «истинным» переопределением я подразумеваю функцию, которая теоретически может быть вызвана самим Qt (например, paintEvent()), а также явно объявлена ​​как виртуальная в API C++, а это означает, что будет вызвано переопределение Python. Я не помню какой-либо части Qt, которая напрямую вызывает addAction() (и не использует функции текстовых полей QLineEdit), но, если они есть, «переопределение» выше не будет вызвано. Так обстоит дело, например, с initStyleOption() виджетами, такими как QPushButton (в отличие от делегатов): даже если он «переопределен» в Python, он никогда не будет вызываться Qt.

musicamante 27.08.2024 18:22

Я заметил одну небольшую проблему. Если у вас включена кнопка «Очистить» и установлено направление макета RightToLeft, когда нет текста (а значит, и кнопки «Очистить»), курсор находится у правого края конечного значка (который теперь слева) без пробела. Это своего рода неестественная ситуация, когда текст по-прежнему располагается слева направо. Кроме того, для меня кнопка «Очистить» всегда является самым внутренним значком, чего не происходит в приведенном выше примере снимка экрана.

mahkitah 28.08.2024 10:03

Я также обнаружил смещение между конечными значками. Это вызвано тем, что moveRight(x), который, как я обнаружил, не перемещает правый край к x (как описано), но перемещает прямоугольник так, что right() возвращает x. А поскольку right() смещен на единицу по «историческим причинам», ход тоже невозможен.

mahkitah 28.08.2024 14:30

@mahkitah Я также заметил несоответствия с четким положением кнопки и текстовыми полями, но некоторые из них также существовали в поведении по умолчанию и основаны на первоначальных исследованиях, частично связанных с проблемой со скрытыми действиями, отмеченной выше. Внешнее положение кнопки очистки было вызвано более старой версией Qt, которую я использовал для снимка экрана. В более поздних версиях они изменились, вероятно, потому, что было нелогично перемещать оставшиеся действия после очистки текста (лично меня раздражает, что Кнопка видна только при наличии текста, и обычно я предпочитаю всегда ее показывать и »

musicamante 28.08.2024 18:26

» просто включите/выключите при необходимости). Спасибо за примечание о смещении в 1 пиксель, которое я использовал в ранней версии, но, вероятно, забыл об этом, когда пересматривал код, чтобы учесть изменение геометрии «первой» кнопки. Я проверю и исправлю это, как только закончу исследование описанных выше проблем с видимостью/позиционированием.

musicamante 28.08.2024 18:31

Другие вопросы по теме