QCompleter Custom Completion Rules - qt

I'm using Qt4.6 and I have a QComboBox with a QCompleter in it.
The usual functionality is to provide completion hints (these can be in a dropdown rather than inline - which is my usage) based on a prefix. For example, given
chicken soup
chilli peppers
grilled chicken
entering ch would match chicken soup and chilli peppers but not grilled chicken.
What I want is to be able to enter ch and match all of them or, more specifically, chicken and match chicken soup and grilled chicken.
I also want to be able to assign a tag like chs to chicken soup to produce another match which is not just on the text's content. I can handle the algorithm but,
Which of QCompleter's functions do I need to override?
I'm not really sure where I should be looking...

Based on #j3frea suggestion, here is a working example (using PySide). It appears that the model needs to be set every time splitPath is called (setting the proxy once in setModel doesn't work).
combobox.setEditable(True)
combobox.setInsertPolicy(QComboBox.NoInsert)
class CustomQCompleter(QCompleter):
def __init__(self, parent=None):
super(CustomQCompleter, self).__init__(parent)
self.local_completion_prefix = ""
self.source_model = None
def setModel(self, model):
self.source_model = model
super(CustomQCompleter, self).setModel(self.source_model)
def updateModel(self):
local_completion_prefix = self.local_completion_prefix
class InnerProxyModel(QSortFilterProxyModel):
def filterAcceptsRow(self, sourceRow, sourceParent):
index0 = self.sourceModel().index(sourceRow, 0, sourceParent)
return local_completion_prefix.lower() in self.sourceModel().data(index0).lower()
proxy_model = InnerProxyModel()
proxy_model.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(proxy_model)
def splitPath(self, path):
self.local_completion_prefix = path
self.updateModel()
return ""
completer = CustomQCompleter(combobox)
completer.setCompletionMode(QCompleter.PopupCompletion)
completer.setModel(combobox.model())
combobox.setCompleter(completer)

Building on the answer of #Bruno, I am using the standard QSortFilterProxyModel function setFilterRegExp to change the search string. In this way no sub-classing is necessary.
It also fixes a bug in #Bruno's answer, which made the suggestions vanish for some reasons once the input string got corrected with backspace while typing.
class CustomQCompleter(QtGui.QCompleter):
"""
adapted from: http://stackoverflow.com/a/7767999/2156909
"""
def __init__(self, *args):#parent=None):
super(CustomQCompleter, self).__init__(*args)
self.local_completion_prefix = ""
self.source_model = None
self.filterProxyModel = QtGui.QSortFilterProxyModel(self)
self.usingOriginalModel = False
def setModel(self, model):
self.source_model = model
self.filterProxyModel = QtGui.QSortFilterProxyModel(self)
self.filterProxyModel.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(self.filterProxyModel)
self.usingOriginalModel = True
def updateModel(self):
if not self.usingOriginalModel:
self.filterProxyModel.setSourceModel(self.source_model)
pattern = QtCore.QRegExp(self.local_completion_prefix,
QtCore.Qt.CaseInsensitive,
QtCore.QRegExp.FixedString)
self.filterProxyModel.setFilterRegExp(pattern)
def splitPath(self, path):
self.local_completion_prefix = path
self.updateModel()
if self.filterProxyModel.rowCount() == 0:
self.usingOriginalModel = False
self.filterProxyModel.setSourceModel(QtGui.QStringListModel([path]))
return [path]
return []
class AutoCompleteComboBox(QtGui.QComboBox):
def __init__(self, *args, **kwargs):
super(AutoCompleteComboBox, self).__init__(*args, **kwargs)
self.setEditable(True)
self.setInsertPolicy(self.NoInsert)
self.comp = CustomQCompleter(self)
self.comp.setCompletionMode(QtGui.QCompleter.PopupCompletion)
self.setCompleter(self.comp)#
self.setModel(["Lola", "Lila", "Cola", 'Lothian'])
def setModel(self, strList):
self.clear()
self.insertItems(0, strList)
self.comp.setModel(self.model())
def focusInEvent(self, event):
self.clearEditText()
super(AutoCompleteComboBox, self).focusInEvent(event)
def keyPressEvent(self, event):
key = event.key()
if key == 16777220:
# Enter (if event.key() == QtCore.Qt.Key_Enter) does not work
# for some reason
# make sure that the completer does not set the
# currentText of the combobox to "" when pressing enter
text = self.currentText()
self.setCompleter(None)
self.setEditText(text)
self.setCompleter(self.comp)
return super(AutoCompleteComboBox, self).keyPressEvent(event)
Update:
I figured that my previous solution worked until the string in the combobox matched none of the list items. Then the QFilterProxyModel was empty and this in turn reseted the text of the combobox. I tried to find an elegant solution to this problem, but I ran into problems (referencing deleted object errors) whenever I tried to change something on self.filterProxyModel. So now the hack is to set the model of self.filterProxyModel everytime new when its pattern is updated. And whenever the pattern does not match anything in the model anymore, to give it a new model that just contains the current text (aka path in splitPath). This might lead to performance issues if you are dealing with very large models, but for me the hack works pretty well.
Update 2:
I realized that this is still not the perfect way to go, because if a new string is typed in the combobox and the user presses enter, the combobox is cleared again. The only way to enter a new string is to select it from the drop down menu after typing.
Update 3:
Now enter works as well. I worked around the reset of the combobox text by simply taking it off charge when the user presses enter. But I put it back in, so that the completion functionality remains in place. If the user decides to do further edits.

