Resizing QTableView section with custom editor - c++

I have an application with a QTableView and a model derived from QAbstractItemModel: the first column of the table contains a text (a label for each row), while the second column shows a value that can be selected using a QComboBox created from a custom item delegate. The content of the table may change dynamically (number of rows, language...).
I'd like to resize columns so the second one fits the content and the first one stretches occupying the remaining space.
My first attempt was:
tblData->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
tblData->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
Result:
The problem is that when the QComboBox is selected it doesn't fit the section and it is clipped:
I've managed to solve this issue by manually increasing the width:
tblData->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
tblData->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed);
tblData->resizeColumnToContents(1);
tblData->horizontalHeader()->resizeSection(1, tblData->horizontalHeader()->sectionSize(1) + 40);
Now the issue here is that by using such constant (40 in this case) the width of the section will vary depending on values displayed rather than in all the possible values (if the largest ones are already displayed vs if only the shortest). Also, that constant will be dependant to the style used, since it is also related to the space consumed by the QComboBox.
I've thought about using the Qt::SizeHintRole to manually compute the section width, but it is completely ignored. Even if it was, I cannot compute the actual width of the text (using QFontMetrics::width) because I don't have any font information in the model.
Another approach I've tried is to set the adjust size policy of the QComboBox in the QItemDelegate::createEditor method:
QWidget* myItemDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const {
auto comboBox = new QComboBox(parent);
comboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents);
// ...
}
But now the combo boxes are either clipped or shortened.
How can I solve set the section size based on the complete range of content instead of just visible data?
I'm self-answering the question with the best approach I've found so far, and the one I'm using in the project right now, but I'm not convinced with it (I detail reasons on the answer) so I'd like to know the correct way to do it. Thanks!

The sizeHint in the delegate is the right way to go but, instead of creating an editor, fill a QStyleOptionComboBox struct and use qApp->style()->sizeFromContents(QStyle::CT_ComboBox, &opt, sh, nullptr);, where sh is the size of the internal string. You can use QFontMetrics to calculate that or just call the base class QStyledItemDelegate::sizeHint(...).

The best option I've found so far is to re-implement the QItemDelegate::sizeHint: I have the font information from the QStyleOptionViewItem and the list of elements to be included in the QComboBox.
QSize myItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
auto hint = QItemDelegate::sizeHint(option, index);
QFontMetrics fm(option.font);
std::unique_ptr<QWidget> editor(createEditor(nullptr, option, index));
auto comboBox = qobject_cast<QComboBox*>(editor.get());
if (comboBox != nullptr) {
int width = 0;
for (int ii = 0; ii < comboBox->count(); ++ii) {
width = std::max(width, fm.width(comboBox->itemText(ii)) + 20);
}
hint.setWidth(std::max(hint.width(), width));
}
return hint;
}
Results:
Drawbacks of this solution are:
I don't have information regarding the additional space required by the QComboBox so it is not style-independant yet (as with the second approach on the question)
If new editors are added to the item delegate then I'd have to include them manually in the size hint computation too, which is not a terrible pain but feels like a bad design.
PS: using the QComboBox::sizeHint here doesn't work since size hint is computed using the QComboBox::sizeAdjustPolicy which, as highlighted in the question, doesn't adjust combo boxes correctly into the cell.
UPDATE
I've updated the solution following the indications from comments and accepted answer. Here is the complete code for future reference:
QStringList myItemDelegate::getPossibleValuesForIndex(const QModelIndex& index) const
{
// returns list of all possible values for given index (the content of the combo box)
}
QSize myItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
auto hint = QItemDelegate::sizeHint(option, index);
QFontMetrics fm(option.font);
QStyleOptionComboBox comboOption;
comboOption.rect = option.rect;
comboOption.state = option.state | QStyle::State_Enabled;
Q_FOREACH (const auto& value, getPossibleValuesForIndex(index)) {
hint = hint.expandedTo(qApp->style()->sizeFromContents(QStyle::CT_ComboBox,
&comboOption, QSize(fm.width(value), hint.height())));
}
return hint;
}

