[Python] How can I delete the text of an editable ComboBox when the ComboBox receives the...

Discussão em 'Python' iniciado por Stack, Março 17, 2025.

  1. Stack

    Stack Membro Participativo

    A short introduction to clarify the context:

    I have an editable CustomCombobox (with a CustomLineEdit). This also contains a QMenu, which opens when the Enter key is pressed.

    My problem:
    I want the content/text of the CustomLineEdit to be deleted when it gets the Focus. But only if this focus comes from outside. Opening and closing the Dropdown and QMenu should be excluded from this.

    Enclosed is a more detailed code snippet. Reason:
    Some events already take place in the said CustomCombobox. Even if I am quite sure that a smaller example could suffice: Better safe than sorry.

    import sys
    from PyQt5 import QtWidgets, QtCore, QtGui

    class ComboBoxItemDelegate(QtWidgets.QStyledItemDelegate):
    #This delegate will make the Combobox Items look nice

    def paint(self, painter, option, index):
    primary = index.data(QtCore.Qt.DisplayRole) or ""
    secondary = index.data(QtCore.Qt.UserRole) or ""

    # Highlight Background if you mouseover
    painter.save()
    if option.state & QtWidgets.QStyle.State_Selected:
    painter.fillRect(option.rect, option.palette.highlight())
    else:
    painter.fillRect(option.rect, option.palette.base())

    # Make/Define the "Boxes" where primary and secondary will be written in
    rect = option.rect.adjusted(5, 0, -5, 0)
    primaryRect = QtCore.QRect(rect.left(), rect.top(), rect.width(), rect.height()//2)
    secondaryRect = QtCore.QRect(rect.left(), rect.top() + rect.height()//2, rect.width(), rect.height()//2)

    # Get font
    font = option.font if option.font else QtGui.QFont()
    primaryFont = QtGui.QFont(font.family(),font.pointSize())

    #Draw Primary
    painter.setFont(primaryFont)
    painter.setPen(option.palette.text().color())
    painter.drawText(primaryRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, primary)

    #Draw Secondary
    secondaryFont = QtGui.QFont(font.family(),font.pointSize()-1)
    painter.setFont(secondaryFont)
    painter.setPen(QtGui.QColor("gray"))
    painter.drawText(secondaryRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, secondary)
    painter.restore()


    def sizeHint(self, option, index):
    # Determine the display size of the combo box items via their font size

    font = option.widget.font() if option.widget else QtGui.QFont()
    font = QtGui.QFont(font.family(),font.pointSize())
    primaryMetrics = QtGui.QFontMetrics(font)
    primaryHeight = primaryMetrics.height()

    secondaryFont = QtGui.QFont(font)
    newSize = font.pointSize() - 2 if font.pointSize() > 2 else font.pointSize()
    secondaryFont.setPointSize(newSize)
    secondaryMetrics = QtGui.QFontMetrics(secondaryFont)
    secondaryHeight = secondaryMetrics.height()

    totalHeight = primaryHeight + secondaryHeight + 4 # zusätzlicher Abstand
    size = super().sizeHint(option, index)
    size.setHeight(totalHeight)
    return size

    class CustomLineEdit(QtWidgets.QLineEdit):
    # This CustomClass is SPECIFICALLY made for CustomCombobox
    def __init__(self, parent=None):
    super().__init__(parent)

    def focusInEvent(self, event):
    super().focusInEvent(event)
    # Deletes the content if the focus does NOT come from a/any popup (e.g. dropdown or QMenu)
    if event.reason() != QtCore.Qt.PopupFocusReason:
    self.clear()


    def paintEvent(self, event):
    # Only change the “passive widget” display/painting
    # ~(aka only if the widget and the associated popups have NO focus)~
    if self.hasFocus():
    super().paintEvent(event)
    return
    top = QtWidgets.QApplication.activePopupWidget()
    if top is not None and top.parent() == self.parent():
    super().paintEvent(event)
    return

    painter = QtGui.QPainter(self)

    #Basic Background
    opt = QtWidgets.QStyleOptionFrame()
    self.initStyleOption(opt)

    #getting parent for "what to draw" (primary/secondary) and font
    combo = self.parent()
    if isinstance(combo, QtWidgets.QComboBox):
    current_index = combo.currentIndex()
    model_index = combo.model().index(current_index, 0)
    display_text = model_index.data(QtCore.Qt.DisplayRole)
    user_text = model_index.data(QtCore.Qt.UserRole)
    base_font = combo.font()
    else:
    print('We should NEVER be here!!! Parent NOT QComboBox! Use only at your own risk!')
    display_text = self.text()
    user_text = ""
    base_font = self.font()

    # Getting "to be painted"-Area and divide them into areas
    text_rect = self.style().subElementRect(QtWidgets.QStyle.SE_LineEditContents, opt, self)
    text_rect = text_rect.adjusted(2, 0, 0, 0) #custom padding to align with dropdown items
    line_height = text_rect.height() // 2
    primary_rect = QtCore.QRect(text_rect.left(), text_rect.top(), text_rect.width(), line_height)
    secondary_rect = QtCore.QRect(text_rect.left(), text_rect.top() + line_height, text_rect.width(), line_height)

    # Draw primary (top)
    painter.setFont(base_font)
    painter.setPen(self.palette().color(QtGui.QPalette.Text))
    painter.drawText(primary_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(display_text))

    # Draw secondary (bottom)
    font = QtGui.QFont(base_font)
    font.setPointSize(font.pointSize() - 1)
    painter.setFont(font)
    painter.setPen(QtGui.QColor("gray"))
    painter.drawText(secondary_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(user_text))


    class CustomComboBox(QtWidgets.QComboBox):

    def __init__(self, parent=None):
    super().__init__(parent)
    self.setEditable(True)
    self._customLineEdit = CustomLineEdit()
    self.setLineEdit(self._customLineEdit)
    self.setCompleter(None)

    # sizeHint is needed or else the font/strings will look compressed
    def sizeHint(self):
    size = super().sizeHint()
    size.setHeight(int(size.height() * 1.6))
    return size


    class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
    super().__init__()
    self.init_ui()
    items = [{'additional_info':'111111', 'Item':'1111'},
    {'additional_info':'WORLD', 'Item':'G.I. JOE'},
    {'additional_info':'NOVABRAIN', 'Item':'FLATEARTH'},
    {'additional_info':'SUPERSTAR', 'Item':'BOB THE BUILDER'}]
    for item in items:
    self.initial_filling(item['Item'], item['additional_info'])

    def initial_filling(self, primary, secondary):
    self.searchComboBox.addItem(primary)
    index = self.searchComboBox.model().index(self.searchComboBox.count()-1, 0)
    self.searchComboBox.model().setData(index, secondary, role=QtCore.Qt.UserRole)

    def init_ui(self):
    self.setWindowTitle("My best Widget")
    self.resize(324, 500)
    central_widget = QtWidgets.QWidget()
    self.setCentralWidget(central_widget)
    layout = QtWidgets.QVBoxLayout(central_widget)

    # Creating the Combobox
    self.searchComboBox = CustomComboBox()
    self.searchComboBox.setItemDelegate(ComboBoxItemDelegate(self.searchComboBox))
    self.searchComboBox.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
    layout.addWidget(self.searchComboBox)

    #Usually triggers API. Just think of it as "make menu" function
    self.searchComboBox.lineEdit().returnPressed.connect(self.on_search_return)
    #put currentindex_item on top of the dropdownlist
    self.searchComboBox.currentIndexChanged.connect(self.change_dropdownitem_order)

    # Part 1 of 2:
    # Clear focus if an item was selected via GUI
    self.searchComboBox.activated.connect(lambda index: self.searchComboBox.clearFocus())

    # !(not relevant)! Dummy Table !(not relevant)!
    self.table = QtWidgets.QTableWidget()
    self.table.setColumnCount(3)
    self.table.setHorizontalHeaderLabels(["Some", "Nice", "Table"])
    self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
    self.table.verticalHeader().setVisible(False)
    self.table.setShowGrid(False)
    layout.addWidget(self.table)

    def on_search_return(self):
    #PREPARE INPUT FOR API CALL AND GETTING RESULT
    symbol = self.searchComboBox.currentText().strip().upper()
    if not symbol:
    return

    #.... RESULTS FROM API

    results = [{'additional_info':'HELLO', 'Item':symbol},
    {'additional_info':'WORLD', 'Item':symbol},
    {'additional_info':'GALAXY', 'Item':symbol},
    {'additional_info':'STAR', 'Item':symbol}]
    self.show_lookup_menu(results)


    def show_lookup_menu(self, results):
    #Creating QMenu based on results
    menu = QtWidgets.QMenu(self.searchComboBox)
    menu.setAttribute(QtCore.Qt.WA_DeleteOnClose)
    for result in results:
    text = f"{result['additional_info']} - {result['Item']}"
    action = QtWidgets.QAction(text, menu)
    action.setData(result)
    menu.addAction(action)
    menu.triggered.connect(self.on_menu_action_triggered)
    pos = self.searchComboBox.mapToGlobal(QtCore.QPoint(0, self.searchComboBox.height()))
    menu.popup(pos)


    def on_menu_action_triggered(self, action):
    result = action.data()
    if result:
    print("You selected:", result)
    primary = result["Item"]
    secondary = result["additional_info"]

    # The following code iterates through the combobox and checks if the chosen/selected combination
    # already exists
    found_index = -1
    for i in range(self.searchComboBox.count()):
    if self.searchComboBox.itemText(i) == primary:
    index = self.searchComboBox.model().index(i, 0)
    if self.searchComboBox.model().data(index, role=QtCore.Qt.UserRole) == secondary:
    found_index = i
    break

    if found_index != -1:
    #if it does, select it.
    self.searchComboBox.setCurrentIndex(found_index)
    else:
    #else add it then select it.
    self.searchComboBox.addItem(primary)
    new_index = self.searchComboBox.count() - 1
    index = self.searchComboBox.model().index(new_index, 0)
    self.searchComboBox.model().setData(index, secondary, role=QtCore.Qt.UserRole)
    self.searchComboBox.setCurrentIndex(new_index)

    # Part 2 of 2:
    # Clear focus if an item was "selected" via Code (setCurrentIndex)
    self.searchComboBox.clearFocus()

    def change_dropdownitem_order(self, index):
    # We assume that the just selected item is important
    # and therefore place it at the top of the combobox dropdown
    # (very volatile itemlist, will change from day to day)
    if self.searchComboBox.count() > 1:
    model = self.searchComboBox.model()
    current_index = model.index(index, 0)
    display = model.data(current_index, QtCore.Qt.DisplayRole)
    user = model.data(current_index, QtCore.Qt.UserRole)

    with QtCore.QSignalBlocker(self.searchComboBox):
    self.searchComboBox.removeItem(index)
    self.searchComboBox.insertItem(0, display, user)
    self.searchComboBox.setCurrentIndex(0)

    def main():
    qt_app = QtWidgets.QApplication(sys.argv)

    window = MainWindow()
    window.show()

    sys.exit(qt_app.exec_())

    if __name__ == "__main__":
    main()


    My current "best" solution is using the focusInEvent of the CustomLineEdit with QtCore.Qt.PopupFocusReason. However, focusInEvent ignores the focus change from all popups to the CustomWidget. Not just its own.

    Continue reading...

Compartilhe esta Página