Qt Support Weekly #27 – Widgets on a header

Just recently someone had asked about having QComboBoxes placed over the sections inside a QHeaderView, initially I thought this would be fairly straightforward as you could use the setViewportMargins() and put the widgets at the top.  But unfortunately this is not possible to do because the itemviews set the viewport margins itself and setting it outside of this will cause problems so that approach is not recommended.

Therefore the way to get it working is to actually create the widgets and place them on the QHeaderView directly, this means that we have to adjust them manually when sections are resized, moved or when the itemview itself is scrolled.  So in order to do this we need to start off by subclassing QHeaderView.  In this case I am focusing purely on a horizontal header as this makes it more straight forward to focus on instead of trying to account for the direction sections are moving.

MyHorizontalHeader(QWidget *parent = 0) : QHeaderView(Qt::Horizontal, parent)
{
connect(this, SIGNAL(sectionResized(int, int, int)), this,
SLOT(handleSectionResized(int)));
connect(this, SIGNAL(sectionMoved(int, int, int)), this, 
SLOT(handleSectionMoved(int, int, int)));
setMovable(true);
}

We have added two slots already here, one is for handling when a section is resized and one for when a section is moved.  We will get to them in a bit, but first we will cover the initialization of the widgets.  We cannot do this in the constructor (although we could do if we know more about the itemview it will be associated with, but for this case I want to be as generic as possible) as we don't know how many sections there will be.  So the best place to do this in a reimplemented showEvent():

void showEvent(QShowEvent *e)
{
for (int i=0;i<count();i++) {
if (!boxes[i]) {
QComboBox *box = new QComboBox(this);
boxes[i] = box;
}
boxes[i]->setGeometry(sectionViewportPosition(i), 0,
sectionSize(i) - 5, height());
boxes[i]->show();
}
QHeaderView::showEvent(e);
}

The boxes array is just a QMap<int, QComboBox *> with the integer referring to the logical index of the section the combobox is connected to.  In order to get the correct position on the viewport for the section we use sectionViewportPosition() and sectionSize() is used to get the size of the section.  The reason the -5 is there is purely so that I can see a bit of the section underneath to allow moving of the sections.  If you only want to see the splitter to resize the sections you can change it to -2.

What is left then is to react to the resize of the sections and the moving of the sections with the following slots:

void handleSectionResized(int i)
{
for (int j=visualIndex(i);j<count();j++) {
int logical = logicalIndex(j);
boxes[logical]->setGeometry(sectionViewportPosition(logical), 0,
sectionSize(logical) - 5, height());
}
}
void handleSectionMoved(int logical, int oldVisualIndex, int newVisualIndex)
{
for (int i=qMin(oldVisualIndex, newVisualIndex);i<count();i++){
int logical = logicalIndex(i);
boxes[logical]->setGeometry(sectionViewportPosition(logical), 0,
sectionSize(logical) - 5, height());
}
}

The code is fundamentally the same in both slots but the loop is tweaked to limit how many widgets have to be touched, when a section is resized we only go from the visual index of the one resized throughout the rest of the sections after that.  When a section is moved we need to go from where the first visual index is hit to the end of the headerview as the other sections will have to be updated to adjust.

So far so good, the only thing left for us to do now is handle the case where the itemview is scrolled as we need to reposition the widgets again.  Since the QHeaderView is not being notified in any way that this occurs as it is on the viewport we need to connect this up from the itemview itself when a scroll occurs.  To do this we need to reimplement scrollContentsBy():

void scrollContentsBy(int dx, int dy)
{
QTableWidget::scrollContentsBy(dx, dy);
if (dx != 0)
horizHeader->fixComboPositions();
}

What is not shown here is the constructor which creates an instance of the MyHorizontalHeader class and sets it with setHorizontalHeader().  So whenever the contents are scrolled in either horizontal direction we call fixComboPositions() which does:

void fixComboPositions()
{
for (int i=0;i<count();i++)
boxes[i]->setGeometry(sectionViewportPosition(i), 0,
sectionSize(i) - 5, height());
}

In effect that is the same as what showEvent() does, since we don't know how much was made visible or hidden the easiest way is to just go over all the widgets.

And there you have it, a fairly straightforward approach to getting widgets on a QHeaderView on a per section basis which should be easily adaptable for your own needs.

Before I finish this blog for this week I want to take the opportunity to point out the latest security announcement regarding OpenSSL and compression in case you have not already seen it.  You can read more about this at http://qt.digia.com/en/Release-Notes/security-issue-september-2012/

Until next time, happy coding :)


Blog Topics:

Comments