Related

How to draw different lines inside column of QTableView depending on data in cells in near column?

I want to draw lines inside column that show possible connections between different signals(Further, I also want to make radiobuttons on them to choose what connections are active).
But now I have trouble that delegates allow me to SetItemDelegate only for all column or all row. So I can't just make different blocks of this lines like vertical line, corner lines, horizontal line and then paint them depending on data in cells. I attached an example image. What should I use to draw something like this?
Something like:
Define a new style, override drawPrimitive method and do custom painting?
Could you show me an example, please?
Lines example
What I have for now
My main code for creating rows with signals(I take them from .txt file for simulation for now):
int IPFilesize = IPfilespl.size();
ui->CompTab->setRowCount(IPFilesize);
for (int i = 0; i<IPFilesize; i++)
{
QWidget *ChBx = new QWidget();
QCheckBox *pCheckBox = new QCheckBox();
QHBoxLayout *pLayout = new QHBoxLayout(ChBx);
pLayout->addWidget(pCheckBox);
pLayout->setAlignment(Qt::AlignCenter);
pLayout->setContentsMargins(0,0,0,0);
ChBx->setLayout(pLayout);
ui->CompTab->setCellWidget(i, 0, ChBx);
//connect(ChBx,SIGNAL(clicked()),this,SLOT(checkboxClicked()));
}
for (int ii = 0; ii<IPFilesize; ii++)
{
ui->CompTab->setItem(ii, 2, new QTableWidgetItem(IPfilespl.at(ii)) );
//connect(ChBx,SIGNAL(clicked()),this,SLOT(checkboxClicked()));
}
ui->CompTab->setItemDelegateForColumn(1, new WireDelegateDown());
Header code
class WireDelegate: public QStyledItemDelegate { protected: void paint(QPainter* painter, const QStyleOptionViewItem& opt, const QModelIndex& index) const {
int x = opt.rect.x();
double y = opt.rect.y();
QPoint c = opt.rect.center();
double centerx = c.x();
double centery = c.y();
double r = opt.rect.right();
double width = opt.rect.width();
double height = opt.rect.height();
QPainterPath path;
path.addRect(centerx, centery-height/2, 5.0, height/2);
path.moveTo(0, 0);
path.addRect(centerx, centery, width/2, 5.0);
path = path.simplified();
painter->drawPath(path);
Your item delegate could be a subclass of QAbstractItemDelegate. Then you can set its type with a property like shapeType (or whatever you name it). Based on the shapeType, you can do internal painting stuff from within the reimplemented paint method.
void MyConnectionDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (m_shapeType == ShapeType::horizontalLine) {
//..... Your fancy drawings happens here based on shapetype
} else if (m_shapeType == ShapeType::verticalLine) {
.
.
.
As I see in the picture (your desired result) it's not going to be simple and it can get quite complicated to implement such behavior. You will have to calculate the width, height, position of lines, colors, dots, arrows, nodes, etc for each delegate. When you exactly know which entities should be drawn in each cell, painting them using QPainter is a simple task.
You might consider whether QTableView is getting in the way more than it helps you. The built-in widgets are fantastic, but often, I've found that when I need to venture outside the realm of what they were specifically designed to do, I end up spending more time working around them than I get benefit. I don't know the right solution for what you're doing, but if it were me, I'd explore writing my own view based on QAbstractItemView and then just doing my own custom painting for the whole thing.
The downside of doing that is that QTableView provides a lot of interaction support for you, so if the interaction is a big benefit to you, then you have to write your own as well. So it's a trade-off. It's also a possibility that the built-in interaction for QTableView also gets in the way of what you're trying to do. It can go either way.

Is it possible to add a custom widget into a QListView?

I have a large log data (100, 1000, 100000, ... records) and I want to visualize it in the following manner:
Which widget (e.g. QListView, QListWidget) should I use and how, in order to stay away from performance and memory problems?
Is it possible to add a custom widget into a QListView?
Please, read about:
How to display a scrollable list with a substantial amount of widgets as items in a Qt C++ app?
I want to show every log message in the above format
Solution
To achieve the desired result and stay away from performance issues, even with a very long data log, use a QListView with a custom delegate:
Create a subclass of QStyledItemDelegate, say Delegate
Reimplement the QStyledItemDelegate::paint method to do the custom drawing
Reimplement the QStyledItemDelegate::sizeHint to report the correct size of the items in the list
Use the custom delegate in the view by calling QAbstractItemView::setItemDelegate
Example
I have prepared a working example for you in order to demonstrate how the proposed solution could be implemented and used in an application.
The essential part of the example is the way the delegate paints the items in the list view:
void Delegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QStyleOptionViewItem opt(option);
initStyleOption(&opt, index);
const QPalette &palette(opt.palette);
const QRect &rect(opt.rect);
const QRect &contentRect(rect.adjusted(m_ptr->margins.left(),
m_ptr->margins.top(),
-m_ptr->margins.right(),
-m_ptr->margins.bottom()));
const bool lastIndex = (index.model()->rowCount() - 1) == index.row();
const bool hasIcon = !opt.icon.isNull();
const int bottomEdge = rect.bottom();
QFont f(opt.font);
f.setPointSize(m_ptr->timestampFontPointSize(opt.font));
painter->save();
painter->setClipping(true);
painter->setClipRect(rect);
painter->setFont(opt.font);
// Draw background
painter->fillRect(rect, opt.state & QStyle::State_Selected ?
palette.highlight().color() :
palette.light().color());
// Draw bottom line
painter->setPen(lastIndex ? palette.dark().color()
: palette.mid().color());
painter->drawLine(lastIndex ? rect.left() : m_ptr->margins.left(),
bottomEdge, rect.right(), bottomEdge);
// Draw message icon
if (hasIcon)
painter->drawPixmap(contentRect.left(), contentRect.top(),
opt.icon.pixmap(m_ptr->iconSize));
// Draw timestamp
QRect timeStampRect(m_ptr->timestampBox(opt, index));
timeStampRect.moveTo(m_ptr->margins.left() + m_ptr->iconSize.width()
+ m_ptr->spacingHorizontal, contentRect.top());
painter->setFont(f);
painter->setPen(palette.text().color());
painter->drawText(timeStampRect, Qt::TextSingleLine,
index.data(Qt::UserRole).toString());
// Draw message text
QRect messageRect(m_ptr->messageBox(opt));
messageRect.moveTo(timeStampRect.left(), timeStampRect.bottom()
+ m_ptr->spacingVertical);
painter->setFont(opt.font);
painter->setPen(palette.windowText().color());
painter->drawText(messageRect, Qt::TextSingleLine, opt.text);
painter->restore();
}
The complete code of the example is available on GitHub.
Result
As written, the given example produces the following result:

Qt5 QStyledItemDelegate on a QListView removes all the default style

I have some QIcon and QString pairs displayed in a QListview. The whole thing has been set up using the Qt Model/View Programming.
I am displaying labeled icons in this QListView. Items are displayed using the IconMode, Snap and TopToBottom flags. Thus, these are organised into a grid.
I would like to layout all the QListView items vertically and centered. In order to do this, I subclassed the QStyledItemDelegate object, and overloaded the paint method. However, I have three main problems:
Icon labels have been moved (in the QStyledItemDelegate subclasses) and a dotted square appears at its original place.
All the default styles are gone (hover, selection). I know how I can add some again, but I would like to use the default one (Windows style).
Everything is rendered into a grid, even if setGridSize is not called. I would like to use only one "column".
Here is a piece of code:
An extract of the constructor of my custom QListView:
setViewMode(QListView::IconMode);
setMovement(QListView::Snap);
setFlow(QListView::TopToBottom);
setSpacing(5);
setIconSize(QSize(iconSize, iconSize));
setGridSize(QSize(iconSize + 10, iconSize + 10));
setDragEnabled(true);
setAcceptDrops(true);
setDropIndicatorShown(true);
The paint method of the QStyledItemDelegate:
void FramesStyledItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QStyleOptionViewItemV4 opt = option;
//initStyleOption(&opt, index);
opt.icon = QIcon();
opt.text = QString();
QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter);
const QRect r = option.rect;
QIcon icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
QString string = qvariant_cast<QString>(index.data(Qt::DisplayRole));
QPixmap pix = icon.pixmap(r.size());
const QPoint p = QPoint((r.width() - pix.width())/2, (r.height() - pix.height())/2);
painter->drawPixmap(r.topLeft() + p, pix);
painter->drawText(r.center() + p + QPoint(-(string.count() / 2), r.height() / 2), string);
}
If I do not use the initStyleOption shown above, I can remove the
dotted square, but I lose all the default styles.
If I uncomment the initStyleOption, The dotted square appears. I also lose all the default styles.
Here are some screenshots:
The cursor is on item 0 (No hover decoration, no selection decoration).
Item 0 has been selected. A small dotted square appears (initStyleOption has been uncommented).
I have switched to the ListMode. Selection decoration is working but not hover. Again, a small dotted square appears at the original place of the label.
Does someone have an idea? Thanks for your answers.

