最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

python - How can I delete the text of an editable ComboBox when the ComboBox receives the focus from outside, and ignore focus c

programmeradmin2浏览0评论

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

SECONDARY_FONTSIZE_MODIFIER = -1
SECONDARY_FONT = QtGui.QColor("gray")

def getfontheight_adjustfontsize(font, offset=0):
    if isinstance(font, QtGui.QFont) and isinstance(offset, int):
        if offset != 0:
            newsize = font.pointSize() + offset if font.pointSize() > -offset else font.pointSize()
            font.setPointSize(newsize)
        fontheight = QtGui.QFontMetrics(font).height()
        return fontheight

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
        if option.state & QtWidgets.QStyle.State_Selected:
            painter.fillRect(option.rect, option.palette.highlight())
        else:
            painter.fillRect(option.rect, option.palette.base())
        
        primaryFont = option.font 
        secondaryFont = QtGui.QFont(option.font)

        primaryHeight = getfontheight_adjustfontsize(primaryFont)
        secondaryHeight = getfontheight_adjustfontsize(secondaryFont, SECONDARY_FONTSIZE_MODIFIER)

        totalTextHeight = primaryHeight + secondaryHeight
        rect = option.rect.adjusted(5, 0, -5, 0) 
        y_offset = rect.top() + (rect.height() - totalTextHeight) // 2

        primaryRect = QtCore.QRect(rect.left(), y_offset, rect.width(), primaryHeight)
        secondaryRect = QtCore.QRect(rect.left(), y_offset + primaryHeight, rect.width(), secondaryHeight)
        
        #Draw Primary       
        painter.save()
        painter.setFont(primaryFont)
        painter.setPen(option.palette.text().color())
        painter.drawText(primaryRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, primary)
        
        #Draw Secondary
        painter.setFont(secondaryFont)
        painter.setPen(SECONDARY_FONT)
        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
        primaryFont = option.font
        secondaryFont = QtGui.QFont(option.font)
 
        primaryHeight = getfontheight_adjustfontsize(primaryFont)
        secondaryHeight = getfontheight_adjustfontsize(secondaryFont, SECONDARY_FONTSIZE_MODIFIER)

        totalHeight = primaryHeight + secondaryHeight + 2  # additional spacing
        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)
            primaryFont = combo.font()
            secondaryFont = combo.font()
        else:
            print('We should NEVER be here!!! Parent NOT QComboBox! Use only at your own risk!')
            display_text = self.text()
            user_text = ""
            primaryFont = self.font()
            secondaryFont = 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

        primaryHeight = getfontheight_adjustfontsize(primaryFont)
        secondaryHeight = getfontheight_adjustfontsize(secondaryFont, SECONDARY_FONTSIZE_MODIFIER)

        totalTextHeight = primaryHeight + secondaryHeight
        y_offset = text_rect.top() + (text_rect.height() - totalTextHeight) // 2

        primaryRect = QtCore.QRect(text_rect.left(), y_offset, text_rect.width(), primaryHeight)
        secondaryRect = QtCore.QRect(text_rect.left(), y_offset + primaryHeight, text_rect.width(), secondaryHeight)
        
        # Draw primary (top)
        painter.setFont(primaryFont)
        painter.setPen(self.palette().color(QtGui.QPalette.Text))
        painter.drawText(primaryRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(display_text))

        # Draw secondary (bottom)
        painter.setFont(secondaryFont)
        painter.setPen(SECONDARY_FONT)
        painter.drawText(secondaryRect, 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.setLineEdit(CustomLineEdit(self))
        self.setCompleter(None)
    
    # sizeHint is needed or else the font/strings will look compressed
    def sizeHint(self):
        primaryFont = self.font()
        secondaryFont = self.font()

        primaryHeight = getfontheight_adjustfontsize(primaryFont)
        secondaryHeight = getfontheight_adjustfontsize(secondaryFont, SECONDARY_FONTSIZE_MODIFIER)

        totalHeight = primaryHeight + secondaryHeight + 4  # extra spacing
        size = super().sizeHint()
        size.setHeight(totalHeight)
        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.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论