Varying list item size using delegate in Qt - c++

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)
}

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:

Resizing QTableView section with custom editor

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;
}

Reimplementing QStyledItemDelegate::paint - How to obtain subelement coordinates?

A QTreeView is rendered with the help of a custom QStyledItemDelegate::paint method. The intention is to add graphical elements to the nodes, e.g. to draw (and fill) a box around the item texts. The tree items may have check boxes, or not.
The Ruby code below achieves the goal, except that I cannot obtain the coordinates of the text element. An empirical offset (x=29; y=4) serves as a workaround. The super method draws the text on top of the box.
How can I obtain the coordinates of the text element?
Is this the right approach at all, or do I have to use drawText and drawControl instead of calling the superclass paint method? In that case, how do you control the layout of the sub elements?
(This question is not Ruby specific. Answers containing C++ are welcome.)
class ItemDelegate < Qt::StyledItemDelegate
def paint(painter, option, index)
text = index.data.toString
bg_color = Qt::Color.new(Qt::yellow)
fg_color = Qt::Color.new(Qt::black)
offset = Qt::Point.new(29,4)
painter.save
painter.translate(option.rect.topLeft + offset)
recti = Qt::Rect.new(0, 0, option.rect.width, option.rect.height)
rectf = Qt::RectF.new(recti)
margin = 4
bounding = painter.boundingRect(rectf, Qt::AlignLeft, text)
tbox = Qt::RectF.new(Qt::PointF.new(-margin,0), bounding.size)
tbox.width += 2*margin
painter.fillRect(tbox, bg_color)
painter.drawRect(tbox)
painter.restore
super
end
end
Edit: Please find a self-contained example here in this Gist.
I had the same problem in C++. Unfortunately, the workaround on option.rect.* properties seems to be the only way to find the text coords.
Here the paint method of my delegate:
void ThumbnailDelegate::paint(QPainter *p_painter, const QStyleOptionViewItem &p_option, const QModelIndex &p_index) const
{
if(p_index.isValid())
{
const QAbstractItemModel* l_model = p_index.model();
QPen l_text_pen(Qt::darkGray);
QBrush l_brush(Qt::black, Qt::SolidPattern);
/** background rect **/
QPen l_pen;
l_pen.setStyle(Qt::SolidLine);
l_pen.setWidth(4);
l_pen.setBrush(Qt::lightGray);
l_pen.setCapStyle(Qt::RoundCap);
l_pen.setJoinStyle(Qt::RoundJoin);
p_painter->setPen(l_pen);
QRect l_border_rect;
l_border_rect.setX(p_option.rect.x() + 5);
l_border_rect.setY(p_option.rect.y() + 5);
l_border_rect.setWidth(p_option.rect.width() - 16);
l_border_rect.setHeight(p_option.rect.height() - 16);
QPainterPath l_rounded_rect;
l_rounded_rect.addRect(QRectF(l_border_rect));
p_painter->setClipPath(l_rounded_rect);
/** background color for hovered items **/
p_painter->fillPath(l_rounded_rect, l_brush);
p_painter->drawPath(l_rounded_rect);
/** image **/
QPixmap l_pixmap = bytearrayToPixmap(l_model->data(p_index, ImageRole).toByteArray()).scaled(150, 150, Qt::KeepAspectRatio);
QRect l_img_rect = l_border_rect;
int l_img_x = (l_img_rect.width()/2 - l_pixmap.width()/2)+l_img_rect.x();
l_img_rect.setX(l_img_x);
l_img_rect.setY(l_img_rect.y() + 12);
l_img_rect.setWidth(l_pixmap.width());
l_img_rect.setHeight(l_pixmap.height());
p_painter->drawPixmap(l_img_rect, l_pixmap);
/** label **/
QRect l_txt_rect = p_option.rect;
l_txt_rect.setX(l_border_rect.x()+5);
l_txt_rect.setY(l_border_rect.y() + l_border_rect.height() -20);
l_txt_rect.setHeight(20);
l_txt_rect.setWidth(l_txt_rect.width()-20);
QFont l_font;
l_font.setBold(true);
l_font.setPixelSize(12);
p_painter->setFont(l_font);
p_painter->setPen(l_text_pen);
QString l_text = l_model->data(p_index, TextRole).toString();
p_painter->drawText(l_txt_rect, Qt::ElideRight|Qt::AlignHCenter, l_text);
}
else
{
qWarning() << "ThumbnailDelegate::paint() Invalid index!";
}
}
I am not skilled on Ruby but, as you can see, I am using drawPath, drawPixmap and drawText.
Here is the result:
I think it is better to avoid invoking paint from the superclass, since it should be done automatically by Qt and you may break something on the UI lifecycle.

Qt: heightForWidth for word-wrapped text

I have a box with a varying width and a word-wrapped text. I need to set new height every time user changes box's width. The box is displayed by QPainter inside paintEvent(QPaintEvent *) function. There is several solutions, for example current (not very smart, i do this in resizeEvent(QResizeEvent *)):
unsigned new_height = 0; // the height i want to find out.
unsigned given_width = width();
QPainter painter (this); // i need painter, because i want to ask it's default font.
QLabel lab; // the widget that can do word-wrap.
lab.setText( "A word wrapped text" ); // the text
lab.setFont( painter.font() ); // set QPainter's default font.
lab.setWordWrap( true ); // enable word-wrap
new_height = lab.heightForWidth( given_width ); // tada! :)
But the code is overkill:
1) Creading QPainter is not good outside paintEvent(QPaintEvent *);
2) BUT i need QPainter to request what font is default for it to ask metrics for that font.
Should i change my code and do this operation with help of QPainter::boundingRect() inside the paintEvent(QPaintEvent *) function? But i'd like to reduce CPU consumption inside the paintEvent(QPaintEvent *) and calculate new height only when width changed, but not every time it displayed.
What is other solutions for the purpose of subject? QFontMectircs?
I think you have the right idea of using QFontMetrics. The whole idea of the class is to assist the situations like you have here. Take a look at QFontMetricsF::boundingRect()
Use your target paint rectangle as the input rect, but set the height to the max that your widget height. I'd just put something like INT_MAX in it just to be sure. :)