PDA

View Full Version : Quick way for QWidget in QHeaderView's columns?



Davor
8th January 2010, 07:41
I am tying to implement a custom widget for each column in QHeaderView.

As the QHeaderView is an extension of the QAbstractItemView, the obvious choice would be to implement a custom delegate. But, for QHeaderView, the delegate is explicitely deleted in the source code, so that doesn't (?) seem a viable option.

An other possible method that could work is to reimplement QHeaderView::paintSection() (and maybe even QHeaderView::paintEvent().

My question is whether this latter method is the one to go, or am I overlooking an other obvious method? Some people (for example) are speaking about QWidgets in QHeaderView's constructors, but that doesn't seem to work well, especially if one wants QWidgets for every columnheader as in my case.

Any suggestions?

wysota
8th January 2010, 19:39
I would skip QHeaderView completely and use a completely custom widget possibly with a layout and position it on the view using setViewportMargins() and reimplementing resizeEvent() of the view to move the "header" around when size of the view changes.

Davor
9th January 2010, 16:58
Thanks for the answer Wysota. In the meantime, I have researched a bit more and implemented tree workable solutions which seem bit more simple. (Primarily because the number of columns should be changeable on the fly, and thereby the headers too.)

FIRST and SECOND:
Forget about QHeaderView, and use the main View. In Qt examples you can find how to lock rows/columns. So, the first way is the most simple one: just use the QAbstractItemView::setIndexWidget(). But this seems a convenience function, given the MVC/MVD paradigm. So in my second try I tried the same with the delegate:



#include <QtGui>

class HeaderObject : public QWidget{
public:
HeaderObject(QWidget *parent = 0) : QWidget(parent){
QComboBox *c = new QComboBox(this);
QCheckBox *cb = new QCheckBox(this);
c->addItem("test");
c->addItem("test2");
QGridLayout *l = new QGridLayout;
l->addWidget(c);
l->addWidget(cb);
setLayout(l);
}
};

class HeaderDelegate : public QStyledItemDelegate{
public:
HeaderDelegate(QWidget *parent = 0) : QStyledItemDelegate(parent) {
for(int i = 0; i < 5; i++){
headerSections.insert(i,new HeaderObject(qobject_cast<QTableView*>(parent)->viewport()));
headerSections[i]->hide();
}
}

void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
if (index.row() == 0){
qDebug() << "QRect: " << option.rect;
headerSections[index.column()]->setGeometry(option.rect);
headerSections[index.column()]->show();
}
}

QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const {
qDebug() << "Asking for SIZEHINT: " << headerSections[0]->sizeHint();
return headerSections[0]->sizeHint();
}

private:
QVector< QPointer <HeaderObject> > headerSections;
};

class CustomModel : public QAbstractTableModel{
public:
CustomModel() : QAbstractTableModel(){}

int rowCount(const QModelIndex &parent = QModelIndex()) const{return 5;}
int columnCount(const QModelIndex &parent = QModelIndex()) const {return 5;}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const{
if (role == Qt::DisplayRole) return QVariant();
return QVariant();
}

private:
};

int main(int argc, char *argv[])
{
QApplication a(argc, argv);

CustomModel *model = new CustomModel;
QTableView *view = new QTableView();
view->setModel(model);
view->setItemDelegateForRow(0, new HeaderDelegate(view));
view->resizeColumnsToContents();
view->resizeRowsToContents();
view->show();
return a.exec();
}


This surprisingly works very well. With some more tweaking it could be made nice. A proxy could be made to supply the widgets for the “header”-row, thereby holding to the MVD paradigm. But then I thought, wouldn't something similar also be possible with the QHeaderView? That's my third try, and also where the problems started to pop up. Here is the code:

THIRD:


#include <QtGui>

class HeaderObject : public QWidget{
public:
HeaderObject(QWidget *parent = 0) : QWidget(parent){
QComboBox *c = new QComboBox(this);
QCheckBox *cb = new QCheckBox(this);
c->addItem("test");
c->addItem("test2");
QGridLayout *l = new QGridLayout;
l->addWidget(c);
l->addWidget(cb);
setLayout(l);
}
};

