How to show QAbstractTableModel's status in a QStatusBar? - c++

I have my custom implementation of QAbstractTableModel and of QSortFilterProxyModel, used for filtering. The table is shown in a QTableView.
The parent dialog of my QTableView has a QStatusBar, with a read-only QLineEdit widget.
In my overriding data() method of QAbstractTableModel, I'm setting the pertinent values for the Qt::StatusTipRole role.
Now I'm missing part of the plumbing: how do I get my per-cell StatusTipRole data to show up in my widget inside QStatusBar?

There is no need to override the view widget. Qt provides built-in support to show status tips for items in a model.
Normally, You just need to return a QString from your model's data() when role is QStatusTipRole, and that QString will be shown in the status bar when you hover your item.
You also need to turn on mouse tracking for the QTableView so that you get status bar updates without the mouse button being pressed. This is because when mouse tracking is disabled (by default), the widget receives mouse move events only while a mouse button is pressed.
Now, in order to display those status tips in your QLineEdit instead of the default status bar, You can override your main window's event function, intercept QStatusTipEvents, and show tips in your QLineEdit.
Here is an example implementation:
#include <QtWidgets>
//model to provide dummy data
class MyModel : public QAbstractTableModel{
public:
explicit MyModel(QObject* parent= nullptr):QAbstractTableModel(parent){}
~MyModel() = default;
int columnCount(const QModelIndex &parent) const{
if(parent.isValid()) return 0;
return 4;
}
int rowCount(const QModelIndex &parent) const{
if(parent.isValid()) return 0;
return 20;
}
QVariant data(const QModelIndex &index, int role) const{
QVariant val;
switch(role){
case Qt::DisplayRole: case Qt::EditRole:
val= QString("Display (%1, %2)")
.arg(index.row(), 2, 10, QChar('0'))
.arg(index.column(), 2, 10, QChar('0'));
break;
case Qt::ToolTipRole:
val= QString("Tooltip (%1, %2)")
.arg(index.row(), 2, 10, QChar('0'))
.arg(index.column(), 2, 10, QChar('0'));
break;
case Qt::StatusTipRole:
val= QString("StatusTip (%1, %2)")
.arg(index.row(), 2, 10, QChar('0'))
.arg(index.column(), 2, 10, QChar('0'));
break;
}
return val;
}
};
class MainWindow : public QMainWindow{
Q_OBJECT
public:
explicit MainWindow(QWidget* parent= nullptr):QMainWindow(parent){
//set up GUI
layout.addWidget(&lineEditFilter);
layout.addWidget(&tableView);
setCentralWidget(&cw);
lineEditStatusBar.setReadOnly(true);
statusBar()->addPermanentWidget(&lineEditStatusBar);
//set up models
filterModel.setSourceModel(&model);
tableView.setModel(&filterModel);
connect(&lineEditFilter, &QLineEdit::textChanged, this, &MainWindow::updateFilter);
//turn on mouse tracking for the table view
tableView.setMouseTracking(true);
}
~MainWindow()= default;
Q_SLOT void updateFilter(const QString& text){
filterModel.setFilterFixedString(text);
}
protected:
//in order to intercept QStatusTipEvents
//and show tips in the line edit instead of the normal status bar
bool event(QEvent *event){
if(event->type() != QEvent::StatusTip) return QMainWindow::event(event);
QStatusTipEvent* statusTipEvent= static_cast<QStatusTipEvent*>(event);
lineEditStatusBar.setText(statusTipEvent->tip());
statusTipEvent->ignore();
return true;
}
private:
QWidget cw;
QVBoxLayout layout{&cw};
QLineEdit lineEditFilter;
QTableView tableView;
MyModel model;
QSortFilterProxyModel filterModel;
QLineEdit lineEditStatusBar;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow mw;
mw.show();
return a.exec();
}
#include "main.moc"

Related

How to have QTreeWidgetItems of different heights in a QTreeWidget utilizing QStyledItemDelegate?

