QFontMetrics returns inaccurate results - c++

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.

Related

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.

Firemonkey: Shrink text font to fit in TLabel

I am attempting to lower the font size of a TLabel if its text is to large to fit in the confines of the label. I didn't see any properties I could set on the label to achieve this, so I have tried writing my own method. My method works by using TCanvas.TextWidth to measure the width of the text in a label, and shrink the font until the width of the text fits within the width of the label.
void __fastcall ShrinkFontToFitLabel( TCanvas * Canvas, TLabel * Label )
{
float NewFontSize = Label->Font->Size;
Canvas->Font->Family = Label->Font->Family;
Canvas->Font->Size = NewFontSize;
while( Canvas->TextWidth( Label->Text ) > Label->Width && NewFontSize > MinimumFontSize )
{
NewFontSize -= FontSizeDecrement;
Canvas->Font->Size = NewFontSize;
}
Label->Font->Size = NewFontSize;
}
This works some of the time, however other times it does not shrink the font near enough. It seems as if the value I get from calling Canvas->TextWidth is a lot of times, much smaller than the number of pixels wide the label actually needs to be in order to fit the text.
Am I using Canvas->TextWidth incorrectly? Is there a better way to calculate the width of a string, or to re-size the font of a TLabel so its text fits within its demensions?
Edit:
In this case, I am passing in to my function, the TCanvas that my label is sitting in. I have tried using that TCanvas as well as Label->Canvas. Both give me the same number for text width, and both are short of the actual value in pixels needed to display the whole string.
The following code is taken from code that works in an FMX application, modified slightly to remove arrays that are being iterated through and declaring a variable locally to the function. It is being run in a TForm method. Canvas here is the Form's Canvas. You can see that I'm using "- 35" at one point - this might be because the numbers weren't quite right.
double InitialFontSize = 30;
Canvas->Font->Size = InitialFontSize;
StoryHeadlineLabel->Font->Size = InitialFontSize;
bool fits = false;
do
{
double widthA = Canvas->TextWidth (StoryHeadlineLabel->Text);
if (widthA > StoryHeadlineLabel->Width - 35)
{
StoryHeadlineLabel->Font->Size --;
Canvas->Font->Size --;
}
else
fits = true;
if (StoryHeadlineLabel->Font->Size < 6)
fits = true;
} while (!fits);

Size QGraphicsItem based on string length

I'm looking for the most effective way to size a QGraphicsItem based on the length of a given QString, so that the text is always contained within the QGraphicsItem's boundaries. The idea is to keep the QGraphicsItem as small as possible, while still containing the text at a legible size. Wrapping onto multiple lines at a certain width threshold would be ideal as well. For example,
TestModule::TestModule(QGraphicsItem *parent, QString name) : QGraphicsPolygonItem(parent)
{
modName = name;
// what would be the best way to set these values?
qreal w = 80.0;
qreal h = 80.0;
QVector<QPointF> points = { QPointF(0.0, 0.0),
QPointF(w, 0.0),
QPointF(w, h),
QPointF(0.0, h) };
baseShape = QPolygonF(points);
setPolygon(baseShape);
}
void TestModule::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
QBrush *brush = new QBrush(Qt::gray, Qt::SolidPattern);
painter->setBrush(*brush);
painter->drawPolygon(baseShape);
painter->drawText(QPointF(0.0, 40.0), modName);
}
what code could I add to the constructor to make my requirement work? Setting the width based on the total length of the string, making assumptions for how much pixel space each character takes up is the most obvious solution, but I'm looking for something a little more elegant. Any ideas? Thank you in advance for any help.
The QFontMetrics class has a function called boundingRect which takes the string that you're wanting to print and returns the QRect for the string, based on the QFont that you used to initalise QFontMetrics.
If you want to wrap, then you'd need to work out the maximum number of words in your string that will allow boundingRect to return a QRect that fits within your QGraphicsItem's boundingRect.
Take a look to QFontMetrics
You can ask your widget for the font
And check this snippet from QFontMetrics docs
QFont font("times", 24);
QFontMetrics fm(font);
int pixelsWide = fm.width("What's the width of this text?");
int pixelsHigh = fm.height();
Edit: As Merlin said in comment, use
QRect QFontMetrics::boundingRect ( const QString & text ) const
So:
int pixelsWide = fm.boundingRect("What's the width of this text?").width();

How do I prevent custom font in Cocos2d from appearing cut off