class CustomHeader : public QHeaderView{
public:
CustomHeader(QWidget *parent = 0):QHeaderView(Qt::Horizontal, parent){

for(int i = 0; i<5; i++){
headerSections.insert(i,new HeaderObject(this));
headerSections[i]->hide();
}
setFont(QFont("Helvetica [Cronyx]", 32));
setMinimumSectionSize(headerSections[0]->minimumSizeHint().width());
setDefaultSectionSize(headerSections[0]->minimumSizeHint().width());
}
protected:
void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const {

if (!rect.isValid())
return;
qDebug() << logicalIndex;
qDebug() << "QRect: " << rect << " AND OFFSET:" << offset();
headerSections[logicalIndex]->setGeometry(rect);
headerSections[logicalIndex]->show();
}

private:
QVector< QPointer <HeaderObject> > headerSections;
};


class CustomModel : public QAbstractTableModel{
public:
CustomModel() : QAbstractTableModel(){
}

int rowCount(const QModelIndex &parent = QModelIndex()) const{return 5;}
int columnCount(const QModelIndex &parent = QModelIndex()) const {return 5;}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const{
if (role == Qt::DisplayRole) return QVariant();
return QVariant();
}
};

int main(int argc, char *argv[])
{
QApplication a(argc, argv);

CustomModel *model = new CustomModel;
QTableView *view = new QTableView();
view->setModel(model);
view->setHorizontalHeader(new CustomHeader(view));
view->show();
return a.exec();
}


The main problem here is the horizontal QHeaderViews height. Guess what, you can not adjust it. Notice the setFont(). Up till now, that's the only way I have found to adjust the QHeaderView's height. I have tried setGeometry on both QHeaderView and it's viewport(), but none work. For some reason (which I can't figure out from the sourcecode) it always reverts to size 100x30. Only by explicitly catching viewport resize events did I manage to get the widgets to the required size (without the QFont hack), but the header remained the same size (so the widgets overlapped with the columns). This still seems to me the desirable way to go, but I do not see how to adjust the height. Any suggestions? You can also criticize my whole approach.

wysota
10th January 2010, 15:55
I think the last approach is wrong (especially the part with showing widgets inside reimplementation of paintSection). The first two are ok but I'd make the "header view" a separate widget from the main view.

boudie
11th January 2010, 08:16
@Davor:
Did you also try to use setSizePolicy() to allow the QHeaderView to change its height?
Because, iirc, a 'horizontal' QHeaderView can, by default, only expand in horizontal direction.

Davor
11th January 2010, 09:00
@ Boudie
Thanks for the suggestion. It made me re-implement sizeHint() of QHeaderView, which made my third approach work:



#include <QtGui>

class HeaderObject : public QWidget{
public:
HeaderObject(QWidget *parent = 0) : QWidget(parent){
QComboBox *c = new QComboBox(this);
QCheckBox *cb = new QCheckBox(this);
c->addItem("test");
c->addItem("test2");
QGridLayout *l = new QGridLayout;
l->addWidget(c);
l->addWidget(cb);
setLayout(l);
}
};

class CustomHeader : public QHeaderView{
public:
CustomHeader(QWidget *parent = 0):QHeaderView(Qt::Horizontal, parent){

for(int i = 0; i<5; i++){
headerSections.insert(i,new HeaderObject(this));
headerSections[i]->hide();
}
//setFont(QFont("Helvetica [Cronyx]", 32));
//setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
setMinimumSectionSize(headerSections[0]->minimumSizeHint().width());
setDefaultSectionSize(headerSections[0]->minimumSizeHint().width());
}
protected:
void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const {
if (!rect.isValid())
return;
//qDebug() << logicalIndex;
//qDebug() << "QRect: " << rect << " AND OFFSET:" << offset();
headerSections[logicalIndex]->setGeometry(rect);
headerSections[logicalIndex]->show();
}

QSize sizeHint() const {
QSize s = size();
s.setHeight(headerSections[0]->minimumSizeHint().height());
return s;
}

private:
mutable QVector< QPointer <HeaderObject> > headerSections;
};


class CustomModel : public QAbstractTableModel{
public:
CustomModel() : QAbstractTableModel(){
}

int rowCount(const QModelIndex &parent = QModelIndex()) const{return 5;}
int columnCount(const QModelIndex &parent = QModelIndex()) const {return 5;}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const{
if (role == Qt::DisplayRole) return QVariant();
return QVariant();
}

private:
};

int main(int argc, char *argv[])
{
QApplication a(argc, argv);

CustomModel *model = new CustomModel;
QTableView *view = new QTableView();
view->setModel(model);
view->setHorizontalHeader(new CustomHeader(view));
view->show();
return a.exec();
}