NOTE: it turned out that the problem was not due to the implementation of QStyledItemDelegate, but it was the fact that in the constructor of MyTreeWidget I was calling setUniformRowHeights(true). The code below and the solution posted by #scopchanov are valid and working
QTreeWidget has a protected method called itemFromIndex() and this is how I am making it accessible:
class MyTreeWidget : public QTreeWidget {
Q_OBJECT
public:
MyTreeWidget(QWidget *parent) : QTreeWidget(parent) {
setItemDelegate(new MyItemDelegate(this));
}
QTreeWidgetItem treeWidgetItemFromIndex(const QModelIndex& index) {
return itemFromIndex(index);
}
}
In my QStyledItemDelegate, I am storing a pointer to MyTreeWidget and then overriding its virtual sizeHint() method and based on the type of the QTreeWidgetItem adding a padding.
class MyItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
MyItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {
_myTreeWidget = dynamic_cast<MyTreeWidget*>(parent);
}
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const {
auto treeWidgetItem = _myTreeWidget->treeWidgetItemFromIndex(index);
QSize padding;
if (dynamic_cast<MyCustomTreeWidgetItem1*>(treeWidgetItem) {
padding = {0, 5};
} else if (dynamic_cast<MyCustomTreeWidgetItem2*>(treeWidgetItem) {
padding = {0, 10};
}
return QStyledItemDelegate::sizeHint(option, index) + padding;
}
}
This doesn't work, since sizeHint() of the delegate doesn't get called for every single QTreeWidgetItem.
So my text options to call setSizeHint() in the constructor of MyCustomTreeWidgetItem1, and that didn't seem to have any effect either. Is Qt ignoring it because there is a delegate?
Another option was to set a minimum height of a QWidget that is contained in MyCustomTreeWidgetItem which is made possible via the QTreeWidget::setItemWidget().
So it looks like the moment I use the delegate, I am confined to only size. Is my option to get rid of the delegate or there's something else I can try?
I know many people would say switch from a QTreeWidget to a QTreeView, but it's not possible at the moment.
Solution
I would approach this problem in a different (simpler) manner:
Define an enumeration for the different item sizes, e.g.:
enum ItemType : int {
IT_ItemWithRegularPadding,
IT_ItemWithBigPadding
};
When creating an item, set the desired size in its user data, depending on its type, e.g.:
switch (type) {
case IT_ItemWithRegularPadding:
item->setData(0, Qt::UserRole, QSize(0, 5));
break;
case IT_ItemWithBigPadding:
item->setData(0, Qt::UserRole, QSize(0, 10));
break;
}
In the reimplementation of sizeHint retrieve the desired size from the index's data, e.g.:
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override {
return QStyledItemDelegate::sizeHint(option, index)
+ index.data(Qt::UserRole).toSize();
}
Example
Here is an example I wrote for you to demonstrate how the proposed solution could be implemented:
#include <QApplication>
#include <QStyledItemDelegate>
#include <QTreeWidget>
#include <QBoxLayout>
class Delegate : public QStyledItemDelegate
{
public:
explicit Delegate(QObject *parent = nullptr) :
QStyledItemDelegate(parent){
}
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override {
return QStyledItemDelegate::sizeHint(option, index)
+ index.data(Qt::UserRole).toSize();
}
};
class MainWindow : public QWidget
{
public:
enum ItemType : int {
IT_ItemWithRegularPadding,
IT_ItemWithBigPadding
};
MainWindow(QWidget *parent = nullptr) :
QWidget(parent) {
auto *l = new QVBoxLayout(this);
auto *treeWidget = new QTreeWidget(this);
QList<QTreeWidgetItem *> items;
for (int i = 0; i < 10; ++i)
items.append(createItem(QString("item: %1").arg(i),
0.5*i == i/2 ? IT_ItemWithRegularPadding
: IT_ItemWithBigPadding));
treeWidget->setColumnCount(1);
treeWidget->setItemDelegate(new Delegate(this));
treeWidget->insertTopLevelItems(0, items);
l->addWidget(treeWidget);
resize(300, 400);
setWindowTitle(tr("Different Sizes"));
}
private:
QTreeWidgetItem *createItem(const QString &text, int type) {
auto *item = new QTreeWidgetItem(QStringList(text));
switch (type) {
case IT_ItemWithRegularPadding:
item->setData(0, Qt::UserRole, QSize(0, 5));
break;
case IT_ItemWithBigPadding:
item->setData(0, Qt::UserRole, QSize(0, 10));
break;
}
return item;
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
Note: This example sets the item's size depending on its index - odd or even. Feel free to change this by implementing the logic you need to differentiate between the items.
Result
The given example produces the following result:
The even and odd items are of a different height.

How to override 'paint' function of custom delegate class to draw QSpinBox

I have added a custom delegator to the QTableView. When I double click on an item I see the editor widget which is a 'QSpinBox' and I am able to edit the value fine. This editor widget disappears once the focus is lost and I understand that. What I want is QSpinBox to be there all the time. Looking at the Qt example here I know I need to override the paint function of QAbstractItemDelegate class to draw the QSpinBox but I don't know how to that. In general, I want to know how any of the Qt widgets can be drawn inside a paint function.
For reference, I am having following test code:
#include <QtWidgets/QApplication>
#include <QtGui>
#include <QTableview>
#include <QLayout>
#include <QColor>
#include <QStyledItemDelegate>
#include <QSpinbox>
class SpinBoxDeligate : public QStyledItemDelegate {
public:
QWidget * createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override {
auto w = new QSpinBox(parent);
w->setFrame(false);
w->setMinimum(0);
w->setMaximum(100);
return w;
}
void setEditorData(QWidget *editor, const QModelIndex &index) const override {
static_cast<QSpinBox*>(editor)->setValue(index.data(Qt::EditRole).toInt());
}
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override {
model->setData(index, static_cast<QSpinBox*>(editor)->value(), Qt::EditRole);
}
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
// What to replace below line with to have a QSpinBox
QStyledItemDelegate::paint(painter, option, index);
}
};
int main(int argc, char** argv)
{
QApplication app(argc, argv);
QStandardItemModel model(3, 1);
for (int r = 0; r < 3; ++r)
{
auto text = QString("%0").arg(r);
QStandardItem* item = new QStandardItem(text);
item->setFlags(Qt::ItemIsUserCheckable
| Qt::ItemIsEnabled
| Qt::ItemIsEditable
);
item->setData(Qt::Unchecked, Qt::CheckStateRole);
item->setData(text, Qt::ToolTipRole);
item->setData(QSize(100, 30), Qt::SizeHintRole);
item->setData(QIcon(":/QtMVC/Desert.jpg"), Qt::DecorationRole);
model.setItem(r, 0, item);
}
QTableView* table = new QTableView();
table->setModel(&model);
table->setItemDelegate(new SpinBoxDeligate());
QWidget w;
QVBoxLayout* containerLayout = new QVBoxLayout();
w.setLayout(containerLayout);
containerLayout->addWidget(table);
w.show();
return app.exec();
}
A possible solution for your background problem is to use paint() and for that you could create a QSpinBox and use grab to take an image, but before that you should calculate the geometry so that it does not cover the QCheckBox, as you see it is a tedious job, another way is using QStyle but it is still much more code.
A simple solution is to keep the editor open with the openPersistentEditor() method.
int main(int argc, char** argv)
{
QApplication app(argc, argv);
QStandardItemModel model(3, 1);
QTableView* table = new QTableView();
table->setModel(&model);
table->setItemDelegate(new SpinBoxDeligate());
for (int r = 0; r < 3; ++r)
{
auto text = QString("%0").arg(r);
QStandardItem* item = new QStandardItem(text);
item->setFlags(Qt::ItemIsUserCheckable
| Qt::ItemIsEnabled
| Qt::ItemIsEditable
);
item->setData(Qt::Unchecked, Qt::CheckStateRole);
item->setData(text, Qt::ToolTipRole);
item->setData(QSize(100, 30), Qt::SizeHintRole);
item->setData(QIcon(":/QtMVC/Desert.jpg"), Qt::DecorationRole);
model.setItem(r, 0, item);
table->openPersistentEditor(model.indexFromItem(item));
}
QWidget w;
QVBoxLayout* containerLayout = new QVBoxLayout();
w.setLayout(containerLayout);
containerLayout->addWidget(table);
w.show();
return app.exec();
}
I can think of two possible solutions:
It is possible to insert widgets into table cells. I know this is not what you're trying to do but it can be a better solution for your problem. Check setIndexWidget.
If you really want to render QSpinBox, you should use the render method of QWidget. To try, in your paint method create a QSpinBox and call it's render method passing it the QPainter pointer. After you get it working this way, you can improve your design by possibly holding a 'template' QSpinBox instance in your QTableView and use it to render different QSpinBox values inside cells where required

How do I animate the border of a list item in a QListView?

I am using a QListView with a custom delegate that extends from QStyledItemDelegate. I reimplemented the paint method to custom paint each item in the list. In the paint method, I am drawing a border around selected items in the list view.
I want to be able to animate the item border as I select an item. For example, if the intended item border is 5 pixels, I want to have it "animate in" from 0 pixels to 5 pixels when the item is selected.
My original idea was to hook up a timer to go off every 50 milliseconds and have the delegate paint every time the timer goes off until the full border width has been painted. However, the delegate's reimplemented paint method is const, so I can't save or update a border width member variable during each pass through of the paint method.
What is the best way of accomplishing this?
A possible solution is to create a role that manages the border size of the item, and update it using a QVariantAnimation:
#include <QApplication>
#include <QListView>
#include <QPainter>
#include <QStandardItemModel>
#include <QStyledItemDelegate>
#include <QVariantAnimation>
int BorderSizeRole = Qt::UserRole+1;
class AnimationDelegate: public QStyledItemDelegate{
public:
using QStyledItemDelegate::QStyledItemDelegate;
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const{
QStyledItemDelegate::paint(painter, option, index);
bool ok;
int borderSize = index.data(BorderSizeRole).toInt(&ok);
if(borderSize >0 && ok){
painter->save();
QPen pen(QBrush(Qt::red), borderSize);
painter->setPen(pen);
painter->drawRect(option.rect);
painter->restore();
}
}
};
class CustomAnimation: public QVariantAnimation{
QPersistentModelIndex m_index;
QAbstractItemModel *m_model;
public:
CustomAnimation(QAbstractItemModel *m_model, QPersistentModelIndex index, QObject *parent=nullptr)
: QVariantAnimation(parent),
m_index(index),
m_model(m_model)
{
setStartValue(0);
setEndValue(5);
setDuration(50*5);
connect(this, &CustomAnimation::valueChanged, this, &CustomAnimation::on_valueChanged);
// delete animation
start(QAbstractAnimation::DeleteWhenStopped);
}
private:
Q_SLOT void on_valueChanged(const QVariant & value){
if(m_model)
m_model->setData(m_index, value, BorderSizeRole);
else
stop();
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QListView view;
view.setItemDelegate(new AnimationDelegate(&view));
QStandardItemModel model;
for(int i=0; i<10; i++){
QStandardItem *item = new QStandardItem(QString("item %1").arg(i));
item->setData(-1, BorderSizeRole);
model.appendRow(item);
}
view.setModel(&model);
QObject::connect(view.selectionModel(), &QItemSelectionModel::selectionChanged,
[&model](const QItemSelection &selected, const QItemSelection & deselected){
for(const QModelIndex & index: selected.indexes()){
new CustomAnimation(&model, QPersistentModelIndex(index));
}
// remove border
for(const QModelIndex & index: deselected.indexes()){
model.setData(index, -1, BorderSizeRole);
}
});
view.show();
return a.exec();
}

Qt::Popup breaks QScroller kinetic scroll?

First of all my problem is reproducible only on device with touch screen,
with PC/mouse all works fine.
The problem that if I use QTableView + QScroller as standalone window all works fine - I move finger from bottom to top content scrolls down, from top to bottom scrolls up.
But if I put QTableView inside QWidget with Qt::Popup attribute, then scrolling changes direction! I move finger from bottom to top and it scrolls up, from top to bottom scrolls down.
Here is my code:
#include <QAbstractTableModel>
#include <QScroller>
#include <QTouchDevice>
#include <QVBoxLayout>
#include <QtDebug>
#include <QtWidgets/QApplication>
#include <QtWidgets/QTableView>
class MyModel : public QAbstractTableModel {
public:
MyModel(QObject *parent) : QAbstractTableModel(parent) {}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return 100;
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
return 3;
}
QVariant data(const QModelIndex &index,
int role = Qt::DisplayRole) const override {
if (role == Qt::DisplayRole) {
return QString("Row%1, Column%2")
.arg(index.row() + 1)
.arg(index.column() + 1);
}
return QVariant();
}
QVariant headerData(int section, Qt::Orientation orientation,
int role) const override {
if (role == Qt::DisplayRole) {
if (orientation == Qt::Horizontal) {
switch (section) {
case 0:
return QString("first");
case 1:
return QString("second");
case 2:
return QString("third");
}
}
}
return QVariant();
}
};
bool is_touch_screen_avaible() {
const auto devs = QTouchDevice::devices();
for (const auto &dev : devs) {
if (dev->type() == QTouchDevice::TouchScreen) {
return true;
}
}
return false;
}
void configure_scoller_for_item_view(QAbstractItemView *view) {
QScroller *scroller = QScroller::scroller(view);
view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
view->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
QScrollerProperties properties =
QScroller::scroller(scroller)->scrollerProperties();
QVariant overshootPolicy =
QVariant::fromValue<QScrollerProperties::OvershootPolicy>(
QScrollerProperties::OvershootAlwaysOff);
properties.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy,
overshootPolicy);
scroller->setScrollerProperties(properties);
properties.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy,
overshootPolicy);
scroller->setScrollerProperties(properties);
if (is_touch_screen_avaible())
scroller->grabGesture(view, QScroller::TouchGesture);
else
scroller->grabGesture(view, QScroller::LeftMouseButtonGesture);
}
#define POPUP
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
#ifdef POPUP
QWidget *mainWin = new QWidget;
mainWin->setWindowFlags(Qt::Popup);
auto lay = new QVBoxLayout(mainWin);
mainWin->setLayout(lay);
auto tableView = new QTableView(mainWin);
lay->addWidget(tableView);
#else
auto tableView = new QTableView;
#endif
MyModel myModel(nullptr);
tableView->setModel(&myModel);
tableView->setSelectionMode(QAbstractItemView::NoSelection);
tableView->setFocusPolicy(Qt::NoFocus);
configure_scoller_for_item_view(tableView);
#ifdef POPUP
mainWin->resize(500, 500);
mainWin->show();
#else
tableView->resize(500, 500);
tableView->show();
#endif
return a.exec();
}
Qt doesn't fully implement gestures in scrollable areas as explained in their own documentation:
Qt does not really reflect the system behavior wrt gestures on scrollable views (widget classes inheriting QAbstractItemView, QML classes) well.
[...]
In widgets, the pan recognizer is currently hard-coded to use 2 touch points. For touchscreens, it should be changed to one. But that can't be done as long as single-finger-panning is reserved for selecting text.
When using a touch screen, the selection in widgets is driven by mouse events synthesized from touch by the system (Windows) or Qt itself (other platforms). The same touch events drive QGestureManager.
On the other hand, there is a known (and old) undefined behavior with QTouchEvents and pop-up widgets:
The behavior of QTouchEvents is undefined when opening a pop-up or grabbing the mouse while there are more than one active touch points.
Probably the combination of both issues is the origin of your problem.
As a possible workaround (although not completely what you want), you can enable the two-fingers scroll with QWidget::grabGesture(Qt::PanGesture) as an alternative. Also, as mentioned by #mohammad-kanan in a comment, you may try with Qt::FramelessWindowHint | Qt::Tool instead of Qt::Popup.

