I have a QListWidget which has some item, and I have "Remove" button on my form which actually removes item from list. Problem is that when form load for first time and I press remove without selecting any item from list, it takes the item at 0th index by default and remove it. Following is the code segment:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->listWidget->addItem(new QListWidgetItem("Item1"));
ui->listWidget->addItem(new QListWidgetItem("Item2"));
ui->listWidget->addItem(new QListWidgetItem("Item3"));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_btnRemove_clicked()
{
printf("on_btnRemove_clicked() \n");
int currentRow = ui->listWidget->currentRow();
QModelIndex currentIndex = ui->listWidget->currentIndex();
QListWidgetItem* currentItem = ui->listWidget->currentItem();
printf("currentRow: %d\n", currentRow);
if(currentRow > -1) {
printf("currentIndex.data() %s: \n", currentIndex.data().toString().toStdString().c_str());
printf("currentItem->data(0): %s \n", currentItem->data(0).toString().toStdString().c_str());
QListWidgetItem* itemToDelete = ui->listWidget->takeItem(currentRow);
delete itemToDelete;
itemToDelete = NULL;
}
}
Any idea how to override this behavior or at least anyway that I can show that 0th index is selected with default blue background of item.
You can show the selected item after adding the items, by calling setCurrentItem:-
QListWidgetItem* pSelectedItem = new QListWidgetItem("Item1");
ui->listWidget->addItem(pSelectedItem);
ui->listWidget->addItem(new QListWidgetItem("Item2"));
ui->listWidget->addItem(new QListWidgetItem("Item3"));
ui->listWidget->setCurrentItem(pSelectedItem);
As the docs state: -
Unless the selection mode is NoSelection, the item is also be selected.
Alternatively, rather than retrieving the current Item, get the selected items with selectedItems(), which I would expect to return an empty list with which you can check the number of items: -
if(ui->listWidget->selectedItems().count())
{
// delete items
}
Note that a call to clearSelection states:-
Deselects all selected items. The current index will not be changed.
I would expect this means that requesting the current index or current item can return a valid index or item, which is why delete removes the item, even though it is not selected.
You can use the function setCurrentRow from QListWidget:
// Set the first row as current
ui->listWidget->setCurrentRow(0);
and add a blue background color with:
// Get default background color
QBrush defaultBrush = ui->listWidget->currentItem->background();
// Set background color
QBrush brush(Qt::blue);
ui->listWidget->currentItem->setBackground(brush);
to set the default color:
// Change background color with default color
ui->listWidget->currentItem->setBackground(defaultBrush);
Although it's a many-years-old question, recently I came across a similar problem and solved the problem by overriding the behavior mentioned by the op. To help people who are dealing with the same problem but are not satisfied with the accepted answer, I'll post my alternative solution here.
I found the weird behavior comes from QAbstractItemView::focusInEvent.
As shown below by the snippet from Qt source code, when a QListWidget (or whatever widget that inherits QAbstractItemView) receives a non-mouse focus event, its first item becomes its current item automatically if there isn't a current item.
// code.woboq.org/qt5/qtbase/src/widgets/itemviews/qabstractitemview.cpp.html#_ZN17QAbstractItemView12focusInEventEP11QFocusEvent
void QAbstractItemView::focusInEvent(QFocusEvent *event)
{
Q_D(QAbstractItemView);
QAbstractScrollArea::focusInEvent(event);
const QItemSelectionModel* model = selectionModel();
bool currentIndexValid = currentIndex().isValid();
if (model
&& !d->currentIndexSet
&& !currentIndexValid) {
bool autoScroll = d->autoScroll;
d->autoScroll = false;
QModelIndex index = moveCursor(MoveNext, Qt::NoModifier); // first visible index
if (index.isValid() && d->isIndexEnabled(index) && event->reason() != Qt::MouseFocusReason) {
selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
currentIndexValid = true;
}
d->autoScroll = autoScroll;
}
if (model && currentIndexValid)
setAttribute(Qt::WA_InputMethodEnabled, (currentIndex().flags() & Qt::ItemIsEditable));
else if (!currentIndexValid)
setAttribute(Qt::WA_InputMethodEnabled, false);
d->viewport->update();
}
To override this behavior, make a subclass of QListWidget and override focusInEvent() along with focusOutEvent() in the subclass.
virtual void focusInEvent(QFocusEvent *event) override {
// bypass QAbstractItemView::focusInEvent
QAbstractScrollArea::focusInEvent(event);
}
virtual void focusOutEvent(QFocusEvent *event) override {
// bypass QAbstractItemView::focusOutEvent
QAbstractScrollArea::focusOutEvent(event);
}
Related
I'm making a Qt5.7 application where I am populating a QListView after reading stuff from a file. Here's the exact code of it.
QStringListModel *model;
model = new QStringListModel(this);
model->setStringList(stringList); //stringList has a list of strings
ui->listView->setModel(model);
ui->listView->setEditTriggers(QAbstractItemView::NoEditTriggers); //To disable editing
Now this displays the list just fine in a QListView that I have set up. What I need to do now is to get the string that has been double clicked and use that value elsewhere. How do I achieve that?
What I tried doing was to attach a listener to the QListView this way
... // the rest of the code
connect(ui->listView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(fetch()));
...
And then I have the function fetch
void Window::fetch () {
qDebug() << "Something was clicked!";
QObject *s = sender();
qDebug() << s->objectName();
}
However the objectName() function returns "listView" and not the listView item or the index.
The signal already provides you with a QModelIndex which was clicked.
So you should change your slot to this:
void Window::fetch (QModelIndex index)
{
....
QModelIndex has now a column and a row property. Because a list has no columns you are interessted in the row. This is the index of the item clicked.
//get model and cast to QStringListModel
QStringListModel* listModel= qobject_cast<QStringListModel*>(ui->listView->model());
//get value at row()
QString value = listModel->stringList().at(index.row());
You should add the index as parameter of your slot. You can use that index to access the list
Your code should be some thing like this.
void Window::fetch (QModelIndex index) {
/* Do some thing you want to do*/
}
I have a QTreeView linked to a QFileSystemModel. I also have a QLineEdit with two purposes :
when the user click the QTreeView, the QLineEdit is populate with the item clicked (path & filename if applied)
when the user edit the QLineEdit and press enter, I want to :
a. synchronize the QTreeView to that position if the path exist
b. if the path do not exist, I want to repopulate the QLineEdit with the current position of the QTreeView
Everything is working well except when the condition 2a above is met, I want to :
return focus on QTreeView
expand the current folder
resize the 1st column to contents
and scroll the view to that index (scroll to top)
Here is the code I put in place :
QDirectorySelector::QDirectorySelector(QString const & title, QWidget *parent)
: QWidget(parent)
{
mDirectoryModel = new QFileSystemModel;
mDirectoryModel->setFilter(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
mDirectoryModel->setRootPath("");
mDirectoryTitle = new QLabel(title);
mDirectoryView = new QTreeView;
mDirectoryView->setModel(mDirectoryModel);
mDirectoryView->setSortingEnabled(true);
mDirectoryView->sortByColumn(0, Qt::AscendingOrder);
mDirectoryView->setSelectionMode(QAbstractItemView::ExtendedSelection);
mDirectoryEdit = new QLineEdit;
QVBoxLayout * layout = new QVBoxLayout;
layout->addWidget(mDirectoryTitle);
layout->addWidget(mDirectoryView);
layout->addWidget(mDirectoryEdit);
layout->setContentsMargins(0, 0, 0, 0);
setLayout(layout);
connect(mDirectoryView, &QTreeView::clicked, this, &QDirectorySelector::synchronizeEditFromTree);
connect(mDirectoryEdit, &QLineEdit::returnPressed, this, &QDirectorySelector::synchronizeTreeFromEdit);
connect(this, &QDirectorySelector::synchronizationDone, this, &QDirectorySelector::reactToExpansionStep1, Qt::QueuedConnection);
connect(this, &QDirectorySelector::synchronizationStep1Done, this, &QDirectorySelector::reactToExpansionStep2, Qt::QueuedConnection);
}
void QDirectorySelector::synchronizeEditFromTree(QModelIndex const & index)
{
QString directory{ mDirectoryModel->fileInfo(index).absoluteFilePath() };
mDirectoryEdit->setText(directory);
}
void QDirectorySelector::synchronizeTreeFromEdit()
{
if (QDir(mDirectoryEdit->text()).exists()) {
mDirectoryView->setFocus();
mDirectoryView->setCurrentIndex(mDirectoryModel->index(mDirectoryEdit->text()));
mDirectoryView->setExpanded(mDirectoryView->currentIndex(), true);
emit synchronizationDone(mDirectoryView->currentIndex());
} else {
selectDirectory(mDirectoryView->currentIndex());
}
}
void QDirectorySelector::reactToExpansionStep1(QModelIndex const & index)
{
mDirectoryView->resizeColumnToContents(0);
emit synchronizationStep1Done(mDirectoryView->currentIndex());
}
void QDirectorySelector::reactToExpansionStep2(QModelIndex const & index)
{
mDirectoryView->scrollTo(mDirectoryView->currentIndex(), QAbstractItemView::PositionAtTop);
}
If I type an existing path and press enter I got this unexpected behavior :
the focus is done (ok)
the current index of the view is done correctly (ok)
the expension of the sub folder is done (ok)
the resize of the 1st column is done only at the path level without including the expended part (not expected)
the scroll to is not done at all (not ok)
At this point, if I return to the QLineEdit and press enter again, everything is done correctly. In fact, I observed this pattern, if the specified folder was never "open" (seen/loaded/expended), I get the unexpected behavior. If the specified was "open" (seen/loaded/expended), I get the expected behavior.
I also try to call two times the code in the finishDirectoryEditing without any success. Everything is the same.
Is anyone have any Idea of what is happening here? And better, any suggestion to solve this unexpected behavior.
I think that the QTreeView working with the QFileSystemModel is already too awkward in term of behavior. I mean, it never hide the expansion mark of an empty folder. May be it's possible to change this but I got no success either with the fetch more strategy I found elsewhere on the net.
Thanks to all!
I have a list view with a custom model. The model allows me to add text to the bottom of the list (using 'addText(const QString&)') and to remove items from the top of the list (using 'removeItemsFromTop(int _iCount)').
What is the best way to add text to the view and keep the model size under some maximum (lets say 'MAX_LIST_SIZE'), while always maintaining the view (i.e. current selection and items in view should not change when items are removed).
The solution should preferably be a function that I can use wherever I'm using my custom model.
I have looked at indexAt(...), scrollTo(...), currentIndex(...) and setCurrentIndex(...) methods on QListView, but I can't figure out how to put all of this together.
So far I have (for auto scrolling the view)
// add items here ...
// cleanup
QModelIndex indexViewTop = listView->indexAt(QPoint(8, 8));
if (listModel->rowCount() > MAX_SIZE)
{
int iRemoveCount = (listModel->rowCount() - MAX_SIZE) + MAX_SIZE/10;
listModel->clearTextFromFront(iRemoveCount);
listView->scrollTo(indexViewTop.sibling(indexViewTop.row() - iRemoveCount, 0), QAbstractItemView::PositionAtTop);
}
This is supposed to scroll the list view as items are removed to keep the view consistent, but indexAt(...) always returns an invalid index.
For keeping the selection consistent I tried:
// add items her ...
// cleanup
if (listModel->rowCount() > MAX_SIZE)
{
int iCurrentViewIndex = listView->currentIndex().row();
int iRemoveCount = (listModel->rowCount() - MAX_SIZE) + MAX_SIZE/10;
listModel->clearTextFromFront(iRemoveCount);
listView->setCurrentIndex(listModel->index(iCurrentViewIndex - iRemoveCount, 0));
}
This seems to work, but I'm still stuck on the auto scrolling.
I did a queue-like table model implementation. I think it is similar for QAbstractItemModel. Best way would be to use QQueue to store data.
Now, this is a snipped for QAbstractTableModel (which is subclass of QAbstractItemModel so it should work; mEvents is QQueue):
// custom table for inserting events
void EventPreviewTableModel::insertEvent(const DeviceEvent &event) {
beginInsertRows(QModelIndex(), 0, 0);
mEvents.enqueue(event);
endInsertRows();
if (mEvents.size() > SIZE) {
beginRemoveRows(QModelIndex(), mEvents.size(), mEvents.size());
mEvents.dequeue();
endRemoveRows();
}
}
And also override data() and rowCount() to serve correct data.
For second part using ItemIsSelected flag for items you want to have selected is done through: Qt::ItemFlags QAbstractItemModel::flags(const QModelIndex & index)
This is my current approach and it seems to work well:
void addTitlesToList(Model *model, QListView *view, std::vector<Object*> &items)
{
QScrollBar *pVerticalScrollBar = view->verticalScrollBar();
bool bScrolledToBottom = pVerticalScrollBar->value() == pVerticalScrollBar->maximum();
QModelIndex indexViewTop = view->indexAt(QPoint(8, 8));
// add to model
model->pushItems(items);
// cleanup if model gets too big
if (model->rowCount() > model->maxListSize())
{
int iCurrentViewIndex = view->currentIndex().row();
int iRemoveCount = (int)(model->rowCount() - model->maxListSize()) + (int)model->maxListSize()/10;
model->removeItemsFromFront(iRemoveCount);
// scrolls to maintain view on items
if (bScrolledToBottom == false)
{
_pView->scrollTo(indexViewTop.sibling(indexViewTop.row() - iRemoveCount, 0), QAbstractItemView::PositionAtTop);
}
// maintain selection
if (iCurrentViewIndex >= iRemoveCount)
{
view->setCurrentIndex(_pModel->index(iCurrentViewIndex - iRemoveCount, 0));
}
else
{
view->setCurrentIndex(QModelIndex());
}
}
// move scroll bar to keep new items in view (if scrolled to the bottom)
if (bScrolledToBottom == true)
{
view->scrollToBottom();
}
}
One issue I had with indexAt(QPoint(...)) is that I was calling it after adding the items to the list, which seems to cause it to always return an invalid index. Calling indexAt before anything is added to the model seems to work.
I also added automatic 'scroll to bottom' if already there (i.e. the view either stays fixed on specific items or sticks to the latest items if scrolled all the way to the bottom).
I want to know how can I open a popup menu when I right click on the table items. In the popup menu some actions like add and delete should be given, which will create a new row or delete the selected row.
I am a new in the Qt world, so if anybody can give me the full details (with code if possible) then I will be really grateful towards him/her.
Thank you.
My goal: Only in the area of QListWidget and only if you click on an item, the menu with Delete will be opened.
Edit : Ok I solved the problem with the QListWidget and the menu. Now the following must be accomplished:
If you click on an item with the right mouse button, and then click Delete, then the item will be deleted.
My code:
void ProvideContextMenu(const QPoint &); // MainWindow.h
// In MainWindow.cpp
ui->listFiles->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->listFiles, SIGNAL(customContextMenuRequested(const QPoint &)),
this, SLOT(ProvideContextMenu(const QPoint &)));
void MainWindow::ProvideContextMenu(const QPoint &pos)
{
QPoint item = ui->listFiles->mapToGlobal(pos);
QMenu submenu;
submenu.addAction("ADD");
submenu.addAction("Delete");
QAction* rightClickItem = submenu.exec(item);
if (rightClickItem && rightClickItem->text().contains("Delete") )
{
ui->listFiles->takeItem(ui->listFiles->indexAt(pos).row());
}
}
Edit2 : Ok I solved the whole problem :D. I uploaded my code, if somebody needs something like that it can help him/her.
Firstly you need to create slot for opening context menu:
void showContextMenu(const QPoint&);
At constructor of your class, which used QListWidget, set context menu policy to custom and connect QListWidget::customContextMenuRequested(QPoint) signal and showContextMenu() slot like this:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
setupUi(this);
listWidget->setContextMenuPolicy(Qt::CustomContextMenu);
connect(listWidget, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showContextMenu(QPoint)));
}
Then need to realize context menu opening:
void MainWindow::showContextMenu(const QPoint &pos)
{
// Handle global position
QPoint globalPos = listWidget->mapToGlobal(pos);
// Create menu and insert some actions
QMenu myMenu;
myMenu.addAction("Insert", this, SLOT(addItem()));
myMenu.addAction("Erase", this, SLOT(eraseItem()));
// Show context menu at handling position
myMenu.exec(globalPos);
}
After this we need to realize slots for adding and removing QListWidget elements:
void MainWindow::eraseItem()
{
// If multiple selection is on, we need to erase all selected items
for (int i = 0; i < listWidget->selectedItems().size(); ++i) {
// Get curent item on selected row
QListWidgetItem *item = listWidget->takeItem(listWidget->currentRow());
// And remove it
delete item;
}
}
As you can see we iterate all selected items (for set multiple selection mode use setSelectionMode() method) and delete it by ourself, because docs says that
Items removed from a list widget will not be managed by Qt, and will
need to be deleted manually.
Adding some items is easier, my solution with static variable for different item caption looks like:
void MainWindow::addItem()
{
static int i = 0;
listWidget->addItem(QString::number(++i));
}
To simplify your code use Qt5 sytax for signals and slots. It eliminates the need to create intermediate slots.
I hope it helps to you.
It's much simpler than the accepted answer. You don't need to deal with creating a context menu or cursor positions or any of that. Instead of Qt::CustomContextMenu, use Qt::ActionsContextMenu and just add your actions directly to the widget:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
ui->setupUi(this);
// you can create the actions here, or in designer
auto actInsert = new QAction("Insert", this);
auto actDelete = new QAction("Delete", this);
// you can set up slot connections here or in designer
connect(actInsert, SIGNAL(triggered()), this, SLOT(addItem()));
connect(actDelete, SIGNAL(triggered()), this, SLOT(eraseItem()));
// and this will take care of everything else:
listWidget->setContextMenuPolicy(Qt::ActionsContextMenu);
listWidget->addActions({ actInsert, actDelete });
}
void MainWindow::addItem () {
...; // add an item
}
void MainWindow::eraseItem () {
...; // erase an item
}
Everything above except addActions (I think) can also be done in Designer.
Alternatively, if you don't want to add actual slot functions for whatever reason, you can do everything in a lambda on connect, too:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
ui->setupUi(this);
// you can create the actions here, or in designer
auto actInsert = new QAction("Insert", this);
auto actDelete = new QAction("Delete", this);
connect(actInsert, &QAction::triggered, [=]() {
...; // add an item
});
connect(actDelete, &QAction::triggered, [=]() {
...; // erase an item
});
// and this will take care of everything else:
listWidget->setContextMenuPolicy(Qt::ActionsContextMenu);
listWidget->addActions({ actInsert, actDelete });
}
The signal/slot option is more organized and flexible, but the lambda option is good for short highly specialized bits of code (or binding to functions that aren't slots).
This works for context menus on any widget. Also, the same QAction can be used on multiple widgets.
I wave a QListView that is backed by a QStandardItemModel. Under certain circonstances, the QStandardItem are made checkable. A checkbox gets displayed besides the item's display. At some point, I want to remove hide the QStandardItem checkbox. I set its checkable state to false but it doesn't hide the checkbox (though it cannot be checked anymore).
The only way I have found of hiding the checkbox is to replace the item with a new one. This doesn't seem the proper way to preceed.
This is the code:
MyModel::MyModel(QObject *parent):QStandardItemModel(parent){}
void MyModel::createItem(int row, const QString &text)
{
setItem(row, new QStandardItem(text));
}
void MyModel::setCheckable(int row)
{
item(row)->setCheckState(Qt::Unchecked);
item(row)->setCheckable(true); // A checkbox appears besides the text
}
void MyModel::hideCheckBox(int row)
{
item(row)->setCheckState(Qt::Unchecked);
item(row)->setCheckable(false); // does not work
// I need to completely replace the item for the checkbox to disapear.
// This doesn't seem the proper way to proceed
setItem(row, new QStandardItem(item(row)->data(Qt::DisplayRole).toString()));
}
Is there better way to proceed?
When you call setCheckState or setCheckable, the qt will update the data of list item by adding or setting a Qt::CheckStateRole data. If the Qt::CheckStateRole data is existed, the check icon will be shown. So you need remove it from the data map of the list item.
Finally, the code of hideCheckBox should be:
void MyModel::hideCheckBox(int row)
{
// check the item pointer
QStandardItem* pitem = item(row);
if (pitem == NULL) return;
// find and delete the Qt::CheckStateRole data
QMap<int, QVariant> mdata = itemData(pitem->index());
if (mdata.remove(Qt::CheckStateRole))
{
setItemData(pitem->index(), mdata);
}
}
Hope it useful. :)
I think the presence of the check boxes in items defined by item flags, so that I would write the function in the following way:
void MyModel::hideCheckBox(int row)
{
// Does not set the Qt::ItemIsUserCheckable flag.
item(row)->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
}