Inside of my QGraphicsRectItem::paint(), I am trying to draw the name of the item within its rect(). However, for each of the different items, they can be of variable width and similarly names can be of variable length.
Currently I am starting with a maximum font size, checking if it fits and decrementing it until I find a font size that fits. So far, I haven't been able to find a quick and easy way to do this. Is there a better, or more efficient way to do this?
Thanks!
void checkFontSize(QPainter *painter, const QString& name) {
// check the font size - need a better algorithm... this could take awhile
while (painter->fontMetrics().width(name) > rect().width()) {
int newsize = painter->font().pointSize() - 1;
painter->setFont(QFont(painter->font().family(), newsize));
}
}
Johannes from qtcentre.org offered the following solution:
float factor = rect().width() / painter->fontMetrics().width(name);
if ((factor < 1) || (factor > 1.25))
{
QFont f = painter->font();
f.setPointSizeF(f.pointSizeF()*factor);
painter->setFont(f);
}
I gave it a try in my program and so far, it seems to work quite well. I like it because it produces results in one pass, but it assumes that font width scales like its height.
http://www.qtcentre.org/threads/27839-For-Qt-4-6-x-how-to-auto-size-text-to-fit-in-a-specified-width
You could have a QGraphicsTextItem as a child of the rect item, measure the text item's width and then scale the text item (setTransform()) to fit into the rect item's width (and height).
Here is my code the fit (in heigth) a text, works quite well (error < 2% I guess)
void scalePainterFontSizeToFit(QPainter &painter, QFont &r_font, float _heightToFitIn)
{
float oldFontSize, newFontSize, oldHeight;
// Init
oldFontSize=r_font.pointSizeF();
// Loop
for (int i=0 ; i<3 ; i++)
{
oldHeight = painter.fontMetrics().boundingRect('D').height();
newFontSize = (_heightToFitIn / oldHeight) * oldFontSize;
r_font.setPointSizeF(newFontSize);
painter.setFont(r_font);
oldFontSize = newFontSize;
//qDebug() << "OldFontSize=" << oldFontSize << "HtoFitIn=" << _heightToFitIn << " fontHeight=" << oldHeight << " newFontSize=" << newFontSize;
}
// End
r_font.setPointSizeF(newFontSize);
painter.setFont(r_font);
}
void myClass::adaptFontSize(QPainter * painter, int flags, QRectF drawRect, QString text){
int flags = Qt::TextDontClip|Qt::TextWordWrap; //more flags if needed
QRect fontBoundRect =
painter->fontMetrics().boundingRect(drawRect.toRect(),flags, text);
float xFactor = drawRect.width() / fontBoundRect.width();
float yFactor = drawRect.height() / fontBoundRect.height();
float factor = xFactor < yFactor ? xFactor : yFactor;
QFont f = painter->font();
f.setPointSizeF(f.pointSizeF()*factor);
painter->setFont(f);
}
or more accurate but greedy
void myClass::adaptFontSize(QPainter * painter, int flags, QRectF rect, QString text, QFont font){
QRect fontBoundRect;
fontBoundRect = painter->fontMetrics().boundingRect(rect.toRect(),flags, text);
while(rect.width() < fontBoundRect.width() || rect.height() < fontBoundRect.height()){
font.setPointSizeF(font.pointSizeF()*0.95);
painter->setFont(font);
fontBoundRect = painter->fontMetrics().boundingRect(rect.toRect(),flags, text);
}
}
Divide an conquer:
You could reduce the number of passes in your brute force method: lets say your preferred (maximum) font size is 40 and you have a minimum font size of 0
if(40 == false && 0 == true)
- 20=true //divide possibilities in half with each guess
- 30=false
- 25=true
- 27=true
- 28=false
- so 27 wins
In what ways is this better?
this took 6 guesses instead of 13, and even if 20, 12, or 39 were the right answer it would always take about 6 guesses. so not only is it fewer guesses most of the time, it's more consistent which is important for user experience.
i think the number of guess it takes when dividing ints by half each time is the square root of the range you are looking in plus one. Math.sqroot(40-0) + 1 (That's just a guess, feel free to correct me.) your minimum font size is probably not 0 so increasing this would speed up the search for an answer.
Illustration:
It's like playing Guess Who, players who ask "does your name have an A" and cuts the possibilities in half no matter what you answer typically finds the answer faster than the player who asks about 1 character each turn "is your name Sam" "is your name Alex"
Alternative: starting with a good guess, then testing for accuracy I would also promote working in some logic to use the result provided by Daren's answer using fontMetrics as a good starting guess and then test it, if it fits test +2 if it doesn't fit test -2; if the new test fits test the 1 you skipped and you will know your answer, if not try moving another 2 and so on, but ideally the fontMetrics answer isn't more than 4 far off...
I suspect this will produce the fastest average results of actual use cases.
assuming you want an int and assuming the font metrics inaccuracies are minimal this will probably only take 2 or 3 guesses.
Unfortunately, no. There's no easy solution to this. The most obvious improvement, performance-wise would be calculate and cache the needed font size when the text is changed instead of recalculating it in each paintEvent. Another improvement would be to try to estimate the correct size based on the proportions of the bounding rect. Making and testing estimates should get you to correct size much quicker than simply decrementing the point size by one each iteration.
This depends on the range over which you expect your font sizes to vary. If the range is large, incrementing by one may take a long time. If it's just a matter of several point sizes, speed probably won't be an issue.
If the range is large, another approach would be to add a larger interval, instead of '1'. If you exceed the desired size during one step, decrease the interval by, say, half. You'll bounce back and forth across the optimum size by smaller and smaller amounts each time; when the difference between two successive intervals is small, you can quit.
This is similar to the approach used in root finding. It may be overkill, since the size of fonts that will be acceptable to display in a given application is likely to be fairly narrow, and the brute force approach you're already using won't consume much time.
Follow function based on the answer and comment to the answer allow optimizing font size to fit text in specified rectangle:
QFont optimizeFontSizeToFitTextInRect(QPainter * painter, QRectF drawRect, QString text, int flags = Qt::TextDontClip|Qt::TextWordWrap, double goalError = 0.01, int maxIterationNumber=10){
painter->save();
QRect fontBoundRect;
QFont font;
double minError = std::numeric_limits<double>::max();
double error = std::numeric_limits<double>::max();
int iterationNumber=0;
while((error > goalError) && (iterationNumber<maxIterationNumber)){
iterationNumber++;
fontBoundRect = painter->fontMetrics().boundingRect(drawRect.toRect(),flags, text);
double xFactor = drawRect.width() / fontBoundRect.width();
double yFactor = drawRect.height() / fontBoundRect.height();
double factor;
if (xFactor<1 && yFactor<1) {
factor = std::min(xFactor,yFactor);
} else if (xFactor>1 && yFactor>1) {
factor = std::max(xFactor,yFactor);
} else if (xFactor<1 && yFactor>1) {
factor = xFactor;
} else {
factor = yFactor;
}
error = abs(factor-1);
if (factor > 1 ) {
if (error < minError) {
minError = error;
} else {
break;
}
}
font = painter->font();
font.setPointSizeF(font.pointSizeF()*factor);
painter->setFont(font);
}
painter->restore();
return font;
}
来源:https://stackoverflow.com/questions/2202717/for-qt-4-6-x-how-to-auto-size-text-to-fit-in-a-specified-width