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.
Related
I have a TreeModel which has some data loaded in it. There is a radio button in my application, which when clicked should update the TreeModel with a new data.
I have tried the following things on switching the radio button and none is working:
emit layoutChanged
emit layoutChanged();
emit dataChanged
emit dataChanged(QModelIndex(), QModelIndex());
Referenced from:
What does 'bottomRight' mean when using dataChanged() with a QTreeView in Qt?
Recursively visiting each node in the tree and emitting dataChanged
void TreeView::getLastExpandedState(const QModelIndex& parent)
{
bool isExpand = isExpanded(parent);
if (!isExpand) {
return;
}
int rows = model()->rowCount(parent);
for (int rowNum = 0; rowNum < rows ; ++rowNum) {
QModelIndex childIndex = model()->index(rowNum, 0, parent);
model->emitChange(parent,childIndex);
getLastExpandedState(childIndex);
}
}
void TreeModel::emitChange(const QModelIndex& parent,const QModelIndex& childIndex) {
emit dataChanged(parent,childIndex);
}
How to solve that?
The answer you reference is at best confusing and probably wrong.
Your model code needs to emit a dataChanged signal for any rectangle of cells where the data returned by the model data method has changed. See, for example, this question and answer, which goes into some detail.
Note that the parameters in the dataChanged signal should not be parent and child - they should be "topLeft" and "bottomRight". They need to have the same parent and also to be different. So you don't need to iterate over the rows and emit the signal for each row. You can send a combined signal for all the rows that have changed.
But you do need to send a signal for each parent (where data has changed) that signals the top left cell and bottom right cell for that parent. So you might end up creating an index for the top left cell for that parent at (0, 0, parent) and another for the bottom right cell at (rows, cols, parent) and then sending the signal for that pair of indices.
I've started to extend the qGet DownloadManager to emit the progress of a TransferItem, so that i can connect to it. I'm inserting the progress data into a cell of a TableView model for display with an Delegate, finally the delegate paints the progress bar. That works in theory, but i'm running into the following
Problem: when there are multiple downloads in parallel, then i get progress updates from both signals into both cells!
Both progress bars show progress data, but the signal is kind of mixed and not unique to the current index (QModelIndex index / index.row()).
(Please ignore the small transitioning problem between UserRoles (after clicking the download button "ActionCell" is displayed and then "Install", before the "ProgressBar" shows up.). That is not the main problem here. My question is about the index problem.) The text "112" and "113" is the int index.row.
Questions:
How to update a TableView with progress data for multiple ProgressBars?
What must i change to render a progress bar for each download?
Source
Emit progress of a download
I've added the following things to re-emit the signal through the classes, until it bubbles up to the top, where it becomes connectable from the GUI.
a connection from QNetworkReply - downloadProgress(qint64,qint64) to TransferItem - updateDownloadProgress(qint64,qint64)
void TransferItem::startRequest()
{
reply = nam.get(request);
connect(reply, SIGNAL(readyRead()), this, SLOT(readyRead()));
connect(reply, SIGNAL(downloadProgress(qint64,qint64)),
this, SLOT(updateDownloadProgress(qint64,qint64)));
connect(reply, SIGNAL(finished()), this, SLOT(finished()));
timer.start();
}
the SLOT function TransferItem - updateDownloadProgress(qint64,qint64) as receiver calculates the progress and stores it in progress (QMap<QString, QVariant>).
After the calculation the downloadProgress(this) signal is emitted.
// SLOT
void TransferItem::updateDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
progress["bytesReceived"] = QString::number(bytesReceived);
progress["bytesTotal"] = QString::number(bytesTotal);
progress["size"] = getSizeHumanReadable(outputFile->size());
progress["speed"] = QString::number((double)outputFile->size()/timer.elapsed(),'f',0).append(" KB/s");
progress["time"] = QString::number((double)timer.elapsed()/1000,'f',2).append("s");
progress["percentage"] = (bytesTotal > 0) ? QString::number(bytesReceived*100/bytesTotal).append("%") : "0 %";
emit downloadProgress(this);
}
QString TransferItem::getSizeHumanReadable(qint64 bytes)
{
float num = bytes; QStringList list;
list << "KB" << "MB" << "GB" << "TB";
QStringListIterator i(list); QString unit("bytes");
while(num >= 1024.0 && i.hasNext()) {
unit = i.next(); num /= 1024.0;
}
return QString::fromLatin1("%1 %2").arg(num, 3, 'f', 1).arg(unit);
}
When a new download is enqueued, i'm connecting the emitted downloadProgress(this) to the Slot DownloadManager - downloadProgress(TransferItem*). (dl is DownloadItem which extends TransferItem).
void DownloadManager::get(const QNetworkRequest &request)
{
DownloadItem *dl = new DownloadItem(request, nam);
transfers.append(dl);
FilesToDownloadCounter = transfers.count();
connect(dl, SIGNAL(downloadProgress(TransferItem*)),
SLOT(downloadProgress(TransferItem*)));
connect(dl, SIGNAL(downloadFinished(TransferItem*)),
SLOT(downloadFinished(TransferItem*)));
}
Finally, i'm re-emitting the download progress one more time:
void DownloadManager::downloadProgress(TransferItem *item)
{
emit signalProgress(item->progress);
}
Now the TableView with Delegate, doDownload(index) and ProgressBarUpdater
QTableView
with added QSortFilterProxyModel (for case-insensitivity)
with added ColumnDelegate, which renders DownloadButton and ProgressBar based on custom UserRoles. The delegate handles the button click: the SIGNAL downloadButtonClicked(index) is emited from the editorEvent(event, model, option, index) method.
actionDelegate = new Updater::ActionColumnItemDelegate;
ui->tableView->setItemDelegateForColumn(Columns::Action, actionDelegate);
connect(actionDelegate, SIGNAL(downloadButtonClicked(QModelIndex)), this, SLOT(doDownload(QModelIndex)));
The doDownload method receives the index and fetches the download URL from the model. Then the URL is added to the DownloadManager
and i'm setting up a ProgressBarUpdater object to set the progress data to the model at the given index. Finally i'm, connecting downloadManager::signalProgress to progressBar::updateProgress and invoke the downloadManager::checkForAllDone to start the download processing.
void UpdaterDialog::doDownload(const QModelIndex &index)
{
QUrl downloadURL = getDownloadUrl(index);
if (!validateURL(downloadURL)) return;
QNetworkRequest request(downloadURL);
downloadManager.get(request); // QueueMode is Parallel by default
ProgressBarUpdater *progressBar = new ProgressBarUpdater(this, index.row());
progressBar->setObjectName("ProgressBar_in_Row_" + QString::number(index.row()) );
connect(&downloadManager, SIGNAL(signalProgress(QMap<QString, QVariant>)),
progressBar, SLOT(updateProgress(QMap<QString, QVariant>)));
QMetaObject::invokeMethod(&downloadManager, "checkForAllDone", Qt::QueuedConnection);
}
The model update part: the ProgressBarUpdater takes the index and the progress and should update the model at the given index.
ProgressBarUpdater::ProgressBarUpdater(UpdaterDialog *parent, int currentIndexRow) :
QObject(parent), currentIndexRow(currentIndexRow)
{
model = parent->ui->tableView_1->model();
}
void ProgressBarUpdater::updateProgress(QMap<QString, QVariant> progress)
{
QModelIndex actionIndex = model->index(currentIndexRow, UpdaterDialog::Columns::Action);
// set progress to model
model->setData(actionIndex, progress, ActionColumnItemDelegate::DownloadProgressBarRole);
model->dataChanged(actionIndex, actionIndex);
}
The rendering part: i'm rendering the fake ProgressBar from the delegate; fetching the progress data with index.model()->data(index, DownloadProgressBarRole).
void ActionColumnItemDelegate::drawDownloadProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionProgressBarV2 opt;
opt.initFrom(bar);
opt.rect = option.rect;
opt.rect.adjust(3,3,-3,-3);
opt.textVisible = true;
opt.textAlignment = Qt::AlignCenter;
opt.state = QStyle::State_Enabled | QStyle::State_Active;
// get progress from model
QMap<QString, QVariant> progress =
index.model()->data(index, DownloadProgressBarRole).toMap();
QString text = QString::fromLatin1(" %1 %2 %3 %4 %5 ")
.arg(QString::number(index.row()))
.arg(progress["percentage"].toString())
.arg(progress["size"].toString())
.arg(progress["speed"].toString())
.arg(progress["time"].toString());
opt.minimum = 0;
opt.maximum = progress["bytesTotal"].toFloat();
opt.progress = progress["bytesReceived"].toFloat();
opt.text = text;
bar->style()->drawControl(QStyle::CE_ProgressBar,&opt,painter,bar);
}
I've added QString::number(index.row() to the progress bar text, so that each ProgressBar gets its row number rendered. In other words: the rendering is unique to the row, but the incoming progress data is somehow mixed.
I'm stuck on the index problem for a while now. Thank you in advance for your help.
Update: The issue is resolved!
Thank you very much ddriver!! I followed your suggestions and fixed it:
The DownloadManager tracks the progress for all transfers, and you keep each transfer item's data in the respective TransferItem.
The logical thing IMO would be to have a connection from each TransferItem to the corresponding ProgressBarUpdater, and emit from the transfer item.
However, in your case, you are reporting progress not from each individual transfer item, but from the download manager. So each time you are emitting a progress, you are emitting the progress for a particular transfer item to all progress bars.
connect(&downloadManager, SIGNAL(signalProgress(QMap<QString, QVariant>)),
progressBar, SLOT(updateProgress(QMap<QString, QVariant>)));
So instead of a
TransferItem --progress--> CorrespondingUI
you have a:
TransferItem --transferItem--> DownloadManager --progress--> AllUIs
This leads to having one single and varying progress for all progress bars, which corresponds to the last download that happen to report progress before the UI updated. Which is why you get no more variation after the first download is completed, as the manager only updates the progress for the second.
Finally, i'm re-emitting the download progress one more time:
void DownloadManager::downloadProgress(TransferItem *item)
{
emit signalProgress(item->progress);
}
And who exactly needs an anonymous progress, containing no information whatsoever to which transfer it applies? Aside from the bug of course.
Would you be so nice to explain, how to simplify it?
I was at the end of my mental rope yesterday when I commented, on a clear head it doesn't look all that overdone, but still I'd probably go for something more streamlined, involving 3 key components only:
DownloadsManager -> DownloadController -> UI
-> DownloadController -> UI
It just seems redundant to have a DownloadItem and then also a TransferItem, considering that a download is a transfer.
The model and view are totally unnecessary as well, as is storing the progress in the model rather than just having it as a member of the progress bar. You could go with just a regular widget for each download, and place them in a vertical layout.
Update:
The excessive, unnecessary compartmentalization has led to a level of fragmentation which makes it hard to get to the data, needed to make it work once you put everything together. The main issue is you have no way to tie a transfer item to the right progress bar updater, and since you still haven't posted all of the relevant code, the simplest possible solution I can offer involves the following minor changes:
// in DownloadManager
void signalProgress(QMap<QString, QVariant>); // this signal is unnecessary, remove
void DownloadManager::downloadProgress(TransferItem *item) // change this
{
registry[item->request.url()]->updateProgress(item->progress);
}
QMap<QUrl, ProgressBarUpdater *> registry; // add this
// in UpdaterDialog
void UpdaterDialog::doDownload(const QModelIndex &index)
{
QUrl downloadURL = getDownloadUrl(index);
if (!validateURL(downloadURL)) return;
QNetworkRequest request(downloadURL);
downloadManager.get(request); // QueueMode is Parallel by default
ProgressBarUpdater *progressBar = new ProgressBarUpdater(this, index.row());
progressBar->setObjectName("ProgressBar_in_Row_" + QString::number(index.row()) );
// remove the connection - source of the bug, instead register the updater
downloadManager.registry[downloadURL] = progressBar;
QMetaObject::invokeMethod(&downloadManager, "checkForAllDone", Qt::QueuedConnection);
}
That's pretty much it, the progress updater is associated with the URL, and in DownloadManager::downloadProgress instead of emitting the progress to all progress updaters, you simply lookup the one which actually corresponds to the particular download, and only update its progress. It is somewhat clumsy, but as I said, if your design is proper, it would not be needed and you wouldn't have the problem in the first place.
There are other solutions as well:
change the DownloadManager's signal to void signalProgress(TransferItem *), and the body of downloadProgress toemit signalProgress(item); , change to void ProgressBarUpdater::updateProgress(TransferItem *), and in the body compare the transfer item's request's url to the one in the model at the currentIndexRow, and only model-setData() if it is the same. This solution is not very efficient, since it will emit to all progress updaters just to modify one.
cut out the middleman, what I've been suggesting from the start, make DownloadManager ::get() return a pointer to the DownloadItem/TransferItem created in its body, then in UpdaterDialog::doDownload() you can connect the transfer item directly to the appropriate progress updater, so you will no longer need DownloadManager::downloadProgress() and the signalProgress signal, you only need to change the signature of the signal in TransferItem to void downloadProgress(QMap<QString, QVariant>); and emit the progress rather than the item. This is actually the most efficient solution, as it involves nothing extra, jsut the removal of unnecessary stuff.
I'm trying to disable users from selecting individual cells in the table widget and I only want to be able to select column and row headers, with their own separate selection behavior. Here's what I tried:
ui->tableWidget->setSelectionMode(QAbstractItemView::NoSelection);
ui->tableWidget->horizontalHeader()->setSelectionMode(QAbstractItemView::SingleSelection);
ui->tableWidget->verticalHeader()->setSelectionMode(QAbstractItemView::MultiSelection);
But it's not allowing me to select anything, and I can't find a method to set the selection behavior for only cells. Anyone?
EDIT:
I tried connecting to the sectionClicked signal of the table widgets vertical and horizontal headers, and those seem to be emitting even with the table widget's selection set to none, but they don't remain highlighted.
setSelectionMode as default is NoSelection to ignore all the selection on widget. Then connect as below code to trigger hhSelected and vhSelected slots. In those slots you just set the corresponding selectionMode and SelectionBehavior.
SO_Qt::SO_Qt(QWidget *parent, Qt::WFlags flags)
: QMainWindow(parent, flags)
{
ui.setupUi(this);
ui.tableWidget->setSelectionMode(QAbstractItemView::NoSelection);
QHeaderView* hh = ui.tableWidget->horizontalHeader();
bool success = connect(hh, SIGNAL(sectionClicked( int )), this, SLOT(hhSelected(int)));
QHeaderView* vh = ui.tableWidget->verticalHeader();
success = connect(vh, SIGNAL(sectionClicked( int )), this, SLOT(vhSelected(int)));
}
void SO_Qt::hhSelected( int index )
{
ui.tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
ui.tableWidget->setSelectionBehavior(QAbstractItemView::SelectColumns);
ui.tableWidget->selectColumn(index);
}
void SO_Qt::vhSelected( int index )
{
ui.tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
ui.tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
ui.tableWidget->selectRow(index);
}
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
I've created a child class of QTreeWidget that I want to be able to drag items from another tree widget too (I want to handle the insertion myself though), as well as from a QListWidget.
I've overridden the dropEvent method in my class, and it looks like the mimetype of the data I'm getting from my QTreeWidget is application/x-qabstractitemmodeldatalist, how can I decode this to get the items from the tree back out?
You can decode it as follows:
QByteArray encoded = qMimeData->data("application/x-qabstractitemmodeldatalist");
QDataStream stream(&encoded, QIODevice::ReadOnly);
while (!stream.atEnd())
{
int row, col;
QMap<int, QVariant> roleDataMap;
stream >> row >> col >> roleDataMap;
/* do something with the data */
}
The QMap<int, QVariant> is what is returned by QAbstractItemModel::itemData(index) for the index representing (row, col).
Looks like you can use QStandardItemModel to decode the data and get an item back out. Don't know if this is the best way to do it though:
model = QStandardItemModel()
model.dropMimeData(event.mimeData(), Qt.CopyAction, 0,0, QModelIndex())
Then you can use the item() method from model to retrieve the item and handle it however you want.
In PySide2 this works for me
def dropEvent(self, event):
if event.mimeData().hasFormat('application/x-qabstractitemmodeldatalist'):
data = event.mimeData()
source_item = QtGui.QStandardItemModel()
source_item.dropMimeData(data, QtCore.Qt.CopyAction, 0,0, QtCore.QModelIndex())
print(source_item.item(0, 0).text())