QStyledItemDelegate inside a QTableView?
-
Hi all -
I'm using a QTableView to display the contents of a model. I need each entry in the table to be able to display a QWidget (I have to paint these widgets to update them). Is there a straightforward way to do this, or am I using the wrong approach?
Thanks...
-
@mzimmers
Normally you are supposed to use aQStyledItemDelegate
to "draw" what you want, as usingQWidget
s inQTableView
s is resource-intensive and to be avoided.
If you do want aQWidget
there I would look to see how it is done in QTableWidget::setCellWidget(). -
@JonB thanks. I'm going to re-title this thread accordingly, as it seems that QStyledItemDelegate is what I want. A couple questions:
- from the doc: "QStyledItemDelegate is the default delegate for all Qt item views, and is installed upon them when they are created." Does this mean that my QTableView already has an actual QStyledItemDelegate object, or do I still need to create one?
- do I then use QStyledItemDelegate::paint() to do my painting? If so, how do I direct the delegate to a particular row/column in the view?
- should I be using QTableWidget instead of QTableView for this application?
Thanks...
-
You can set a delegate for one column or one row if you want.
Otherwise, the usual, check the index and do your custom drawing if it matches the conditions.
-
So, here's the signature of my delegate's paint() method:
void TimeDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
- how do I derive my model from the index? The model will dictate what I draw.
- can I position my paint object (in this case, a rectangle) within a cell in the view? Currently, it shows up like this:
I want it in the Widget column. How do I designate which row/column the paint takes place?
Thanks...
EDIT: to make these questions a bit more concrete, let's say I wanted to check a different column in my current row. If the value in that column is odd, I'd make my rectangle green; if even, red.
-
@mzimmers said in QStyledItemDelegate inside a QTableView?:
how do I derive my model from the index? The model will dictate what I draw.
What do you mean by this?
index
is an index into the model being drawn. const QAbstractItemModel *QModelIndex::model() const gives you the model, i.e. fromindex.model()
in thepaint()
event. Andindex.data()
gives you the data value from the model.This
paint()
method is called for every cell to draw, one at a time, as necessary. You look atindex.row
/column()
if that is relevant to your painting. If you want to do something special in certain rows/columns use anif
/switch
statement. Just call the base classpaint()
for the default painting. So you do not "position the object into/designate which row/column the paint takes place", you wait till it's called withindex.column() == 2
for your Widget column.If you used QAbstractItemView::setItemDelegate(QAbstractItemDelegate *delegate) to set your delegate on the tableview it is set/will be called for every cell drawn. However, as a convenience to save you having to use an
if
/switch
for the Widget column, you could instead use QAbstractItemView::setItemDelegateForColumn(int column, QAbstractItemDelegate *delegate) to set a delegate for an individual column. -
So, I guess the correct way to get a new index is something like this:
void TimeDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QModelIndex qmi; if (index.column() == COLUMN_WIDGET) { qmi = index.model()->index(index.row(), COLUMN_CURRENTTIME); QString qs = qmi.data().toString(); etc...
And then process qs as necessary to influence how I do my painting.
And, thanks for the reference to setItemDelegateForColumn(). That is indeed very convenient.
-
@mzimmers said in QStyledItemDelegate inside a QTableView?:
And then process qs as necessary to influence how I do my painting.
why? You already have the correct index - it's the one which is passed to your paint function... why would you paint something from another cell in the one you have to paint?
-
@Christian-Ehrlicher I need the contents of another cell in order to know what to paint. The other cell in question contains a QString with digits; I need to know those digits in order to know what to paint. (I'm sort of doing a manual implementation of the Qt LCD class.)
EDIT: this brings me to another issue, though -- the delegate can access the data from within its paint() method, but there doesn't appear to be a way to pass that to the object that actually does the painting. I'm using the StarDelegate example, and the delegate looks like this:
void TimeDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QModelIndex qmi;// = new QModelIndex(m_model->index(index.row(), COLUMN_CURRENTTIME)); if (index.column() == COLUMN_WIDGET) { // qmi = index.model()->index(index.row(), COLUMN_CURRENTTIME); qmi = index.siblingAtColumn(COLUMN_CURRENTTIME); QString qs = qmi.data().toString(); StarRating starRating = qvariant_cast<StarRating>(index.data()); starRating.paint(painter, option.rect, option.palette, StarRating::EditMode::ReadOnly);
My starRating object needs to know the data in order to paint. I don't know how to pass that information to that object. Or...am I doing this wrong?
EDIT 2: I see that I can modify the signature of my starRating to include the value I want to paint, so disregard my last question.
-
@mzimmers
A different way of abstracting sounds like doing the work/mapping for the Widgets column in aQAbstractProxyModel
instead of in the delegate. AQIdentityProxyModel
with an extra Widgets column. The delegate then just draws the rectangle or whatever without doing the logic of calculating anything. Just a possibility. -
Thanks to everyone's help, I think I'm very close to having this functional. I'm able to paint the data in the manner I want, and it displays correctly.
One remaining issue: when the data changes, the display doesn't automatically repaint. (It does repaint if I defocus, or resize or much of anything else, just not on its own.) What is the recommended way of getting this to update? I do have a one second timer in my worker thread that updates the model and signals the display thread, but that doesn't provoke a repaint (I guess because I have the delegate).
Thanks...
-
@mzimmers
The fact that you have your own delegate should not be at issue here. The fact that it does repaint when resized (calls for a complete redraw) but not when data changes indicates the rightdataChanged()
signal is not being emitted. Put inqDebug()
statements to see when that does/does not occur. If you are still computing one column's value/need to redraw from another column's value change, you would need thedataChanged()
signal parameters to include both columns, or multiple signals. -
@JonB I have this connection in my Widget:
QObject::connect(m_model, &QStandardItemModel::dataChanged, this, &Widget::processChange); void Widget::processChange(QModelIndex qmi1, QModelIndex qmi2) { Q_UNUSED(qmi1) Q_UNUSED(qmi2) ui->tableView->repaint(); }
The slot is called, but the repaint() doesn't seem to do anything until I click on the Widget.
Could this somehow be related to the use of an incorrect model class? I'm using QStandardItemView, but my display widget is a QTableView.
Thanks...
-
When a model changes you don't have to call update on the view - this is done automatically when the dataChanged() emits with the correct indexes.
-
@Christian-Ehrlicher well then, I'm doing something wrong. My model has the following columns:
enum MODEL_COLUMNS { COLUMN_LABEL, COLUMN_OFFSET, COLUMN_TEXT_TIME, COLUMN_PAINT_TIME, NBR_COLS } ;
In my Widget:
ui->tableView->setModel(model); timeDelegate = new TimeDelegate(); ui->tableView->setItemDelegateForColumn(COLUMN_PAINT_TIME, timeDelegate);
The idea is that when something in column COLUMN_TEXT_TIME changes, I want to alter the corresponding entry in COLUMN_PAINT_TIME (in the view). I'm getting the signal from the model, and I do repaint, but the repaint only becomes visible when I click on the window (or try to resize it, or try to move it). These actions also cause the signal to be sent again.
Am I missing something here?
Thanks...
-
@mzimmers said in QStyledItemDelegate inside a QTableView?:
Am I missing something here?
As I said - you may be calling dataChanged() with wrong indexes or roles but you don't show us the code. Did you disabled updates for your widgets somehow?
Also you should rather use update() instead repaint() -
@Christian-Ehrlicher I don't understand why columns or roles play a factor here. The dataChanged() signal is getting delivered; I've verified that in the debugger. And when it does, I unconditionally call:
ui->tableView->repaint();
(I'll change that to update().)
Shouldn't this be sufficient without regard to roles or columns?
I thought I'd posted all the relevant code; what else would be helpful?
Thanks...
EDIT: I verified that updates are indeed enabled on the widget.
-
@mzimmers said in QStyledItemDelegate inside a QTableView?:
The dataChanged() signal is getting delivered
When you use the wrong indexes it will not help. But do what you want.
-
@Christian-Ehrlicher I'm sorry, but I'm just not understanding something here. If I get a signal from the model, and respond to that signal by repainting/updating/whatever my QTableView, is that not sufficient to trigger my delegate's paint() event (which will update the COLUMN_PAINT_TIME column)?
Thanks....
EDIT: well, what @Christian-Ehrlicher said was 100% right, but...I still don't understand the mechanism. I added a line in my worker thread to update the COLUMN_PAINT_TIME in the model, and now it seems to be working. (The date of model update in this case seems to be irrelevant, as the display is changed by the delegate.)
What I don't understand is, if the COLUMN_PAINT_TIME in the view is under control of a delegate, why does the model have to be updated? If my widget slot is calling repaint/update/whatever unconditionally, why isn't that sufficient irrespective of the arguments in the signal/slot mechanism?