1

I found a great resource here for building a QComboBox that gives a filtered list of suggested. It works well except for the fact that the "activated" and "currentIndexChanged" signals get emitted three times every time I select a suggested option in the combobox. The behavior is different depending on if the option is selected by mouse or using the arrow keys and the enter button.

My question is, how do I debug this? There is no point in the code to catch and prevent the first two signals from getting emitted. Is there a way to override the QComboBox "activated" signal to try and catch it in the act? Or do I have to define my own signal and use that instead?

Here's the code:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt, QSortFilterProxyModel
from PySide2.QtWidgets import QCompleter, QComboBox

class ExtendedComboBox(QComboBox):
    def __init__(self, parent=None):
        super(ExtendedComboBox, self).__init__(parent)

        self.setFocusPolicy(Qt.StrongFocus)
        self.setEditable(True)

        # add a filter model to filter matching items
        self.pFilterModel = QSortFilterProxyModel(self)
        self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.pFilterModel.setSourceModel(self.model())

        # add a completer, which uses the filter model
        self.completer = QtWidgets.QCompleter(self)
        self.completer.setModel(self.pFilterModel)

        # always show all (filtered) completions
        self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)
        self.setCompleter(self.completer)

        # connect signals
        self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString)
        self.completer.activated.connect(self.on_completer_activated)

    # on selection of an item from the completer, select the corresponding item from combobox 
    def on_completer_activated(self, text):
        if text:
            index = self.findText(text)
            self.setCurrentIndex(index)
            # self.activated.emit(self.itemText(index))


    # on model change, update the models of the filter and completer as well 
    def setModel(self, model):
        super(ExtendedComboBox, self).setModel(model)
        self.pFilterModel.setSourceModel(model)
        self.completer.setModel(self.pFilterModel)


    # on model column change, update the model column of the filter and completer as well
    def setModelColumn(self, column):
        self.completer.setCompletionColumn(column)
        self.pFilterModel.setFilterKeyColumn(column)
        super(ExtendedComboBox, self).setModelColumn(column)    

def change_option(text):
    print(text)

if __name__ == "__main__":
    import sys
    from PySide2.QtWidgets import QApplication
    from PySide2.QtCore import QStringListModel

    app = QApplication(sys.argv)

    string_list = ['hola muchachos', 'adios amigos', 'hello world', 'good bye']

    combo = ExtendedComboBox()

    # either fill the standard model of the combobox
    combo.addItems(string_list)
    combo.currentIndexChanged[str].connect(change_option)
    # or use another model
    #combo.setModel(QStringListModel(string_list))

    combo.resize(300, 40)
    combo.show()

    sys.exit(app.exec_())

You'll notice if you run the code and start typing "hello" into the text box, then click on the suggested "hello world", the activated signal returns the correct "hello world". While if you start typing "hello" but this time use the arrow keys to scroll down to "hello world", it will emit three times.

I've tried multiple implementations of this same idea all with the same result. I've even noticed similar behavior with unmodified QComboBox after swapping out the model with a new one.

PySide2 5.6.0a1 Windows 10.0.18362 Build 18362

Thanks for taking a look!

  • In Linux with PySide2 5.13.1 I do not reproduce what you indicate – eyllanesc Sep 26 '19 at 18:21
  • @eyllanesc Thanks for the hint! I'll try that tonight when I have access to a Linux machine. I'll update my post to reflect the current operating system and Pyside version. – Adam Thompson Sep 26 '19 at 19:10
  • 2
    PySide2 5.6.0a1 is an old and alpha version that must have many bugs, I recommend you install the latest version – eyllanesc Sep 26 '19 at 19:31
  • @eyllanesc That was it! I'm using that version because it's the one that anaconda provides for python 2.7. I tried an Anaconda environment with Python 3.7 and PySide2-5.13.1 and everything worked as expected. Thanks again! – Adam Thompson Sep 26 '19 at 21:06
  • Post that explanation as an answer and mark it as correct in 2 days. – eyllanesc Sep 26 '19 at 21:06

2 Answers2

2

I was using PySide2 5.6.0a1 because that's the one Anaconda installs in Python 2.7 environments. @eyllanesc pointed out this is an early and outdated version and probably buggy.

