PDA

View Full Version : Calculating best bounding box for text



xtal256
1st October 2010, 04:31
I wanted to make a tooltip "bubble" or "balloon" for my app which can pop up in my app's status bar.
Since it appears that the only way to get such a widget is in the Window's taskbar (using QSystemTrayIcon), i decided to make my own.

I have it mostly working, but one thing i do not know how to do is make it resize so that it fits all the text but keeps a decent aspect ratio (i.e. does not get extremely long or tall).

This is what i have so far:
http://img214.imageshack.us/img214/5279/balloonx.png

I thought i could get the bounding box of the text within an initial rect, with Qt::TextWordWrap set, then check it's aspect ratio. If it is not right try again with a bigger box until i get one the right size.
I came up with this code:

QRect findBestSize(const QFontMetrics& fontMetrics, const String& message) {
QRect rect = fontMetrics.boundingRect(0, 0, 100, 50,
Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, message);
float ratio = float(rect.width())/float(rect.height());
while(ratio < 1.5 || ratio > 2.0) {
rect.adjust(0, 0, 10, 5);
rect = fontMetrics.boundingRect(0, 0, rect.width(), rect.height(),
Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, message);
ratio = float(rect.width())/float(rect.height());
}
return rect;
}

But it doesn't work. And i am not sure what boundingRect() actually does. What is the rect that you give it and what rect does it return?
I am not even sure how my code should work, but i am thinking that there are many different sized rects which can contain the text (i.e. such that the text wraps to two lines, three lines, etc) and i want the rect which is about 3:1 ratio.


Here is the full code below:


// File: BalloonTip.h

#ifndef BALLOON_TIP_H
#define BALLOON_TIP_H

class BalloonTip : public QWidget {
private:
static const QBrush backgroundBrush;
static const QPen outlinePen;
static const int textPadding = 6;
static const int arrowHeight = 20; // That triangle bit at the bottom
QTimer expireTimer;
QWidget* owner; // TODO: think of a better name.
public:
BalloonTip();
~BalloonTip() {}

void setOwner(QWidget* widget) { owner = widget; }
void showMessage(const String& message, int timeout/*ms*/);
void showMessage(QWidget* widget, const String& message, int timeout/*ms*/) {
setOwner(widget); showMessage(message, timeout);
}
protected:
void paintEvent(QPaintEvent*);
};

#endif // BALLOON_TIP_H



/************************************************** ************/

// File: BalloonTip.cpp

#include "BalloonTip.h"

const QBrush BalloonTip::backgroundBrush(QColor(255, 255, 225));
const QPen BalloonTip::outlinePen(Qt::black, 1);

BalloonTip::BalloonTip() :
QWidget(NULL, Qt::ToolTip | Qt::FramelessWindowHint),
expireTimer(), owner(NULL) {
setAttribute(Qt::WA_TranslucentBackground);
expireTimer.setSingleShot(true);
connect(&expireTimer, SIGNAL(timeout()), this, SLOT(hide()));
}

/* TODO: Get this working. What it should do is start at a minimum size,
100,50 and increase the bounding box until all the text fits in AND
the aspect ration is between 1.5 and 2.0
QRect findBestSize(const QFontMetrics& fontMetrics, const String& message) {
QRect rect = fontMetrics.boundingRect(0, 0, 100, 50,
Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, message);
float ratio = float(rect.width())/float(rect.height());
while(ratio < 1.5 || ratio > 2.0) {
rect.adjust(0, 0, 10, 5);
rect = fontMetrics.boundingRect(0, 0, rect.width(), rect.height(),
Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, message);
ratio = float(rect.width())/float(rect.height());
}
return rect;
}*/

void BalloonTip::showMessage(const String& message, int timeout/*ms*/) {
if (!owner) return;

QPoint ownerPos = owner->mapToGlobal(QPoint(0,0));
int width = (textPadding*2)+200; // Hard-code size for now
int height = (textPadding*2)+60+arrowHeight;
int x = ownerPos.x() - (width-15-(owner->width()/2));
int y = ownerPos.y() - height;

setWindowTitle(message);
move(x, y); resize(width, height);
expireTimer.start(timeout);
show();
update(); // Force redraw if it is already showing
}

/*------------------------------------------------------------------+
| Do custom painting. The entire rect is filled with the background |
| colour and the clip region is used to make an outline path. |
+------------------------------------------------------------------*/
void BalloonTip::paintEvent(QPaintEvent*) {
QPainter painter(this);
const int roundness = 12;
const int rectBase = height()-arrowHeight;

painter.setRenderHint(QPainter::Antialiasing);

QPoint p1(width()-35, rectBase-2);
QPoint p2(width()-15, height());
QPoint p3(width()-15, rectBase-2);
QPolygon triangle;
triangle << p1 << p2 << p3;

// TODO: This doesn't look quite right when drawing the bottom
// triangle bit. I need to erase the border of the rectangle
// in the triangle area, but anti-aliasing means that the triangle's
// border then slighly overlaps.
painter.setBrush(backgroundBrush);
painter.setPen(outlinePen);
painter.drawRoundedRect(1, 1, width()-2, rectBase-2, roundness, roundness);
painter.setPen(Qt::transparent);
painter.drawPolygon(triangle);
painter.setPen(outlinePen);
painter.drawLine(p1, p2);
painter.drawLine(p2, p3);

painter.drawText(textPadding, textPadding,
width()-textPadding, rectBase-textPadding,
Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap,
windowTitle());
}


Can anyone help me?
thanks

Lykurg
1st October 2010, 05:50
The problem seems to be that you alter the rect which is given back from boundingRect. I would do it like that: Start with a fix width (= minimum widht), use boundingRect to get the hight for the with. If it is not in the ratio you wont only adjust the with and let the height recalculated. Not tested, but:
int width = 100;
QRect rect = fontMetrics.boundingRect(0, 0, width, 0,
Qt::AlignLeft | Qt::TextWordWrap, message);
float ratio = float(rect.width())/float(rect.height());
while(ratio < 1.5 || ratio > 2.0) {
if (ratio < 1.5)
width += 15;
else
width -= 15;
rect = fontMetrics.boundingRect(0, 0, width, 0,
Qt::AlignLeft | Qt::TextWordWrap, message);
ratio = float(rect.width())/float(rect.height());
}

xtal256
1st October 2010, 07:38
The problem seems to be that you alter the rect which is given back from boundingRect.
Yeah, i was trying to reuse that object by increasing it's dimensions and passing it back into boundingRect.


I would do it like that: Start with a fix width (= minimum widht), use boundingRect to get the hight for the with. If it is not in the ratio you wont only adjust the with and let the height recalculated. Not tested, but:
...
That looks more like what i want. And it makes sense to only adjust the width and have the height calculated from that. I will try this code later.

Thanks!