Я планирую использовать QLineEdit
с тремя действиями, добавленными через addAction()
.
Достаточно просто и выглядит это так: (квадратики в качестве значков для примера)
Но небольшое неудобство заключается в том, что расстояние между значками кажется мне слишком большим.
Можно ли отрегулировать это расстояние? QLineEdit, похоже, не имеет доступного макета, в котором можно было бы установить интервал.
@musicamante, я как бы предполагал, что изменение размера кнопок инструментов не изменит интервал. Как вы думаете, так и будет?
Вам нужно будет вызвать setGeometry()
. Я бы вызвал базу resizeEvent()
, затем получил геометрию всех кнопок, перевел их, используя крайнюю правую в качестве ссылки, и вызвал setGeometry()
с новыми прямоугольниками.
@musicamante, мне удалось заставить его работать достаточно хорошо, вызвав setGeometry для кнопок из переопределенного события resizeEvent. Но я вижу, что вы были заняты поиском действительно правильного решения.
Да, на самом деле я понял, что этот интервал меня часто раздражал, и решил более тщательно продумать его реализацию. setGeometry()
одних может быть недостаточно, поскольку они не учитываются для эффективного расчета полей текста.
Действия, показанные в 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 Под «истинным» переопределением я подразумеваю функцию, которая теоретически может быть вызвана самим Qt (например, paintEvent()
), а также явно объявлена как виртуальная в API C++, а это означает, что будет вызвано переопределение Python. Я не помню какой-либо части Qt, которая напрямую вызывает addAction()
(и не использует функции текстовых полей QLineEdit), но, если они есть, «переопределение» выше не будет вызвано. Так обстоит дело, например, с initStyleOption()
виджетами, такими как QPushButton (в отличие от делегатов): даже если он «переопределен» в Python, он никогда не будет вызываться Qt.
Я заметил одну небольшую проблему. Если у вас включена кнопка «Очистить» и установлено направление макета RightToLeft, когда нет текста (а значит, и кнопки «Очистить»), курсор находится у правого края конечного значка (который теперь слева) без пробела. Это своего рода неестественная ситуация, когда текст по-прежнему располагается слева направо. Кроме того, для меня кнопка «Очистить» всегда является самым внутренним значком, чего не происходит в приведенном выше примере снимка экрана.
Я также обнаружил смещение между конечными значками. Это вызвано тем, что moveRight(x)
, который, как я обнаружил, не перемещает правый край к x (как описано), но перемещает прямоугольник так, что right()
возвращает x. А поскольку right()
смещен на единицу по «историческим причинам», ход тоже невозможен.
@mahkitah Я также заметил несоответствия с четким положением кнопки и текстовыми полями, но некоторые из них также существовали в поведении по умолчанию и основаны на первоначальных исследованиях, частично связанных с проблемой со скрытыми действиями, отмеченной выше. Внешнее положение кнопки очистки было вызвано более старой версией Qt, которую я использовал для снимка экрана. В более поздних версиях они изменились, вероятно, потому, что было нелогично перемещать оставшиеся действия после очистки текста (лично меня раздражает, что Кнопка видна только при наличии текста, и обычно я предпочитаю всегда ее показывать и »
» просто включите/выключите при необходимости). Спасибо за примечание о смещении в 1 пиксель, которое я использовал в ранней версии, но, вероятно, забыл об этом, когда пересматривал код, чтобы учесть изменение геометрии «первой» кнопки. Я проверю и исправлю это, как только закончу исследование описанных выше проблем с видимостью/позиционированием.
На самом деле это QToolButtons. Вы можете попробовать создать подкласс QLineEdit и обновить их геометрию на
resizeEvent()