Search code examples
pythonpyqtpyqt5qcompleter

Multicolumn Model for QCompleter


I'm trying to work with QCompleter for autocompletion on a QComboBox. The two pictures below show the populated combobox(left) and a QCompleter on the column 1 (right).

This example works fine as long as the model columns for the QComboBox and QCompleter are the same. But it would be much more intuitive for this example for QCompleter to use column 0 (city/state), but still have QComboBox use column 1 (airport code). Here is the code:

from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtWidgets import (QApplication, QMainWindow, QComboBox,
                             QCompleter, QTableView, QLabel)
from PyQt5.QtSql import (QSqlQuery, QSqlQueryModel, QSqlDatabase)
import sys
import sqlite3

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__()

        self.setGeometry(300, 300, 400, 300)

        fid = open('example.db', 'w')
        fid.close()

        db = QSqlDatabase.addDatabase("QSQLITE")
        db.setDatabaseName('example.db')
        db.open()
        query = QSqlQuery(db)

        query.exec_('CREATE TABLE "airports" ("city" TEXT, "code" TEXT)')

        airports = [('Aberdeen, SD','ABR'), ('Abilene, TX','ABI'),
                    ('Adak Island, AK','ADK'), ('Akiachak, AK','KKI'),
                    ('Akiak, AK','AKI'), ('Akron/Canton, OH','CAK'),
                    ('Akuton, AK','KQA'), ('Alakanuk, AK','AUK'),
                    ('Alamogordo, NM','ALM'), ('Alamosa, CO','ALS'),
                    ('Albany, NY','ALB'), ('Albuquerque, NM','ABQ')]
        for item in airports:
            sql = 'INSERT INTO airports(city, code) VALUES(?, ?)'
            query.prepare(sql)
            query.addBindValue(item[0])
            query.addBindValue(item[1])
            query.exec_()

        query.exec_('SELECT * FROM airports')

        model =  QSqlQueryModel()
        model.setQuery(query)

        self.cb = QComboBox(parent = self)
        self.cb.setModel(model)
        self.cb.setModelColumn(1)
        self.cb.setView(QTableView(self.cb))
        self.cb.setGeometry(50,50, 250, 50)
        self.cb.currentIndexChanged.connect(self.indexer)

        self.label = QLabel(parent = self)
        self.label.setGeometry(20,200, 250, 50)

        self.cb.view().setMinimumWidth(500)
        self.cb.setEditable(True)

        self.completer = QCompleter()
        self.completer.setCaseSensitivity(False)
        self.cb.setCompleter(self.completer)
        self.completer.setModel(model)
        self.completer.setCompletionColumn(1)
        self.completer.setPopup(QTableView())
        self.completer.popup().setMinimumWidth(500)
        self.completer.popup().setMinimumHeight(500)

        self.completer.activated.connect(self.activatedHandler)

    @pyqtSlot(QObject)        
    def activatedHandler(self, arg):
        pass

    def indexer(self, idx):
        self.label.setText('%d' % idx)

app = QApplication(sys.argv)
main = MainWindow(None)
main.show()
sys.exit(app.exec_())

If I change the QCompleter to use column zero, i.e.

self.completer.setCompletionColumn(0)

then the correct combobox row is selected after the completer does it's thing, but the displayed text is the city/state and not the aiport code(below left and middle). Further if the user presses enter then the combobox tries to use the text to select a row, but cannot find it and resets combobox row to -1. (below right). Is there any way to change this behavior so that the desired text (airport code) is shown in the combobox?


Solution

  • By default the QCompleter uses the text of the model completionColumn to do the search and the autocomplete, so therefore there are conflicts, a possible solution is to override the pathFromIndex method and return the appropriate text

    class CustomCompleter(QCompleter):
        def pathFromIndex(self, index):
            sibling_index = index.sibling(index.row(), 1)
            return super().pathFromIndex(sibling_index)
    
    self.completer = CustomCompleter()
    

    I can't reproduce the second issue so I'll wait for OP feedback.