Use filterMode : Qt::MatchFlags property. This property holds how the filtering is performed. If filterMode is set to Qt::MatchStartsWith, only those entries that start with the typed characters will be displayed. Qt::MatchContains will display the entries that contain the typed characters, and Qt::MatchEndsWith the ones that end with the typed characters. Currently, only these three modes are implemented. Setting filterMode to any other Qt::MatchFlag will issue a warning, and no action will be performed. The default mode is Qt::MatchStartsWith.
This property was introduced in Qt 5.2.
Access functions:
Qt::MatchFlags filterMode() const
void setFilterMode(Qt::MatchFlags filterMode)

Thanks Thorbjørn,
I actually did solve the problem by inheriting from QSortFilterProxyModel.
The filterAcceptsRow method must be overwritten and then you just return true or false depending on whether or not you want that item displayed.
The problem with this solution is that it only hides items in a list and so you can never rearrange them (which is what I wanted to do to give certain items priority).
[EDIT]
I thought I'd throw this into the solution since it's [basically] what I ended up doing (because the above solution wasn't adequate). I used http://www.cppblog.com/biao/archive/2009/10/31/99873.html:
#include "locationlineedit.h"
#include <QKeyEvent>
#include <QtGui/QListView>
#include <QtGui/QStringListModel>
#include <QDebug>
LocationLineEdit::LocationLineEdit(QStringList *words, QHash<QString, int> *hash, QVector<int> *bookChapterRange, int maxVisibleRows, QWidget *parent)
: QLineEdit(parent), words(**&words), hash(**&hash)
{
listView = new QListView(this);
model = new QStringListModel(this);
listView->setWindowFlags(Qt::ToolTip);
connect(this, SIGNAL(textChanged(const QString &)), this, SLOT(setCompleter(const QString &)));
connect(listView, SIGNAL(clicked(const QModelIndex &)), this, SLOT(completeText(const QModelIndex &)));
this->bookChapterRange = new QVector<int>;
this->bookChapterRange = bookChapterRange;
this->maxVisibleRows = &maxVisibleRows;
listView->setModel(model);
}
void LocationLineEdit::focusOutEvent(QFocusEvent *e)
{
listView->hide();
QLineEdit::focusOutEvent(e);
}
void LocationLineEdit::keyPressEvent(QKeyEvent *e)
{
int key = e->key();
if (!listView->isHidden())
{
int count = listView->model()->rowCount();
QModelIndex currentIndex = listView->currentIndex();
if (key == Qt::Key_Down || key == Qt::Key_Up)
{
int row = currentIndex.row();
switch(key) {
case Qt::Key_Down:
if (++row >= count)
row = 0;
break;
case Qt::Key_Up:
if (--row < 0)
row = count - 1;
break;
}
if (listView->isEnabled())
{
QModelIndex index = listView->model()->index(row, 0);
listView->setCurrentIndex(index);
}
}
else if ((Qt::Key_Enter == key || Qt::Key_Return == key || Qt::Key_Space == key) && listView->isEnabled())
{
if (currentIndex.isValid())
{
QString text = currentIndex.data().toString();
setText(text + " ");
listView->hide();
setCompleter(this->text());
}
else if (this->text().length() > 1)
{
QString text = model->stringList().at(0);
setText(text + " ");
listView->hide();
setCompleter(this->text());
}
else
{
QLineEdit::keyPressEvent(e);
}
}
else if (Qt::Key_Escape == key)
{
listView->hide();
}
else
{
listView->hide();
QLineEdit::keyPressEvent(e);
}
}
else
{
if (key == Qt::Key_Down || key == Qt::Key_Up)
{
setCompleter(this->text());
if (!listView->isHidden())
{
int row;
switch(key) {
case Qt::Key_Down:
row = 0;
break;
case Qt::Key_Up:
row = listView->model()->rowCount() - 1;
break;
}
if (listView->isEnabled())
{
QModelIndex index = listView->model()->index(row, 0);
listView->setCurrentIndex(index);
}
}
}
else
{
QLineEdit::keyPressEvent(e);
}
}
}
void LocationLineEdit::setCompleter(const QString &text)
{
if (text.isEmpty())
{
listView->hide();
return;
}
/*
This is there in the original but it seems to be bad for performance
(keeping listview hidden unnecessarily - havn't thought about it properly though)
*/
// if ((text.length() > 1) && (!listView->isHidden()))
// {
// return;
// }
model->setStringList(filteredModelFromText(text));
if (model->rowCount() == 0)
{
return;
}
int maxVisibleRows = 10;
// Position the text edit
QPoint p(0, height());
int x = mapToGlobal(p).x();
int y = mapToGlobal(p).y() + 1;
listView->move(x, y);
listView->setMinimumWidth(width());
listView->setMaximumWidth(width());
if (model->rowCount() > maxVisibleRows)
{
listView->setFixedHeight(maxVisibleRows * (listView->fontMetrics().height() + 2) + 2);
}
else
{
listView->setFixedHeight(model->rowCount() * (listView->fontMetrics().height() + 2) + 2);
}
listView->show();
}
//Basically just a slot to connect to the listView's click event
void LocationLineEdit::completeText(const QModelIndex &index)
{
QString text = index.data().toString();
setText(text);
listView->hide();
}
QStringList LocationLineEdit::filteredModelFromText(const QString &text)
{
QStringList newFilteredModel;
//do whatever you like and fill the filteredModel
return newFilteredModel;
}

Unfortunately, the answer is currently that it's not possible. To do that you'd need to duplicate much of the functionality of QCompleter in your own application (Qt Creator does that for its Locator, see src/plugins/locator/locatorwidget.cpp for the magic if you're interested).
Meanwhile you could vote on QTBUG-7830, which is about making it possible to customize the way completion items are matched, like you want. But don't hold your breath on that one.

You can get around QTBUG-7830 as mentioned above by providing custom role and making completion on that role. In the handler of that role, you can do the trick to let QCompleter know that item is there. This will work if you also override filterAcceptsRow in your SortFilterProxy model.

Easiest solution with PyQt5 :
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QCompleter
completer = QCompleter()
completer.setFilterMode(Qt.MatchContains)

This page now has been viewed over 14k times and referenced by many other posts on SO. It seems that people are creating and setting a new proxy model every time when splitPath is called, which is completely unnecessary (and expensive for large models). We just need to set the proxy model once in setModel.
As #bruno mentioned:
It appears that the model needs to be set every time splitPath is called (setting the proxy once in setModel doesn't work).
That is because if we don't invalidate the current filtering, the proxy model won't update internally. Just make sure to invalidate any current filtering or sorting on the proxy model and then you will be able to see the updates:
def splitPath(self, path):
self.local_completion_prefix = path
self.proxyModel.invalidateFilter() # invalidate the current filtering
self.proxyModel.invalidate() # or invalidate both filtering and sorting
return ""
This is available since Qt 4.3, see https://doc.qt.io/qt-5/qsortfilterproxymodel.html#invalidateFilter

Related

QTableView scrolling stopped after dynamic resizing

I have a QTableView with a custom QSortFilterProxyModel for searching and sorting and a QSqlQueryModel for populating the table.
void ProxyModel::searchTable(QString name, QString type, QString date, QString time ){
if(name_ != name)
name_ = name;
if(type_ != type)
type_ = type;
if(date_ != date)
date_ = date;
if(time_ != time)
time_ = time;
invalidateFilter();
}
bool ProxyModel::filterAcceptsRow(int source_row,
const QModelIndex &source_parent) const{
QModelIndex indName = sourceModel()->index(source_row,
0, source_parent);
QModelIndex indType= sourceModel()->index(source_row,
4, source_parent);
QModelIndex indDate = sourceModel()->index(source_row,
2, source_parent);
QModelIndex indTime = sourceModel()->index(source_row,
3, source_parent);
if(
sourceModel()->data(indName).toString().toLower().contains(name_.toLower())
&&sourceModel()->data(indType).toString().toLower().contains(type_.toLower())
&&sourceModel()->data(indDate).toString().toLower().contains(date_.toLower())
&&sourceModel()->data(indTime).toString().toLower().contains(time_.toLower())
)
{
emit adjust();
return true;
}
return false;
}
After a successful search, I emit a signal from my proxy model to a slot where it adjusts the table height to fit the size of the rows.
connect(proxyModel, SIGNAL(adjust()), this, SLOT(dataChanged()));
And when the search button is clicked
connect(ui->searchBtn, &QToolButton::clicked, this, &AllVisitedPlaces::getSearchOptions);
I call the proxy model searchTable method with search parameters
void AllVisitedPlaces::getSearchOptions()
{
proxyModel->searchTable(ui->nameLineEdit->text(),
ui->typeLineEdit->text(),
ui->dateLineEdit->text(),
ui->timeLineEdit->text());
adjustTableSize();
}
void AllVisitedPlaces::dataChanged()
{
adjustTableSize();
this->verticalHeader->setSectionResizeMode(QHeaderView::ResizeToContents);
}
void AllVisitedPlaces::adjustTableSize()
{
QRect rect = ui->table->geometry();
int height = 0;
for (int i =0; i < proxyModel->rowCount() ; i++)
height+= ui->table->rowHeight(i);
rect.setHeight(18 + ui->table->horizontalHeader()->height() + height);
ui->table->setGeometry(rect);
verticalHeader->setSectionResizeMode(QHeaderView::Stretch);
}
The problem is, when the table re-sizes , I lose scrolling.
How can I fix that ?
Before re-sizing :
After re-sizing :
Why would you expect scrolling when you've resized the table to fit its contents?
The likely problem is that your Ui is broken in other ways and the table is obscured: it has been resized, but you can't see that because whatever widget the table view sits in doesn't manage the table widget properly. But we can't tell for sure because you didn't minimize your code to provide a complete compileable example of what you're doing. We're talking about <100 lines of code in all - surely it wouldn't be a big deal to just paste such a main.cpp in your question. See e.g. example 1 or example 2.
This is a bad design anyway since you're presuming that the table will fit on screen. Yet it won't: once the resizing works, you'll end up with a vertically huge window that cannot be used as some of its corners will extend past the screen, with no way to reach them to see the contents or to resize the window.
Finally, once you properly use layouts in your Ui design, the setGeometry() call on any widget below top-level is a no-op: it's the layout that controls the child widget geometry. The solution then is not to set the widget's geometry, but to set its minimum size instead.
You're facing an XY Problem: you're dead set on a solution, without telling us what it is that you're trying to achieve and making sure first that what you're after makes sense (as in: that it will actually lead to a usable Ui!).

QTextEdit and cursor interaction

I'm modifying the Qt 5 Terminal example and use a QTextEdit window as a terminal console. I've encountered several problems.
Qt does a strange interpretation of carriage return ('\r') in incoming strings. Ocassionally, efter 3-7 sends, it interprets ('\r') as new line ('\n'), most annoying. When I finally found out I choose to filter out all '\r' from the incoming data.
Is this behaviour due to some setting?
Getting the cursor interaction to work properly is a bit problematic. I want the console to have autoscroll selectable via a checkbox. I also want it to be possible to select text whenever the console is running, without losing the selection when new data is coming.
Here is my current prinout function, that is a slot connected to a signal emitted as soon as any data has arrived:
void MainWindow::printSerialString(QString& toPrint)
{
static int cursPos=0;
//Set the cursorpos to the position from last printout
QTextCursor c = ui->textEdit_console->textCursor();
c.setPosition(cursPos);
ui->textEdit_console->setTextCursor( c );
ui->textEdit_console->insertPlainText(toPrint);
qDebug()<<"Cursor: " << ui->textEdit_console->textCursor().position();
//Save the old cursorposition, so the user doesn't change it
cursPos= ui->textEdit_console->textCursor().position();
toPrint.clear();
}
I had the problem that if the user clicked around in the console, the cursor would change position and the following incoming data would end up in the wrong place. Issues:
If a section is marked by the user, the marking would get lost when new data is coming.
When "forcing" the pointer like this, it gets a rather ugly autoscroll behaviour that isn't possible to disable.
If the cursor is changed by another part of the program between to printouts, I also have to record that somehow.
The append function which sound like a more logical solution, works fine for appending a whole complete string but displays an erratic behaviour when printing just parts of an incoming string, putting characters and new lines everywhere.
I haven't found a single setting regarding this but there should be one? Setting QTextEdit to "readOnly" doesn't disable the cursor interaction.
3.An idea is to have two cursors in the console. One invisible that is used for printouts and that is not possible at all to manipulate for the user, and one visible which enables the user to select text. But how to do that beats me :) Any related example, FAQ or guide are very appreciated.
I've done a QTextEdit based terminal for SWI-Prolog, pqConsole, with some features, like ANSI coloring sequences (subset) decoding, command history management, multiple insertion points, completion, hinting...
It runs a nonblocking user interface while serving a modal REPL (Read/Eval/Print/Loop), the most common interface for interpreted languages, like Prolog is.
The code it's complicated by the threading issues (on user request, it's possible to have multiple consoles, or multiple threads interacting on the main), but the core it's rather simple. I just keep track of the insertion point(s), and allow the cursor moving around, disabling editing when in output area.
pqConsole it's a shared object (I like such kind of code reuse), but for deployment, a stand-alone program swipl-win is more handy.
Here some selected snippets, the status variables used to control output are promptPosition and fixedPosition.
/** display different cursor where editing available
*/
void ConsoleEdit::onCursorPositionChanged() {
QTextCursor c = textCursor();
set_cursor_tip(c);
if (fixedPosition > c.position()) {
viewport()->setCursor(Qt::OpenHandCursor);
set_editable(false);
clickable_message_line(c, true);
} else {
set_editable(true);
viewport()->setCursor(Qt::IBeamCursor);
}
if (pmatched.size()) {
pmatched.format_both(c);
pmatched = ParenMatching::range();
}
ParenMatching pm(c);
if (pm)
(pmatched = pm.positions).format_both(c, pmatched.bold());
}
/** strict control on keyboard events required
*/
void ConsoleEdit::keyPressEvent(QKeyEvent *event) {
using namespace Qt;
...
bool accept = true, ret = false, down = true, editable = (cp >= fixedPosition);
QString cmd;
switch (k) {
case Key_Space:
if (!on_completion && ctrl && editable) {
compinit2(c);
return;
}
accept = editable;
break;
case Key_Tab:
if (ctrl) {
event->ignore(); // otherwise tab control get lost !
return;
}
if (!on_completion && !ctrl && editable) {
compinit(c);
return;
}
break;
case Key_Backtab:
// otherwise tab control get lost !
event->ignore();
return;
case Key_Home:
if (!ctrl && cp > fixedPosition) {
c.setPosition(fixedPosition, (event->modifiers() & SHIFT) ? c.KeepAnchor : c.MoveAnchor);
setTextCursor(c);
return;
}
case Key_End:
case Key_Left:
case Key_Right:
case Key_PageUp:
case Key_PageDown:
break;
}
you can see that most complexity goes in keyboard management...
/** \brief send text to output
*
* Decode ANSI terminal sequences, to output coloured text.
* Colours encoding are (approx) derived from swipl console.
*/
void ConsoleEdit::user_output(QString text) {
#if defined(Q_OS_WIN)
text.replace("\r\n", "\n");
#endif
QTextCursor c = textCursor();
if (status == wait_input)
c.setPosition(promptPosition);
else {
promptPosition = c.position(); // save for later
c.movePosition(QTextCursor::End);
}
auto instext = [&](QString text) {
c.insertText(text, output_text_fmt);
// Jan requested extension: put messages *above* the prompt location
if (status == wait_input) {
int ltext = text.length();
promptPosition += ltext;
fixedPosition += ltext;
ensureCursorVisible();
}
};
// filter and apply (some) ANSI sequence
int pos = text.indexOf('\x1B');
if (pos >= 0) {
int left = 0;
...
instext(text.mid(pos));
}
else
instext(text);
linkto_message_source();
}
I think you should not use a static variable (like that appearing in your code), but rely instead on QTextCursor interface and some status variable, like I do.
Generally, using a QTextEdit for a feature-rich terminal widget seems to be a bad idea. You'll need to properly handle escape sequences such as cursor movements and color mode settings, somehow stick the edit to the top-left corner of current terminal "page", etc. A better solution could be to inherit QScrollArea and implement all the needed painting–selection-scrolling features yourself.
As a temporary workaround for some of your problems I can suggest using ui->textEdit_console->append(toPrint) instead of insertPlainText(toPrint).
To automatically scroll the edit you can move the cursor to the end with QTextEdit::moveCursor() and call QTextEdit::ensureCursorVisible().

QFileSystemModel sorting DirsFirst

How do you do to sort a QFileSystemModel with QDir::DirsFirst like in QDirModel?
The QFileSystemModel does not have a setSorting method.
Maybe somebody will need this. I have implemented directories first sorting using QSortFilterProxyModel for QFileSystemModel as Kuba Ober mention in comment.
Might be not perfect yet, but still right direction.
bool MySortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
// If sorting by file names column
if (sortColumn() == 0) {
QFileSystemModel *fsm = qobject_cast<QFileSystemModel*>(sourceModel());
bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
QFileInfo leftFileInfo = fsm->fileInfo(left);
QFileInfo rightFileInfo = fsm->fileInfo(right);
// If DotAndDot move in the beginning
if (sourceModel()->data(left).toString() == "..")
return asc;
if (sourceModel()->data(right).toString() == "..")
return !asc;
// Move dirs upper
if (!leftFileInfo.isDir() && rightFileInfo.isDir()) {
return !asc;
}
if (leftFileInfo.isDir() && !rightFileInfo.isDir()) {
return asc;
}
}
return QSortFilterProxyModel::lessThan(left, right);
}
As far as I can tell, you can't (in Qt4).
The default sort order (by the "name" column), or sorting by size behaves like QDir::DirsFirst (or DirsLast if in reverse order for ), but sorting by time or type doesn't treat directories differently from ordinary files.
The QFileSystemModel doesn't expose an API for changing the sort order, and I don't see any opportunity for influencing it in the QFileSystemModel code.
(I don't see anything in the current Qt5 docs to indicate that this has changed, but those aren't final and I haven't looked very closely.)

Row, deleted from model, stays in view, what am I doing wrong?

I have QTableView, filled by QSqlRelationalTableModel. Changes should be committed or reverted on button hit.
When I edit some row, it changes state in the view when editing finishes, and succesfully commits changes to DB when submitAll() called.
But when I trying to delete row, it stays in view. Here is slot, connected to Remove button:
def _removeSelectedStatuses(self):
'''
Удаляет выбранные строки из таблицы
pre[self]: self._model is not None
'''
model = self.ConservationStatusesTableView.selectionModel()
l = model.selectedRows()
if not len(l): return
rows = set([i.row() for i in l])
rows = list(rows)
rows.sort()
first = rows[0]
count = len(rows)
self._model.removeRows(first, count)
What am I doing wrong?
I faced the same problem recently and found another solution for myself.
You may use QTableView.setRowHidden() method after QSqlTableModel.deleteRow() if you have only one QTableView connected to this model. Works fine.
(I would prefere to strike out text in custom paint delegate... but I failed to find suitable flag to distinguish non-coommited rows.)
I investigated, that this nasty behaviour is by design. Rows are deleted from model on commit, and no views know, which rows must be drawn and which aren't. Only thing done when rows removed from model is '!' marker in header.model().headerData(index, vert).text(). And it's disgusting.
I'm ashamed the way I fixed the problem, but here is my ugly hack:
from PyQt4 import QtGui
from PyQt4 import QtSql
from PyQt4 import QtCore
class SqlTableView(QtGui.QTableView):
'''
Представление, которое не показывает удалённые столбцы,
когда коммит ещё не прошёл
'''
def __init__(self, parent = None):
'''
Конструктор
'''
QtGui.QTableView.__init__(self, parent)
def setModel(self, model):
'''
Мы не можем соединиться с моделями, не являющимися QSqlTableModel
'''
assert isinstance(model, QtSql.QSqlTableModel)
QtGui.QTableView.setModel(self, model)
def paintEvent(self, event):
'''
Тут всё и происходит. Осторожно, может стошнить.
'''
if self.model() is not None:
header = self.verticalHeader()
hm = header.model()
for i in range(hm.rowCount()):
if (hm.headerData(i, QtCore.Qt.Vertical).toPyObject() == '!'
and not header.isSectionHidden(i)):
header.hideSection(i)
elif (header.isSectionHidden(i) and
hm.headerData(i, QtCore.Qt.Vertical).toPyObject() != '!'):
header.showSection(i)
PyQt4.QtGui.QTableView.paintEvent(self, event)
I also added it to QtDesigner to simplify interface design.
Second solution, not so nasty:
class PSqlRelationalTableModel : public QSqlRelationalTableModel
{
Q_OBJECT
public:
explicit PSqlRelationalTableModel(QObject *parent = 0,
QSqlDatabase db = QSqlDatabase());
virtual ~PSqlRelationalTableModel();
bool removeRows(int row, int count,
const QModelIndex &parent = QModelIndex());
public slots:
void revertRow(int row);
signals:
void rowIsMarkedForDeletion(int index);
void rowDeletionMarkRemoved(int index);
private:
QSet<unsigned int> rowsToDelete;
};
//////////////////////////////////////////////////////////////////
void PTableView::setModel(PSqlRelationalTableModel *model)
{
connect(model, SIGNAL(rowIsMarkedForDeletion(int)),
this, SLOT(onRowMarkedForDeletion(int)));
connect(model, SIGNAL(rowDeletionMarkRemoved(int)),
this, SLOT(onRowDeletionMarkRemoved(int)));
QTableView::setModel(model);
}
void PTableView::onRowMarkedForDeletion(int index)
{
QHeaderView *hv = verticalHeader();
hv->hideSection(index);
}
void PTableView::onRowDeletionMarkRemoved(int index)
{
QHeaderView *hv = verticalHeader();
hv->showSection(index);
}
Did you implement the removeRows method ?
Have a look here :
pyqt: Trying to understand insertrows for QAbstractDataModel and QTreeView
I guess what is missing is simply a emitDataChanged that tells the view that something changed ! Without that, the view cannot know if it has to refresh itself !
Hope this helps !
If you want to remove the selected row from a model you just need to call:model->removeRow(row);
Here row is the row number of which you want to delete.
This works fine for me.

QTableView and horizontalHeader()->restoreState()

I can't narrow down this bug, however I seem to have the following problem:
saveState() of a horizontalHeader()
restart app
modify model so that it has one less column
restoreState()
Now, for some reason, the state of the headerview is totally messed up. I cannot show or hide any new columns, nor can I ever get a reasonable state back
I know, this is not very descriptive but I'm hoping others have had this problem before.
For QMainWindow, the save/restoreState takes a version number. QTableView's restoreState() does not, so you need to manage this case yourself.
If you want to restore state even if the model doesn't match, you have these options:
Store the state together with a list of the columns that existed in the model upon save, so you can avoid restoring from the data if the columns don't match, and revert to defualt case
Implement your own save/restoreState functions that handle that case (ugh)
Add a proxy model that has provides bogus/dummy columns for state that is being restored, then remove those columns just afterwards.
I personally never use saveState()/restoreState() in any Qt widget, since they just return a binary blob anyway. I want my config files to be human-readable, with simple types. That also gets rid of these kind of problems.
In addition, QHeaderView has the naughty problem that restoreState() (or equivalents) only ever worked for me when the model has already been set, and then some time. I ended up connecting to the QHeaderView::sectionCountChanged() signal and setting the state in the slot called from it.
Here is the solution I made using Boost Serialization.
It handles new and removed columns, more or less. Works for my use cases.
// Because QHeaderView sucks
struct QHeaderViewState
{
explicit QHeaderViewState(ssci::CustomTreeView const & view):
m_headers(view.header()->count())
{
QHeaderView const & headers(*view.header());
// Stored in *visual index* order
for(int vi = 0; vi < headers.count();++vi)
{
int li = headers.logicalIndex(vi);
HeaderState & header = m_headers[vi];
header.hidden = headers.isSectionHidden(li);
header.size = headers.sectionSize(li);
header.logical_index = li;
header.visual_index = vi;
header.name = view.model()->headerData(li,Qt::Horizontal).toString();
header.view = &view;
}
m_sort_indicator_shown = headers.isSortIndicatorShown();
if(m_sort_indicator_shown)
{
m_sort_indicator_section = headers.sortIndicatorSection();
m_sort_order = headers.sortIndicatorOrder();
}
}
QHeaderViewState(){}
template<typename Archive>
void serialize(Archive & ar, unsigned int)
{
ar & m_headers;
ar & m_sort_indicator_shown;
if(m_sort_indicator_shown)
{
ar & m_sort_indicator_section;
ar & m_sort_order;
}
}
void
restoreState(ssci::CustomTreeView & view) const
{
QHeaderView & headers(*view.header());
const int max_columns = std::min(headers.count(),
static_cast<int>(m_headers.size()));
std::vector<HeaderState> header_state(m_headers);
std::map<QString,HeaderState *> map;
for(std::size_t ii = 0; ii < header_state.size(); ++ii)
map[header_state[ii].name] = &header_state[ii];
// First set all sections to be hidden and update logical
// indexes
for(int li = 0; li < headers.count(); ++li)
{
headers.setSectionHidden(li,true);
std::map<QString,HeaderState *>::iterator it =
map.find(view.model()->headerData(li,Qt::Horizontal).toString());
if(it != map.end())
it->second->logical_index = li;
}
// Now restore
for(int vi = 0; vi < max_columns; ++vi)
{
HeaderState const & header = header_state[vi];
const int li = header.logical_index;
SSCI_ASSERT_BUG(vi == header.visual_index);
headers.setSectionHidden(li,header.hidden);
headers.resizeSection(li,header.size);
headers.moveSection(headers.visualIndex(li),vi);
}
if(m_sort_indicator_shown)
headers.setSortIndicator(m_sort_indicator_section,
m_sort_order);
}
struct HeaderState
{
initialize<bool,false> hidden;
initialize<int,0> size;
initialize<int,0> logical_index;
initialize<int,0> visual_index;
QString name;
CustomTreeView const *view;
HeaderState():view(0){}
template<typename Archive>
void serialize(Archive & ar, unsigned int)
{
ar & hidden & size & logical_index & visual_index & name;
}
};
std::vector<HeaderState> m_headers;
bool m_sort_indicator_shown;
int m_sort_indicator_section;
Qt::SortOrder m_sort_order; // iff m_sort_indicator_shown
};
I would expect it to break if you change the model! Those functions save and restore private class member variables directly without any sanity checks. Try restoring the state and then changing the model.
I'm attempting to fix this issue for Qt 5.6.2, after hitting the same issue. See
this link for a Qt patch under review, which makes restoreState() handle the case where the number of sections (e.g. columns) in the saved state does not match the number of sections in the current view.

Resources