Qt4 QTableWidget make Colums resized to contents, interactive and aligned to tableborder

This code
horizontalHeader()->setResizeMode(QHeaderView::Stretch);
stretches the cloumns of a qtablewidget. I want them to be stretched, what means be aligned to the border of the qtablewidget, no matter how big it is.
I also want them not to be smaller than their contents and to be resizable by the user.
This means, I would have to use
horizontalHeader()->setResizeMode(QHeaderView::Stretch);
horizontalHeader()->setResizeMode(QHeaderView::Interactive);
horizontalHeader()->setResizeMode(QHeaderView::ResizeToContents);
at once, which is not possible.
I know I can give every column another view, like
horizontalHeader()->setResizeMode(0, QHeaderView::Interactive);
horizontalHeader()->setResizeMode(1, QHeaderView::ResizeToContents);
but this ist not what I want. I want the colums to be
not smaller than their contents
resizable by the user
aligned to the border of the qtablewidget
Any ideas?
I think that you should reimplement sizeHintForColumn. The below code will give you a start.
int TableWidget::sizeHintForColumn(int column) const // to get resize on all rows in the column, i.e. not only visible rows.
{
if(d_resizeColumnsOnVisibleRowsOnly)
return QTableView::sizeHintForColumn(column);
if(!model())
return -1;
QStyleOptionViewItem option(viewOptions());
int hint(0);
QModelIndex index;
QWidget* w(0);
for(int row(0);row<rowCount();++row)
{
index=model()->index(row,column);
w=cellWidget(row,column);
int hint_for_row(qMax(itemDelegate(index)->sizeHint(option,index).width(),(w?w->sizeHint().width():0)));
hint=qMax(hint,hint_for_row);
}
return showGrid()?hint+1:hint;
}

Varying list item size using delegate in Qt

I have a list view. That list view has items. For each item I use setItemDelegate and I override the paint method of the delegate. The think is that in each item I am writing some text, and when the text is really long there is no space enough.
How can I resize the item from the paint event? since I get the bounding box of the drawn text in the paint event.
Thanks in advance,
You cannot. When the item delegate's paint method is called, the list view has already been laid out and the QPainter you receive as argument might have a drawing surface that is the same size as the size hint or at least have a transform and clipping rect set to respect the size hint.
You must calculate the text size in the QAbstractItemDelegate::sizeHint method (using QFontMetrics) and return an appropriate size hint. Cache your results for better performance.
you need to implement sizeHint method
QListItemDelegat::QListItemDelegat(): QStyledItemDelegate(0){}
QSize
QListItemDelegat::sizeHint( const QStyleOptionViewItem& option, const DataClass& data ) const
{
const QStyle* style( QApplication::style( ) );
QFont nameFont( option.font );
nameFont.setWeight( QFont::Bold );
const QFontMetrics nameFM( nameFont );
const QString nameStr( data.GetName() );
int nameWidth = nameFM.width(nameStr);
int nameHeight = nameFM.height(nameStr);
return QSize(nameWidth ,nameHeight)
}