I would like my QTableWidget to trigger the edition callbacks when pressing Enter while editing item BUT I would like the editor to remain activated – like it would just select all of the item’s content like when you start editing the cell.
What is the best way to do this?
Thanks for having a look here.
You should modify the table's item delegate and use event filters to filter out Enter event and implement custom behavior:
class MyDelegate : public QStyledItemDelegate {
public:
MyDelegate(QObject* parent) : QStyledItemDelegate(parent) {}
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option,
const QModelIndex& index) const {
QWidget* editor = QStyledItemDelegate::createEditor(parent, option, index);
editor->installEventFilter(const_cast<MyDelegate*>(this));
return editor;
}
bool eventFilter(QObject* object, QEvent* event) {
QWidget* editor = qobject_cast<QWidget*>(object);
if (editor && event->type() == QEvent::KeyPress) {
QKeyEvent* key_event = static_cast<QKeyEvent*>(event);
if (key_event->key() == Qt::Key_Return) {
emit commitData(editor); //save changes
QLineEdit* line_edit = qobject_cast<QLineEdit*>(editor);
if (line_edit) {
line_edit->selectAll();
}
return true;
}
}
return false;
}
};
Usage:
ui->tableWidget->setItemDelegate(new MyDelegate(this));
PyQt5 code looks like this:
class GlyphCellItemDelegate(QStyledItemDelegate):
def eventFilter(self, editor, event):
if (event.type() == QEvent.KeyPress and event.key() == Qt.Key_Return):
self.commitData.emit(editor)
# Don't emit closeEditor, select contents instead
editor.selectAll()
return True
return False
Related
I'm making a program where the user can add multiple people (participants) in a list. When the "Add" button is clicked, a new row is added and "edit" is called for the name field. All is well so far, but there is a thing I'd like to implement, and I can't seem to figure out how: when the user closes the editing field (presses enter or escape, clicks elsewhere, etc.) and if the name field remains empty, I'd like the row to be deleted. In other words, a name has to be filled in. Here is what I have so far:
void MainWindow::addParticipant()
{
QList<QStandardItem *> newRow;
newRow << new QStandardItem()
<< new QStandardItem();
participantModel->appendRow(newRow);
participantView->edit(participantModel->index(participantModel->rowCount()-1, 0));
}
Here participantModel is a QStandardItemModel and participantView is a QTreeView. I tried using signals and slots to detect when a row is empty and to delete it, but it hasn't worked and the syntax is elusive to me.
Ideally I'd be able to detect when the name field is not being edited anymore, so that I can delete the row if need be.
Here is ugly but working solution: subclass from QItemDelegate and check input data inside setModelData member function. As far setModelData has a const qualifier you can not modify model inside it, so you need some trick: in the following example the model is modified inside handler of closeEditor signal.
class MainWidget : public QWidget
{
Q_OBJECT
public:
MainWidget ()
{
QStandardItemModel * model = new QStandardItemModel ();
ItemDelegate * delegate = new ItemDelegate ();
table->setItemDelegate (delegate);
connect (delegate, & ItemDelegate::closeEditor, [=](){
if (isEmpty) {
model->removeRow (emptyRow);
isEmpty = false;
emptyRow = -1;
}
});
connect (delegate, & ItemDelegate::cellEdited, [=](const int row){
isEmpty = true;
emptyRow = row;
});
}
bool isEmpty;
int emptyRow;
};
class ItemDelegate : public QItemDelegate
{
Q_OBJECT
signals:
void cellEdited (int) const;
public:
void setModelData (QWidget * widget, QAbstractItemModel * model, const QModelIndex & index) const override
{
if (0 == index.column () ) {
if (QLineEdit * cellWidget = qobject_cast <QLineEdit *> (widget) ) {
if (cellWidget->text ().isEmpty () ) {
emit cellEdited (index.row () );
return;
}
}
}
QItemDelegate::setModelData (widget, model, index);
}
};
Complete example available at GitLab.
The comments/answers posted thus far have urged me to look more into item delegates. Quite embarrassingly, after relatively little googling I found the following solution for my problem:
void MainWindow::addParticipant()
{
QStyledItemDelegate *participantDelegate = new QStyledItemDelegate;
participantView->setItemDelegateForColumn(0, participantDelegate);
QList<QStandardItem *> newRow;
newRow << new QStandardItem()
<< new QStandardItem();
participantModel->appendRow(newRow);
connect(participantDelegate, SIGNAL(closeEditor(QWidget*,QAbstractItemDelegate::EndEditHint)), this, SLOT(checkRow()));
participantView->edit(participantModel->index(participantModel->rowCount()-1, 0));
}
Apparently the closeEditor signal (only available to delegates) is exactly what I was looking for. When the editor is closed, the slot checkRow() checks if the name field of the participant is empty and decides whether or not to delete the row.
I couldn't find anything in the docs. I found one solution that uses PyQt or something, but I'd rather not have to use that. Also, it would be pretty nice if there was a signal for it, but it doesn't look like there is.
1.When you install an event filter as Sam suggested (m_lineEdit->installEventFilter(this);) you need to handle QEvent::KeyPress and check the key to be equal Qt::Key_Tab:
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (obj == m_lineEdit) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Tab)
{
//do what you need;
return true;
}
}
// pass the event on to the parent class
return QMainWindow::eventFilter(obj, event);
}
2.Another way to do that is to create a new class inherited from QLineEdit and reimplement keyPressEvent:
void LineEdit::keyPressEvent(QKeyEvent* event)
{
if (keyEvent->key() == Qt::Key_Tab)
{
emit tabPressed();
return;
}
QLineEdit::keyPressEvent(event);
}
You should be able to use QObject::installEventFilter(QObject*) to intercept the key press event. There's an example here: http://qt-project.org/doc/qt-4.8/qobject.html#installEventFilter .
I am using QMouseEvent and QKeyEvents in my program. I programmatically make various widgets (QDockWidgets and QCustomPlots).
I would like to use a single click and keypress handler function. However, I am struggling to get the widget that is actually being clicked, so that I can do certain stuff within that widget.
Is there a way to return from a ClickEvent the name of the widget that was clicked?
You can implement eventFilter in your mainWindow and listen for events from widgets there :
bool MainWindow::eventFilter(QObject * obj, QEvent * event)
{
if((myWidget *)obj == widget1 && event->type()==QEvent::KeyPress)
{
int pressedKey = ((QKeyEvent*)event)->key();
...
}
else if((myWidget *)obj == widget2 && event->type()==QEvent::MouseButtonRelease)
{
if(((QMouseEvent*)event)->button() == Qt::LeftButton)
{
...
}
}
return false;
}
Also do not forget to install event filters for your widgets in the mainWindow constructor :
widget1->installEventFilter(this);
widget2->installEventFilter(this);
I didn't manage to select the first item of the completer when the popup is displayed in PopupCompletion mode.
My current code that doesnt work:
completer->setCompletionPrefix(text);
QItemSelectionModel* sm = new QItemSelectionModel(completer->completionModel());
sm->select(completer->completionModel()->index(0,0), QItemSelectionModel::Select);
completer->popup()->setSelectionModel(sm);
Any suggestions?
I would try changing the order of the last 2 lines:
completer->popup()->setSelectionModel(sm);
sm->select(completer->completionModel()->index(0,0), QItemSelectionModel::Select);
Probably the change of the selection for the popup (its a view) ocurs when selectionChanged() is emited.
So you have to set first the selection model, then do the select.
void QItemSelectionModel::select ( const QModelIndex & index,
QItemSelectionModel::SelectionFlags command ) [virtual slot]
Selects the model item index using the specified command, and emits
selectionChanged().
BTW, u dont have to create a new selection model, just ask the popup for it (Againt, its a view):
completer->popup()->selectionModel();
http://qt-project.org/doc/qt-5.0/qtwidgets/qabstractitemview.html#selectionModel
I don't know if this is what you wanted, but in my case I wanted to be able to press enter and auto-select the first item in the pop-up list (as it does with UnfilteredPopupCompletion).
What worked for me was:
class AutoSelectFirstFilter : public QObject
{
Q_OBJECT
protected:
virtual bool eventFilter(QObject *obj, QEvent *event) override
{
if (event->type() == QEvent::KeyPress)
{
if(static_cast<QKeyEvent *>(event)->key() == Qt::Key_Return)
{
QAbstractItemView* l = static_cast<QAbstractItemView*>(obj);
QModelIndex i = l->model()->index(0,0);
if(i.isValid())
l->selectionModel()->select(i, QItemSelectionModel::Select);
}
}
return false;
}
};
and than:
AutoSelectFirstFilter tmp;
completer->popup()->installEventFilter(&tmp);
PS: Don't forget to re-run qmake.
I need to process some data AFTER a drop event has occured on a widget.
i.e a user selects some items in list A and drops them in list B. My program needs to compare the 2 lists after B has added the items selected from A.
Any ideas?
Here's a PyQt script that demonstrates two ways to trap drop events:
from PyQt4 import QtGui, QtCore
class Window(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
widget = QtGui.QWidget(self)
self.setCentralWidget(widget)
layout = QtGui.QVBoxLayout(widget)
self.listA = ListWidget(self)
self.listB = QtGui.QListWidget(self)
self.listB.viewport().installEventFilter(self)
for widget in (self.listA, self.listB):
widget.setAcceptDrops(True)
widget.setDragEnabled(True)
for item in 'One Two Three Four Five Six'.split():
widget.addItem(item)
layout.addWidget(widget)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.Drop and
source is self.listB.viewport()):
self.listB.dropEvent(event)
if event.isAccepted():
print 'eventFilter', self.listB.count()
return True
return QtGui.QMainWindow.eventFilter(self, source, event)
class ListWidget(QtGui.QListWidget):
def __init__(self, parent):
QtGui.QListWidget.__init__(self, parent)
def dropEvent(self, event):
QtGui.QListWidget.dropEvent(self, event)
if event.isAccepted():
print 'dropEvent', self.count()
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
The event you're listening for is QDropEvent. To do some work on receipt of the event you'll need to either:
Reimplement QWidget::dropEvent on the target widget or
Use a separate controller widget as an event filter for the drop target (reimplement QObject::eventFilter on the controller and then install it on the target [installEventFilter]).
EDIT: Going into more depth:
Reimplementing dropEvent
Header:
cDropTarget : public QWidget
{
Q_OBJECT
public:
cDropTarget(QWidget *Parent = 0);
protected:
virtual void dropEvent(QDropEvent *event);
}
Implementation:
cDropTarget::cDropTarget(QWidget *Parent)
: QWidget(Parent)
{
setAcceptDrops(true);
}
void cDropTarget::dropEvent(QDropEvent *event)
{
//check that you want to process the drop event
//i.e. its mimeData()->hasFormat(that you can interpret)
//extract mimeData() from the drop event
//do something with it
}
Using a separate event filter
header:
cDropEventFilter : public QObject
{
...
protected:
virtual bool eventFilter(QObject *watched, QEvent *event);
...
}
Implementation:
bool cDropEventFilter::eventFilter(QObject *watched, QEvent *event)
{
if(event->type() == QEvent::DropEnter)
{
QDropEvent *DropEvent = static_cast<QDropEvent*>(event);
if (DropEvent->mimeData()->hasFormat(MIME_TYPE))
{
DropEvent->acceptProposedAction();
return true;
}
}
else
{
return QObject::eventFilter(watched, event);
}
//Handle all events not matching mimeData format MIME_TYPE
event->ignore();
return true;
}
Got it working!
My code for anyone interested:
class MyList(QListWidget):
def __init__(self , parent = None):
super(MyList, self).__init__(parent)
self.setAlternatingRowColors(True)
self.setDragDropMode( QAbstractItemView.InternalMove )
def dropEvent( self , event ):
# get index to insert at
insertPos = event.pos()
fromList = event.source()
insertRow = fromList.row( fromList.itemAt( insertPos ) )
lowestRow = insertRow
for item in fromList.selectedItems():
name = item.text()
sip.delete( item )
listItem = QListWidgetItem( name )
listItem.setTextAlignment( Qt.AlignHCenter )
self.insertItem( insertRow , listItem )
insertRow += 1
self.emit( SIGNAL("stuffDropped") , lowestRow )
event.accept()