I am using a custom font called KomikaTitle. In some cases the font appears cut off on the left in the first character. This doesn't happen when I use a native font such as Arial.
The following is the code I am using:
scoreDisplayLabel = [CCLabelTTF labelWithString:#"0" dimensions:CGSizeMake(200,30) hAlignment:UITextAlignmentLeft fontName:#"KomikaTitle" fontSize:18];
scoreDisplayLabel.color = (ccc3(r,b,g));
[self addChild:scoreDisplayLabel z:2];
[scoreDisplayLabel setPosition:ccp(115,wins.height-73)];
How do I prevent this from happening? I am attaching a screenshot of the issue.
I tried messing around as suggested in http://www.cocos2d-iphone.org/forums/topic/custom-font-being-cut-off/, but no luck.
Thanks guys!
This maybe isn't a real answer, but I had the same problem with that font in an old cocos2d project I made. Just just added an extra space and a row.
This may or may not be related, but according to this source you have to include the file extension of your font. Where you have
fontName:#"KomikaTitle"
it should be
fontName:#"KomikaTitle.ttf"
for example.
If there are any android users out there using cocos2dx, this is not necessarily an easy problem to solve, but it is doable once you go down the rabbit hole. It does require editing the Cocos2dxBitmap.java file, which means that any changes made could be overrided by an update. Basically, the methods that are used to measure text are, while not incorrect, inadequate.
First, we need to add a new variable to the TextProperty
private final int mX;
Next, replace the computeTextProperty code with the following:
private static TextProperty computeTextProperty(final String pString, final int unusedWidth, final int unusedHeight, final Paint pPaint) {
final FontMetricsInt fm = pPaint.getFontMetricsInt();
final int h = (int) Math.ceil(fm.bottom - fm.top);
int maxContentWidth = 0;
final String[] lines = Cocos2dxBitmap.splitString(pString, 0,
0, pPaint);
/* Compute the max width. */
int temp = 0;
float left = 0;
for (final String line : lines) {
//get a path from text
Path path = new Path();
pPaint.getTextPath(line, 0, line.length(), 0, 0, path);
RectF bounds = new RectF();
path.computeBounds(bounds, true);
temp = (int) FloatMath.ceil(bounds.width());
//if the text extends to the left of 0
if (bounds.left < left) {
left = bounds.left;
}
if (temp > maxContentWidth) {
maxContentWidth = temp;
//extend the width to account for text rendered to the left of 0
if (left < bounds.left) {
maxContentWidth += (int) FloatMath.ceil(Math.abs(left));
}
}
}
left = Math.abs(left);
return new TextProperty(maxContentWidth, h, lines, (int) FloatMath.ceil(left));
}
What has basically happened is that we have used information returned by the text path to get if the left bound is less than 0, which would mean it would be rendered outside the bitmap. We also extend the width when there are multiple lines of text, as we are going to shift everything to match the left bounds, we need the right bounds shifted too.
Finally, replace computeX with
private static int computeX(final String pText, final int pMaxWidth,
final int pHorizontalAlignment, final int pX) {
int ret = 0;
int expectedWidth = pX + pMaxWidth;
switch (pHorizontalAlignment) {
case HORIZONTALALIGN_CENTER:
ret = expectedWidth / 2;
break;
case HORIZONTALALIGN_RIGHT:
ret = expectedWidth;
break;
case HORIZONTALALIGN_LEFT:
ret = pX;
default:
break;
}
return ret;
}
You'll have to do all the hookups yourself, but this will provide the most accurate text rendering.

QPainter::drawText, get bounding boxes for each character

I'm using QPainter to draw multiline text on QImage. However, I also need to display a colored rectangle around each character's bounding box.
So I need to know the bounding box that each character had when being drawn.
For example, for
painter.drawText(QRect(100, 100, 200, 200), Qt::TextWordWrap, "line\nline2", &r);
I would need to get 10 rectangles, taking into account newlines, word-wrap, tabs, etc.
For example, the rectangle of the second 'l' would be below the rectangle of the first 'l', instead of being to the right of 'e', because of the newline.
Something like the coordinates of the red rectangles in this picture (I've put them by hand so they're not really the correct positions):
This may not be the best solution, but it's the best one I can think of.
I believe you will have to "do it yourself". That is, instead of drawing a block of text, draw each character one at a time. Then you can use QFontMetrics to get the bounding box of each character.
It's a little work, but not too bad. Something like (pseudo code, not code):
QFontMetrics fm(myFont, paintDevice);
int x = startX;
int y = startY;
for (unsigned int i = 0; i < numChars; i++)
{
char myChar = mystr[i]; // get character to print/bound
QRect rect = fm.boundingRect( myChar ); // get that char's bounding box
painter.drawText(x, y, Qt::TextWordWrap, mystr[i], &r); // output char
painter.drawRect(...); // draw char's bounding box using 'rect'
x += rect.width(); // advance current position horizontally
// TODO:
// if y > lineLen // handle cr
// x = startX;
// y += line height
}
Check out QFontMetrics, it has a number of different methods for getting bounding boxes, minimum bounding boxes, etc.
QFontMetrics 4.7
Ahhh... I see now that the overload you're using returns the actual bounding rect. You can just use that and skip the QFontMetrics if you like - otherwise the overall algorithm is the same.
You can retrieve the bounding boxes of individual characters with QFontMetrics::boundingRect(QChar), but they have to be rendered at an offset (QFontMetrics::ascent from the top as well as QFontMetrics::width of the preceding characters from the left) because they are relative to the font’s base line and not to the bottom of the bounding box of the complete string.
Several lines also have to be handled separately.
QFontMetrics::lineSpacing give you their offset.
QPainter painter(this);
painter.setFont(QFont("Arial", 72));
auto pen = painter.pen();
QString text{"line\nline2\ngg\n`"};
QRect boundingRect;
painter.drawText(rect(), Qt::AlignLeft | Qt::AlignTop, text, &boundingRect);
painter.drawRect(boundingRect.adjusted(0, 0, -pen.width(), -pen.width()));
pen.setColor(Qt::red);
painter.setPen(pen);
const auto lines = text.split('\n');
const auto fm = painter.fontMetrics();
for (int linei = 0; linei < lines.size(); ++linei) {
const auto & line = lines[linei];
for (int chi = 0; chi < line.size(); ++chi) {
const auto bounds = fm.boundingRect(line[chi]);
const auto xoffset = bounds.x() + fm.width(line, chi);
const auto lineOffset = linei * fm.lineSpacing() + fm.ascent();
const auto yoffset = lineOffset + bounds.y();
painter.drawRect(QRect{xoffset, yoffset, bounds.width(), bounds.height()});
}
}
results in
which, sadly – isn’t perfect though.