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

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.

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.

Qt Scaling Custom QGraphicsItem with unscaled text

I'm trying to create customObject (rectangle and it inherit from QGraphicsItem) that will be painted on scene with ceratin text(stored in attribute), but when I scale it - i wish to keep same size of text. Here is my over. paint function:
void CustomRectangle::paint(QPainter *painter, const QStyleOptionGraphicsItem *options, QWidget *widget)
{
QColor currentColor = get_ColorByCurrentState();
QRectF rect = boundingRect();
QPen pen(currentColor, Own_LineWidith);
painter->setPen(pen);
painter->drawRect(rect);
QRectF rect_text(rect.x(), rect.y(),100,100);
painter->drawText(rect_text,this->getText() );
}
and my two scaling functions:
void CustomObject::scaleUp()
{
scale(ScaleFactor_X,ScaleFactor_Y);
}
void CustomObject::scaleDown()
{
scale(1/ScaleFactor_X,1/ScaleFactor_Y);
}
But text still keep scaling along with rectangle.
EDIT 1
I tried adding it another way, i nfucntion that creates and adds my rectangle to scene (here - named "newObject"), but result is still the same.
QGraphicsTextItem* GTI = new QGraphicsTextItem(newObject->toStringForScene(), newObject);
I'm beginign to think that I shoud create each text object as separeted object and save it different list. Ofcours, i would have to update it then, whenever it's object moved.
Try this:
QGraphicsTextItem* gti = new QgraphicsTextItem("text");
gti->setFont(QFont("Arial", 18));
// this is important
gti->setFlag(QGraphicsTextItem::ItemIgnoresTransformations, true);
scene->addItem(gti);
The QGraphicsItem::ItemIgnoresTransformations flag prevents your graphics item to be scaled when you scale your view (QGraphicsView).
That means that you need a separated item for rendering text. But it can be a child item of your rectangle item.
I resolved this with QGraphicsTextItem's poitner as class's attribute.
QGraphicsTextItem* GTI;
I initialzie it in constructor:
GTI_Description = new QGraphicsTextItem(this->toStringForScene());
and then I call function to updated it's X and Y:
void updateTextPosition()
{
GTI->setX( this->x() );
GTI->setY( this->y() );
}
and to add it to the scene:
addTextToScene(DragScene* _scene)
{
updateDescriptionPosition();
_scene->addItem(GTI_GTI);
_scene->update();
}
Then i just call updateTextPosition() whenerver I change positions (in my mouseRelease event's handler).

QFontMetrics returns inaccurate results

I have a custom delegate in my QTableWidget to hightlight matches if a user searches something. Unfortunately the rectangle position does often not really fit This happens on some characters or phrases or depending on the number of matches or the size of the leading string. I can't find something specific causing this. Here is one example: .
This is my paint routine (a bit messy from all the trial and error trying to fix the issue):
void custom_delegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const{
const QTableWidget* table_widget = qobject_cast<const QTableWidget*>(qstyleoption_cast<const QStyleOptionViewItemV3*>(&option)->widget);
const int cell_width = table_widget->columnWidth(index.column());
// basic table cell rectangle
QRect rect_a = option.rect;
// adjust rectangle to match text begin
QStyle* style;
if(table_widget != 0){
style = table_widget->style();
}else{
style = QApplication::style();
}
const int text_horizontal_margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, 0, table_widget) + 1;
QRect rect_b = rect_a.adjusted(text_horizontal_margin, 0, -text_horizontal_margin, 0);
// adjust rectangle to match text height
QFont cell_font = index.model()->data(index, Qt::FontRole).value<QFont>();
cell_font.setPointSize(9);
QFontMetrics fm(cell_font);
const int height = fm.height();
rect_b.setY(rect_a.y() + (rect_a.height() - height)/2);
rect_b.setHeight(height);
// displayed text
std::string cell_text = qstrtostr(fm.elidedText(index.model()->data(index, Qt::DisplayRole).toString(),Qt::ElideRight,rect_a.width()));
int found_pos = find_ci(cell_text, this->filter_string, 0);
int old_pos = 0;
int found_width = 0;
QRect rect_c = rect_b;
// find occurence of filter string in cell_text
while(found_pos != std::string::npos){
std::string front = cell_text.substr(0, found_pos);
rect_c.setX(rect_b.x() + fm.tightBoundingRect(QString::fromStdString(front)).width());
rect_c.setWidth(fm.width(QString::fromStdString(cell_text.substr(found_pos, this->filter_string.size()))));
painter->fillRect(rect_c, Qt::yellow);
old_pos = found_pos+1;
found_pos = find_ci(cell_text, this->filter_string, old_pos);
}
}
Notes: filter_string is the string searched for, find_ci is just a wrapper for std::string::find including case-insensitivity but not important here as this test case is fully lower-case and I use std::string for non-qt stuff.
Edit: For the width calculation I tried fm.tightBoundingRect().width(), fm.boundingRect.width() and fm.width() with different but never correct results.
I use Qt 5.2
In my case I got the desired result with the following hack:
auto initialRect = fm.boundingRect(text);
auto improvedRect = fm.boundingRect(initialRect, 0, text);
It's not entirely clear why the other overload of boundingRect returns the correct result, but it may be just accidental, because as the documentation states:
The bounding rectangle returned by this function is somewhat larger than that calculated by the simpler boundingRect() function. This function uses the maximum left and right font bearings as is necessary for multi-line text to align correctly. Also, fontHeight() and lineSpacing() are used to calculate the height, rather than individual character heights.
The width method you propose also will return larger result, but it does not seem correct, as it should be used only when you need a position for a next word:
[...] width() returns the distance to where the next string should be drawn.
Also, sometimes it matters whether you pass the result of painter.device() to QFontMetrics constructor.