@Wysota: I don't see any other option than to re-implement the paint-sections (if one insists to subclass QHeaderView.) I my second approach I am also showing widgets from inside QItemDelegate's paint() member function. The delegate only allows widgets for editing, but not for showing. This seems logical, but in a sense also limiting.

wysota
11th January 2010, 09:30
@Wysota: I don't see any other option than to re-implement the paint-sections (if one insists to subclass QHeaderView.)
But not this way. You are simply covering the header with other widgets and at the same time you do it in a very wrong way.

Do it properly - provide your own header widget but don't derive it from QHeaderView.

Edit: A dirty example attached...

Davor
14th January 2010, 12:21
Thanks for the example Wysota.


But not this way. You are simply covering the header with other widgets and at the same time you do it in a very wrong way.

It was just an example for one to start with. On the net, you can find many questions on this issue, but no examples.

Furthermore, I don't see what's wrong with covering a widget with other widgets, as long as they remain in the hierarchy. Furthermore, the header isn't painting anything. A better way to do it would be to put them on the viewport:

headerSections.insert(i,new HeaderObject(this.viewport()))

The paintSection() should arrange all the widgets on the viewport, and they would be in sync all the time with the columns. So basicly, it only paints once, when new columns are added. Other times, it just checks whether widget is in the correct position.
Here a quick sample code:


protected:
void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const {
if (headerSections[logicalIndex]->geometry() != rect) {
qDebug() << "WRONG!";
for(int i = 0; i < model()->columnCount(); i++){
QRect newRect(sectionViewportPosition(i) ,rect.y(), sectionSize(i), rect.height());
headerSections[i]->setGeometry(newRect);
if (!headerSections[i]->isVisible()) headerSections[i]->show();
}
}
}




Do it properly - provide your own header widget but don't derive it from QHeaderView.


For one, that's a lot of work for something that is already there, which is (between other things) keeping the header columns in sync with the rest. For two, doesn't the Object oriented paradigm say to reuse the items, instead of to hide them? QHeaderView is there, like it or not.

wysota
14th January 2010, 15:13
Furthermore, I don't see what's wrong with covering a widget with other widgets, as long as they remain in the hierarchy.
If you do it in paintEvent() then it's terribly wrong. If not then it's still more logical to hide the header first and then replace it with something of your own.


Furthermore, the header isn't painting anything. A better way to do it would be to put them on the viewport:

headerSections.insert(i,new HeaderObject(this.viewport()))
No. The header is above the viewport, not inside it.


The paintSection() should arrange all the widgets on the viewport, and they would be in sync all the time with the columns. So basicly, it only paints once, when new columns are added. Other times, it just checks whether widget is in the correct position.
I think at least one of us doesn't understand how repainting widget in Qt works and what does QWidget::show() do. In essence it does something different that QWidget::render()


For one, that's a lot of work for something that is already there,
Not really. QHeaderView is a very stupid class, it's very easy to replace it.


which is (between other things) keeping the header columns in sync with the rest.
That's very easy. All the required signals are already there.


For two, doesn't the Object oriented paradigm say to reuse the items, instead of to hide them? QHeaderView is there, like it or not.
The thing is it is mostly a stub, not a real class. And you are not using the header view if you just put a widget over it.

martonmiklos
18th April 2012, 19:07
Dear wysota!

Could you please repost the attachment because I could download it but the archive seems to be malformed.

Thanks in avance!

wysota
19th April 2012, 09:12
It's probably double-gzipped. I don't know why that happens but that's often the case with my attachments here.

martonmiklos
19th April 2012, 18:22
Thanks! You are right, it seems to be a gz file which contains a tar.gz.

Added after 1 2 minutes:

I have checked both code, and I would like to warm up this thread a little bit.

I would like to show a QComboBox in a headerview. No not for sorting reasons, but for selecting the column's function in a CSV import dialog. The column count is dynamic, and the columnd width is user changable, and the table can be scrolled vertically.

I have tried to improve the wysota's example. I have connected a lot of signals from the original headerview so now my widgets are properly resized, but I cannot deal anything with the scrolling. I do not want to get my tableview to the game, so from my point of view the first implementation seems to be more suitable even if it has technical problems. Do you have any suggestions about the topic?

wysota
19th April 2012, 21:18
What do you mean regarding scrolling? What's the problem?

Indian
7th March 2013, 09:10
This helped me a lot ... Thanks for the post..:)