When I tried the same code in a Python 3.7 environment with PySide2-5.13.1 everything worked as expected.

1

I do not have PySide2 but for the most part I think all you have to do is replace my PyQt5 reference with PySide2 -- as that is about all I did to get you program switched from PySide2 to PyQt5 --- that along with a bit of restructuring and fine-tuning which gave me the following functioning bit of code:

from sys import exit as sysExit

from PyQt5.QtCore import Qt, QSortFilterProxyModel, QStringListModel, pyqtSlot
from PyQt5.QtWidgets import QApplication, QWidget, QCompleter, QComboBox, QCompleter, QHBoxLayout

class ExtendedComboBox(QComboBox):
    def __init__(self):
        QComboBox.__init__(self)

        self.setFocusPolicy(Qt.StrongFocus)
        self.setEditable(True)

        # add a filter model to filter matching items
        self.pFilterModel = QSortFilterProxyModel(self)
        self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.pFilterModel.setSourceModel(self.model())

        # add a completer, which uses the filter model
        self.completer = QCompleter(self)
        self.completer.setModel(self.pFilterModel)

        # always show all (filtered) completions
        self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)
        self.setCompleter(self.completer)

        # connect signals
        self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString)
        self.completer.activated.connect(self.on_completer_activated)

    # on selection of an item from the completer, select the corresponding item from combobox 
    def on_completer_activated(self, text):
        if text:
            index = self.findText(text)
            self.setCurrentIndex(index)
            # self.activated.emit(self.itemText(index))


    # on model change, update the models of the filter and completer as well 
    def setModel(self, model):
        self.setModel(model)
        self.pFilterModel.setSourceModel(model)
        self.completer.setModel(self.pFilterModel)


    # on model column change, update the model column of the filter and completer as well
    def setModelColumn(self, column):
        self.completer.setCompletionColumn(column)
        self.pFilterModel.setFilterKeyColumn(column)
        self.setModelColumn(column)    

class MainApp(QWidget):
    def __init__(self):
        QWidget.__init__(self)

        string_list = ['hola muchachos', 'adios amigos', 'hello world', 'good bye']

        self.combo = ExtendedComboBox()

        # either fill the standard model of the combobox
        self.combo.addItems(string_list)
        self.combo.currentIndexChanged[str].connect(self.change_option)
        # or use another model
        #combo.setModel(QStringListModel(string_list))

        self.resize(300, 100)
        self.combo.resize(300, 50)

        HBox = QHBoxLayout()
        HBox.addWidget(self.combo)

        self.setLayout(HBox)

    @pyqtSlot(str)
    def change_option(self, text):
        print(text)

if __name__ == "__main__":
    MainThred = QApplication([])

    MainGui = MainApp()
    MainGui.show()

    sysExit(MainThred.exec_())

I think the issue was you were trying to use Signals/Slots with a non-QObject function (aka) your change_option function is not associated directly with anything that inherits from a QObject so I am not sure what it was or was not doing but that is only a guess as all I did was put into a normal Qt structure and it worked just fine

Dennis Jensen
  • 214
  • 1
  • 14
  • Interesting. I took your code and did the minor conversions to pyside2. Just swapped out "PyQt5" with "PySide2" and "pyqtSlot" with just "Slot". I'm getting exactly the same behavior. When I type "hel" into the combobox and use the down arrow key to select "hello world" and click enter I get these three print messages: ``` hello world hola muchachos hello world ``` Maybe PySide behaves slightly differently in this case. – Adam Thompson Sep 26 '19 at 19:05
  • Oh okay so there is an issue with the autocomplete? – Dennis Jensen Sep 26 '19 at 19:10
  • Okay I just tested the PyQt5 version typed in "hel" and pressed the down arrow and it autocompletes and upon pressing enter prints "hello world" like it should so the issue might reside within something PySide2 is doing – Dennis Jensen Sep 26 '19 at 19:12
  • I was afraid of that... Thanks a lot for testing, Dennis! I'll have to find a work around. – Adam Thompson Sep 26 '19 at 19:31
  • @AdamThompson What about just using PyQt5 for the work around ;) – Dennis Jensen Sep 26 '19 at 19:52