checkbox on QtableWidget's header

How to set checkbox on QTableWidget Header. How To Add Select All Checkbox in QHeaderView..
It does not displays a checkbox..
QTableWidget* table = new QTableWidget();
QTableWidgetItem *pItem = new QTableWidgetItem("All");
pItem->setCheckState(Qt::Unchecked);
table->setHorizontalHeaderItem(0, pItem);
Here, at Qt Wiki, it says there is no shortcut for it and you have to subclass headerView yourself.
Here is a summary of that wiki answer:
"Currently there is no API to insert widgets in the header, but you can paint the checkbox yourself in order to insert it into the header.
What you could do is to subclass QHeaderView, reimplement paintSection() and then call drawPrimitive() with PE_IndicatorCheckBox in the section where you want to have this checkbox.
You would also need to reimplement the mousePressEvent() to detect when the checkbox is clicked, in order to paint the checked and unchecked states.
The example below illustrates how this can be done:
#include <QtGui>
class MyHeader : public QHeaderView
{
public:
MyHeader(Qt::Orientation orientation, QWidget * parent = 0) : QHeaderView(orientation, parent)
{}
protected:
void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const
{
painter->save();
QHeaderView::paintSection(painter, rect, logicalIndex);
painter->restore();
if (logicalIndex == 0)
{
QStyleOptionButton option;
option.rect = QRect(10,10,10,10);
if (isOn)
option.state = QStyle::State_On;
else
option.state = QStyle::State_Off;
this->style()->drawPrimitive(QStyle::PE_IndicatorCheckBox, &option, painter);
}
}
void mousePressEvent(QMouseEvent *event)
{
if (isOn)
isOn = false;
else
isOn = true;
this->update();
QHeaderView::mousePressEvent(event);
}
private:
bool isOn;
};
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QTableWidget table;
table.setRowCount(4);
table.setColumnCount(3);
MyHeader *myHeader = new MyHeader(Qt::Horizontal, &table);
table.setHorizontalHeader(myHeader);
table.show();
return app.exec();
}
Instead of above solution, You can simply put Push button in place of select all check box and give a name push button to "select all".
So If you press select all button then it called push button and go to whenever you go with push button(Here select all).