Qt5 C++ resize font based on label width - c++

I have written what I think is a pretty good font resize algorithm that I use on labels in a QGridLayout. The algorithm works rather well on its own. However, the first time I run the algorithm, the layout hasn't been painted yet, and as such, the grid widgets which own the labels haven't been sized to the layout, which means my width() call is wrong, though I have assigned the label text prior to the resize (which doesn't matter). My question is, what is the best way to know the bounding rectangles have been created to fit the layout so my calculation is on the actual label width?
Note, this is Qt5.7, so I know that fm.width() is obsolete, but that's what's available in Raspbian.
My app is reading Sonos metadata and showing it on a 7" RPi display. I'm resizing the metadata that is too long to fit in the layout grids. So, I have a QStackedLayout with a set of layouts to show (clock when Sonos isn't playing, metadata when it is), and I frequently poll the a local Sonos server to find out if there is metadata to show. This works well. What doesn't happen is that on the first time the Sonos metadata layout is shown, the QGridLayout hasn't actually been laid out yet (I believe), so the labels are too big. At some point after the first time I fill in the metadata, the grid layout gets "shown" and the labels are then the correct size. The problem is, by then, it's all done setting metadata and the labels look funny in some cases.
int pointSize = FontSize::Default;
for (auto label : m_labels) {
QFont f = label->font();
if (label->accessibleName() == "title")
pointSize = FontSize::Title;
QFontMetrics fm(f);
if (fm.width(label->text()) > label->width()) {
float factor = (float)label->width() / (float)fm.width(label->text());
if (factor <= .6) {
factor = .6;
}
f.setPointSizeF((f.pointSize() * factor) * .9);
qDebug() << __FUNCTION__ << ": label width:" << label->width();
qDebug() << __FUNCTION__ << ": font width:" << fm.width(label->text());
qDebug() << __FUNCTION__ << ": Calculated font scaling factor to be" << (float)(factor * .9);
qDebug() << __FUNCTION__ << ": Reset font size for text\"" << label->text() << "\" to" << f.pointSize();
}
else
f.setPointSize(pointSize);
label->setFont(f);
}
On an 800x480 display, this results in the first event label width being wrong
calculateLabelFontSize : label width: 640
calculateLabelFontSize : font width: 2051
The label width the next time it's called would end up being correct with a width of 584. However, because I don't always call it a second time, and because sometimes, the Sonos metadata hiccups and causes the display to revert, it may always be wrong, or it may just be right the first time no matter what.
I've considered trying to overload my gui app paintEvent(QPaintEvent *event), but the QPaintEvent class doesn't give me a way to determine which widget called for the event, so I can't just ignore the event until I want it. At least, not that I have determined, so if that's possible, please let me know.
I've tried overloading showEvent, but that doesn't run except when the frame is first shown, so no help there.
I've considered subclassing QGridLayout to simply overload the paintEvent() and use that, but I do have an issue with that solution. There is a progress bar in this layout, which means that the paintEvent is going to fire every half second as I get metadata from the Sonos server. Since I don't know if the paintEvent is firing for the time update, or for the text being set, I get a lot of resize attempts.
I've also simply run the algorithm a second time on the next event, but it's a really weird looking graphical hiccup when the fonts realign, and I don't like it.
Last up, I may just subclass QLabel, simply to overload paintEvent, and use that custom label just for the labels that would be resizable. However, is the label in the layout painted before or after the container it lives in inside the layout? I'll be testing this today I think, but I'm not convinced it would work.
I figure this has a much easier solution though. I just can't figure it out.

I borrowed code from https://github.com/jonaias/DynamicFontSizeWidgets which provided the resize algorithm. It's nice, but iterative. I have been tinkering with a math based solution to calculate instead of just trying to fit by resizing until it works. However, that solution still doesn't work well enough to use. This isn't slow, and does what is implied. It resizes the font until it fits.
This all is done in paintEvent() because using resizeEvent() may result in either an infinite resize loop or the layout may resize itself and suddenly everything looks shifted/squished/stretched. Doing it in paintEvent means it's only called once every time you update the text, but not otherwise.
I may come back to this to try to find a math based solution. I still think this isn't the correct way, but I found something that worked well enough for now.
NewLabel.h
#ifndef NEWLABEL_H
#define NEWLABEL_H
#include <QtCore/QtCore>
#include <QtWidgets/QtWidgets>
#include <cmath>
class NewLabel : public QLabel
{
Q_OBJECT
public:
explicit NewLabel(const QString &text, QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
explicit NewLabel(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
virtual ~NewLabel() {}
void setDefaultPointSize(int p)
{
m_defaultPointSize = static_cast<float>(p);
}
protected:
void paintEvent(QPaintEvent *e) override;
private:
float getWidgetMaximumFontSize(QWidget *widget, QString text);
float m_defaultPointSize;
};
#endif
NewLabel.cpp
#define FONT_PRECISION (0.5)
NewLabel::NewLabel(const QString &text, QWidget *parent, Qt::WindowFlags f) : QLabel(text, parent, f)
{
m_defaultPointSize = 12;
}
NewLabel::NewLabel(QWidget *parent, Qt::WindowFlags f) :
QLabel(parent, f)
{
m_defaultPointSize = 12;
}
void NewLabel::paintEvent(QPaintEvent *e)
{
QFont newFont = font();
float fontSize = getWidgetMaximumFontSize(this, this->text());
if (fontSize < m_defaultPointSize) {
newFont.setPointSizeF(fontSize);
setFont(newFont);
}
QLabel::paintEvent(e);
}
float NewLabel::getWidgetMaximumFontSize(QWidget *widget, QString text)
{
QFont font = widget->font();
const QRect widgetRect = widget->contentsRect();
const float widgetWidth = widgetRect.width();
const float widgetHeight = widgetRect.height();
QRectF newFontSizeRect;
float currentSize = font.pointSizeF();
float step = currentSize/2.0;
/* If too small, increase step */
if (step<=FONT_PRECISION){
step = FONT_PRECISION*4.0;
}
float lastTestedSize = currentSize;
float currentHeight = 0;
float currentWidth = 0;
if (text==""){
return currentSize;
}
/* Only stop when step is small enough and new size is smaller than QWidget */
while(step>FONT_PRECISION || (currentHeight > widgetHeight) || (currentWidth > widgetWidth)){
/* Keep last tested value */
lastTestedSize = currentSize;
/* Test label with its font */
font.setPointSizeF(currentSize);
/* Use font metrics to test */
QFontMetricsF fm(font);
/* Check if widget is QLabel */
QLabel *label = qobject_cast<QLabel*>(widget);
if (label) {
newFontSizeRect = fm.boundingRect(widgetRect, (label->wordWrap()?Qt::TextWordWrap:0) | label->alignment(), text);
}
else{
newFontSizeRect = fm.boundingRect(widgetRect, 0, text);
}
currentHeight = newFontSizeRect.height();
currentWidth = newFontSizeRect.width();
/* If new font size is too big, decrease it */
if ((currentHeight > widgetHeight) || (currentWidth > widgetWidth)){
//qDebug() << "-- contentsRect()" << label->contentsRect() << "rect"<< label->rect() << " newFontSizeRect" << newFontSizeRect << "Tight" << text << currentSize;
currentSize -=step;
/* if step is small enough, keep it constant, so it converge to biggest font size */
if (step>FONT_PRECISION){
step/=2.0;
}
/* Do not allow negative size */
if (currentSize<=0){
break;
}
}
/* If new font size is smaller than maximum possible size, increase it */
else{
//qDebug() << "++ contentsRect()" << label->contentsRect() << "rect"<< label->rect() << " newFontSizeRect" << newFontSizeRect << "Tight" << text << currentSize;
currentSize +=step;
}
}
return lastTestedSize;
}

Related

How to automatically increase/decrease text size in label in Qt

I have a Qt application where I have a textedit and a label. When a user presses the button, the textedit text should be displayed on label. For the label I have set few properties like word wrap is enabled and horizontal and vertically it is aligned center. Below is the screenshot :
Now I have to automatically adjust the size of the text in label so that if someone enters a large string, then it should fit inside the label, that means size of text should decrease. And if the text string is small, then size should increase automatically to fill up the complete label. Currently if I am typing it the large string, it looks like something:
As you can see, in the above image, text is moving out of the label. It should remain inside the label.
How to detect in application if the text is moving out of the label height & width. Then how to reduce the text size. I want the size to automatically increase if the string is small and decrease it string is large to fill up the complete label. Is there any class or something provided in QT. Any help or example please. Thanks.
EDIT: With the below code I am able to reduce the size of text to fit inside the label width but not able to make the text multi line.
QString string = ui->textEdit->toPlainText(); //Getting data from textEdit
ui->label->setAlignment(Qt::AlignCenter); //Aligning label text to center
QFont f("Arial",50); //Setting the default font size to 50
QFontMetrics fm(f);
ui->label->setFont(f); //Setting the font to the label
int width = fm.width(string); //Getting the width of the string
int size;
while(width >= 870) //870 is the max width of label
{
size = ui->label->font().pointSize()-1; //Reduce font size by 1
QFont newFont("Arial",size);
QFontMetrics nfm(newFont);
ui->label->setFont(newFont); //Set the new font with new size
width = nfm.width(string); //Get the new width
}
ui->label->setText(string);
You (S. Andrew) solved it a little bit different like I proposed (just a statement but not critics). You did the word wrapping by yourself.
I wrote a minimal complete application to check how the Qt internal word wrapping can be used for your problem:
// standard C++ header:
#include <iostream>
#include <string>
// Qt header:
#include <QApplication>
#include <QBoxLayout>
#include <QFrame>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QMainWindow>
#include <QStyle>
using namespace std;
class Label: public QLabel {
public:
void layout();
QRect documentRect(); // borrowed from QLabelPrivate
protected:
virtual void resizeEvent(QResizeEvent *pQEvent);
};
QRect Label::documentRect()
{
QRect rect = contentsRect();
int m = margin(); rect.adjust(m, m, -m, -m);
layoutDirection();
const int align
= QStyle::visualAlignment(layoutDirection(), QLabel::alignment());
int i = indent();
if (i < 0 && frameWidth()) { // no indent, but we do have a frame
m = fontMetrics().width(QLatin1Char('x')) / 2 - m;
}
if (m > 0) {
if (align & Qt::AlignLeft) rect.setLeft(rect.left() + m);
if (align & Qt::AlignRight) rect.setRight(rect.right() - m);
if (align & Qt::AlignTop) rect.setTop(rect.top() + m);
if (align & Qt::AlignBottom) rect.setBottom(rect.bottom() - m);
}
return rect;
}
void Label::layout()
{
// get initial settings
QString text = this->text();
QRect rectLbl = documentRect(); // wrong: contentsRect();
QFont font = this->font();
int size = font.pointSize();
QFontMetrics fontMetrics(font);
QRect rect = fontMetrics.boundingRect(rectLbl,
Qt::TextWordWrap, text);
// decide whether to increase or decrease
int step = rect.height() > rectLbl.height() ? -1 : 1;
// iterate until text fits best into rectangle of label
for (;;) {
font.setPointSize(size + step);
QFontMetrics fontMetrics(font);
rect = fontMetrics.boundingRect(rectLbl,
Qt::TextWordWrap, text);
if (size <= 1) {
cout << "Font cannot be made smaller!" << endl;
break;
}
if (step < 0) {
size += step;
if (rect.height() < rectLbl.height()) break;
} else {
if (rect.height() > rectLbl.height()) break;
size += step;
}
}
// apply result of iteration
font.setPointSize(size);
setFont(font);
}
void Label::resizeEvent(QResizeEvent *pQEvent)
{
QLabel::resizeEvent(pQEvent);
layout();
}
int main(int argc, char **argv)
{
cout << QT_VERSION_STR << endl;
// main application
#undef qApp // undef macro qApp out of the way
QApplication qApp(argc, argv);
// setup GUI
QMainWindow qWin;
QGroupBox qGBox;
QVBoxLayout qBox;
Label qLbl;
qLbl.setFrameStyle(Label::Box);
qLbl.setFrameShadow(Label::Sunken);
qLbl.setWordWrap(true);
qBox.addWidget(&qLbl, 1);
QLineEdit qTxt;
qBox.addWidget(&qTxt, 0);
qGBox.setLayout(&qBox);
qWin.setCentralWidget(&qGBox);
qWin.show();
// install signal handlers
QObject::connect(&qTxt, &QLineEdit::editingFinished,
[&qTxt, &qLbl]() {
QString text = qTxt.text();
qLbl.setText(text);
qLbl.layout();
});
return qApp.exec();
}
Compiled and tested with VS2013 / Qt 5.6 on Windows 10 (64 bit):
When playing around with this test application, I recognized that the text fits not everytimes perfectly into the QLabel. I tried to improve the code exchanging QRect rectLbl = rect(); with QRect rectLbl = contentsRect();. This made it better but still not perfect. It seems there is some finetuning necessary (where the development starts to become effort). (See update at end of text.)
Actually, it would not be necessary to derive QLabel. In my first implementation, layout() was a function with QLabel& and const QString& as parameters.
After I got the font size management working, I intended to consider resize events also. Googling a little bit, I found the solution to apply event filters. However, event filters are called before the event is processed but I need after. Finally, I decided to inherit QLabel and to overload QLabel::resizeEvent() to keep things simple.
Btw. I noticed it is even not necessary to set
height eventually to a very large value
as I suggested in a comment earlier. It seems that QFontMetrics::boundingRect(const QRect &rect, int flags, ...) increases the height automa[gt]ically to keep required width when Qt::TextWordWrap is enabled.
Update:
#annacarolina encouraged me to investigate a little bit deeper into this issue that font size is sometimes choosen to large. Some debugging in Label::layout() uncovered that sometimes computed rect looked like unwrapped text where visual output was wrapped. This made me suspiciuous about correctness of the rectLbl. Thus, I started in qlabel.cpp on woboq.org but actually the Qt forum QLabel: Resize font to contentsRect provided the final hint which leaded me to QLabelPrivate::documentRect() (actually again on woboq.org where I already had looked for enlightment). Thus, I added a method Label::documentRect() to my class. This makes results much better (although I'm not fully convinced about "perfect").
In the following code, I am making a logic where I am first getting all the words in the string. Then I am appending the words in QList<QString> data and checking if the width of the appended words is smaller than then width of the label. If the width goes above the width of label then I break it using \n. So in this way I made a list of the sub strings whose total width is around the width of the label and saved it in the List. Then I am calculating the width of sub strings stored in the list and then decreasing its font size till its total width is less than width of the label. After this I am displaying it on the label.
QList<QString> data;
CountWords Word;
ui->label->clear();
QString string = ui->textEdit->toPlainText(); //Getting data from textEdit
QStringList count = Word.GetWords(string); //Here I get the list of words in string
ui->label->setAlignment(Qt::AlignCenter); //Aligning label text to center
QFont f("Arial",50); //Setting the default font size to 50
QFontMetrics fm(f);
ui->label->setFont(f); //Setting the font to the label
int size,fontSize;
QString temp = ui->label->text();
int last = count.size();
//Saving the words in QList
for(int i=0;i<count.size();i++)
{
temp.append(count[i]+" ");
size = fm.width(temp);
if(size > 870)
{
temp.append("\n");
data << temp;
//data.append(temp);
temp.clear();
}
if((last-1)==i)
{
subString.append("\n");
data << subString;
subString.clear();
}
}
//decreasing the font size
QList<int> wide;
for(int i=0;i<data.size();i++)
{
wide << fm.width(data[i]);
while(wide[i] >= 870)
{
fontSize = ui->label->font().pointSize() - 1;
QFont newFont("Arial",fontSize);
QFontMetrics nfm(newFont);
ui->label->setFont(newFont);
wide[i] = 0;
wide[i] = nfm.width(data[i]);
}
}
//Finally displaying it on label
QString labelData;
for(int i=0;i<data.size();i++)
{
labelData = ui->label->text();
labelData.append(data[i]);
ui->label->setText(labelData);
}
After struggling with this issue, I create DynamicFontSizeLabel and DynamicFontSizePushButton widgets. Hope it helps.
https://github.com/jonaias/DynamicFontSizeWidgets/
Thanks Scheff for some inspiration.

Qt - Adding items to QGraphicsScene with absolute position

This is probably a dump question but I'm really stuck with it right now. I'm trying to draw some squares in a QGraphicsScene and I want them to be aligned from position x = 0 towards the positive position of x coordinates. However they are aligned according to the alignment configuration of the QGraphicsView and the setting of position is only effective for the second item and upwards relative to the first item! This means that if I have a single item, then setting of it's position has no effect. Mainly this line seems not to be working:
graphicsView->setAlignment(Qt::AlignAbsolute);
This is my code:
QGraphicsScene *scene = new QGraphicsScene();
QGraphicsView *graphicsView;
graphicsView->setScene(scene);
graphicsView->setAlignment(Qt::AlignAbsolute);
for(int i = 0; i < 500; i+= 50)
{
QGraphicsPolygonItem *item = new QGraphicsPolygonItem();
item->setPen(QPen(QColor(qrand()%255,qrand()%255,qrand()%255)));
item->setBrush(QBrush(QColor(255,251,253)));
item->setPolygon(*myPolygon);
graphicsView->scene()->addItem(item);
item->setPos(i , 40);
item->setFlag(QGraphicsItem::ItemIsMovable, true);
item->setFlag(QGraphicsItem::ItemIsSelectable, true);
graphicsView->show();
}
I do not know what the problem might be, so I tried the following code
const QRectF rect = QRectF(0, 0, ui->graphicsView->width(), ui->graphicsView->height());
ui->graphicsView->setScene(&_scene);
ui->graphicsView->fitInView(rect, Qt::KeepAspectRatio);
ui->graphicsView->setSceneRect(rect);
With respect to the previous four lines, the following output does not produce sizes even close to each other:
qDebug() << "scene =>" << _scene.width() << _scene.height();
qDebug() << "graphicview =>" << ui->graphicsView->width() << ui->graphicsView->height();
I highly appreciate your help.
It seems that Qt::AlignAbsolute does not do what you assume it does. What you actually need is Qt::AlignLeft | Qt::AlignTop.
It is explained here:
http://qt-project.org/doc/qt-4.8/qgraphicsview.html#alignment-prop
http://qt-project.org/doc/qt-4.8/qt.html#AlignmentFlag-enum
In my code, I only use graphicsView->setSceneRect(rect); and not fitInView(). The latter introduces a scaling and may hinder your understanding on what is going wrong.
Basically, I overwrite the QGraphicsview to re-implement resizeEvent():
void AutohideView::resizeEvent(QResizeEvent *event)
{
/* always resize the scene accordingly */
if (scene())
scene()->setSceneRect(QRect(QPoint(0, 0), event->size()));
QGraphicsView::resizeEvent(event);
}
I do not alter alignment or any other options and use absolute coordinates in the range [0,0 .. scene()->width(),scene()->height()] when positioning my items.

DrawArc are not drawen correctly (qt/c++)

I'm pretty new to QT and I can not understand why my arc's are drawed so bad.
I have 2 problems.
First one, which I think is just normal for such drawing, is:
If I draw with a QPainterPath a straight line will be drawed on every arc, from the end of the arc to the direction of point 0,0 but not completely to 0,0 instead it is just, i think, the half way to that point...
Second one:
If I use QPainterPath or painter.drawArc the "rings" are unsemetric if i change the pen width.
I have this code which will init my Arc's.
//Edit//
Sorry forgot to provide where w and h is created.
this->getMainWidget() returns just a QWidget where my elements are drawed.
the geometry and position of the toplevel widget and one from this->getMainWidget() are the same.
QRect mainWidgetGeo = geometry();
int w = mainWidgetGeo.width();
int h = mainWidgetGeo.height();
QPen secondPen(Qt::yellow);
secondPen.setWidth(50);
circleSeconds = new Circle(this->getMainWidget());
circleSeconds->setMaxValue(60);
circleSeconds->setValue(55);
circleSeconds->setSteps(60);
circleSeconds->setMouseTracking(true);
circleSeconds->setPen(secondPen);
circleSeconds->setGeometry(QRect(0, 0, w, h));
QPen minutePen(Qt::red);
minutePen.setWidth(100);
circleMinutes = new Circle(this->getMainWidget());
circleMinutes->setMaxValue(60);
circleMinutes->setValue(50);
circleMinutes->setSteps(60);
circleMinutes->setMouseTracking(true);
circleMinutes->setPen(minutePen);
circleMinutes->setGeometry(QRect(50, 50, w-100, h-100));
QPen hourPen(Qt::green);
hourPen.setWidth(50);
circleHours = new Circle(this->getMainWidget());
circleHours->setMaxValue(12);
circleHours->setValue(45);
circleHours->setSteps(12);
circleHours->setMouseTracking(true);
circleHours->setPen(hourPen);
circleHours->setGeometry(QRect(150, 150, w-300, h-300));
This will setup 3 Arc's.
First and third one have the same pen width of 50, the second one has 100.
For completion here is the Circle class:
#include <QtGui>
#include "Circle.h"
#include <QDebug>
Circle::Circle(QWidget *parent): QWidget(parent)
{
}
void Circle::setSteps(int i)
{
this->steps = i;
}
void Circle::setValue(int i)
{
this->value = i;
repaint();
}
void Circle::setMaxValue(int i)
{
this->maxValue = i;
}
void Circle::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(this->pen);
int stepSize = 360/this->steps;
float devideValue = ((100.0/this->maxValue)*this->value)/100.0;
int roundedSize = this->steps*devideValue;
int angel = -1.0*16.0*(stepSize*roundedSize);
qDebug() << "steps: " << steps;
qDebug() << "stepSize: " << stepSize;
qDebug() << "devideValue: " << devideValue;
qDebug() << "roundedSize: " << roundedSize;
qDebug() << "stepSize*roundedSize: " << (stepSize*roundedSize);
qDebug() << "angel: " << angel;
qDebug() << "angel: " << angel;
painter.drawArc(this->pen.width()/2, this->pen.width()/2, this->geometry().width()-(this->pen.width()), this->geometry().height()-(this->pen.width()), 0, angel);
/*QPainterPath circle_path;
circle_path.arcTo(this->pen.width()/2, this->pen.width()/2, this->geometry().width()-(this->pen.width()), this->geometry().height()-(this->pen.width()), 0, angel);
painter.drawPath(circle_path);*/
}
void Circle::setPen(QPen pen)
{
this->pen = pen;
}
Also I have noticed that, if the pen width differs from other arc's the "starting point 0" is different for each pen width...
Here are the output's to get a better understanding what goes wrong.
At this image the first problem with the line issue also present. (QPainterPath)
This is the output with painter.drawArc
//Edit//
The expected result should be something like this. Please note that the green circle spanAngle is different from the 2 images above because i did the result with photoshop and it was easier with those spanAngles :)
It should make it know clear what my problem is.
After testing with drawEllipse i recognize the same behaviour that the pen width is smaller at 45 clock as at 90 clock.
Can anybody help me to get rid of those issues? I'm also happy with different solutions to get such opened rings.
best regards,
PrDatur
There are 2 issues. The first is that starting point of arc depends on pen's width. It can be easily fixed by settings pen.setCapStyle(Qt::FlatCap); for each used pen.
The second issue is unfilled space between arcs. I can't understand why is it happening. It's somehow connected with Qt's QPen/QPainter system, but I can't find any way to fix it.
However, I found a workaround. Create appropriate QPainterPath containing borders of your figure, and then use QPainter::fillPath instead of stroking with a pen.
A side task is to use QPainterPath::moveArcTo to move and stroke a line. As far as I see, It's not supported. We'll need the following helper function that will be used with QPainterPath::lineTo method:
QPointF my_find_ellipse_coords(const QRectF &r, qreal angle) {
QPainterPath path;
path.arcMoveTo(r, angle);
return path.currentPosition();
}
In paintEvent function:
double angle = -1.0*(stepSize*roundedSize); // removed '*16' here
QPainterPath path;
QRectF outer_rect(0, 0, width(), height());
QRectF inner_rect(pen.width(), pen.width(),
width() - pen.width() * 2, height() - pen.width() * 2);
path.arcMoveTo(outer_rect, 0);
path.arcTo(outer_rect, 0, angle);
path.lineTo(my_find_ellipse_coords(inner_rect, angle));
path.arcTo(inner_rect, angle, -angle);
path.lineTo(my_find_ellipse_coords(outer_rect, 0));
path.closeSubpath();
painter.fillPath(path, QBrush(pen.color()));
There are some other minor issues in your code. For circleHours you have set the value bigger than maxValue. Also you should omit this-> when accessing class members.
In case of any issues with my code, examine complete file I was using to test it.

Draw a line in column 80 of QPlainTextEdit

I'm writing a text editor and using Qt for the GUI. I'm a noob in Qt and I'm having trouble to do this.
I need to draw a line in the column 80 of the QPlainTextEdit but I really don't know how. I'm using QPainter but I just can't get it right, any help?
Here's how I'd do it. It's admittedly not entirely trivial. The inputs to determining the 80th column position are:
80 x the average character width in floating point. Using the integer value will magnify the roundoff error by a factor of 80. Thus use QFontMetricsF.
The offset due to scrollbars comes from contentOffset(). It'd be bad to use horizontalScrollbar()->value(). The latter currently works, but relies on the implementation-specific detail. QPlainTextEdit happens to map scrollbar values to pixels -- who knows if it won't change tomorrow. It's not documented, thus falls under unspecified behavior.
The QTextDocument implements its own margin, available via documentMargin().
Another pitfall: you must paint on the viewport() in any class that derives from QAbstractScrollArea -- and QPlainTextEdit does so. If you don't, your paintEvent becomes a no-op. It's documented, but you must be clever enough to actually look into documentation. I'd consider it a bad corner case of an API that does something unexpected. In every other paintEvent, you simply create QPainter p or QPainter p(this) and it works.
Note: this is tested, compileable code.
//main.cpp
#include <cmath>
#include <QtWidgets>
class Edit : public QPlainTextEdit
{
public:
Edit(QWidget * parent = 0) : QPlainTextEdit(parent) {}
protected:
void paintEvent(QPaintEvent * ev)
{
QPlainTextEdit::paintEvent(ev);
const QRect rect = ev->rect();
const QFont font = currentCharFormat().font();
int x80 = round(QFontMetricsF(font).averageCharWidth() * 80.0)
+ contentOffset().x()
+ document()->documentMargin();
QPainter p(viewport());
p.setPen(QPen("gray"));
p.drawLine(x80, rect.top(), x80, rect.bottom());
qDebug() << x80 << contentOffset() << document()->documentMargin() << font << endl;
}
};
static QString filler()
{
QString str;
for (char c = '0'; c < '9'; ++ c) {
str.append(QString(10, c));
}
return str;
}
int main(int argc, char ** argv)
{
QApplication app(argc, argv);
Edit ed;
QTextCharFormat fmt = ed.currentCharFormat();
fmt.setFontFamily("courier");
fmt.setFontFixedPitch(true);
ed.setCurrentCharFormat(fmt);
ed.setLineWrapMode(QPlainTextEdit::NoWrap);
qDebug() << fmt.font() << endl;
ed.setPlainText(filler());
ed.show();
app.exec();
}
#include "main.moc"

Qt Resizing a QMainWindow with a Splitter

I have a QMainWindow with:
Two widgets in a horizontal splitter. "m_liner" is on the right side
Both widgets have a minimum size of say, 300 pixels.
A checkbox to hide/show the right-side widget m_liner.
I want the overall QMainWindow to expand when showing the widget, and shrink when hiding. The code below does this except:
If both widgets are shown, the minimum window size is 600 pixels.
Shrink the window to this smallest size.
Uncheck the box to hide the right-side widget.
Program hides the right-side widget.
Program calls this->resize(300, height);
The window ends up being 600 pixels wide (the minimum size with both widgets visible), instead of around 300 (the minimum size with only the left widget).
Later, I can resize the window down to 300 pixels with the mouse or another button. But it won't resize to 300 in the checkbox event, even if I call resize several times.
Does anyone have an idea how to solve this?
Critical bit of code follows, I have a full project available if you need it:
void MainWindow::on_checkBox_stateChanged(int val)
{
std::cout << "-------------------- Checkbox clicked " << val << std::endl;
bool visible = val;
QWidget * m_liner = ui->textEdit_2;
QSplitter * m_splitter = ui->splitter;
int linerWidth = m_liner->width();
if (linerWidth <= 0) linerWidth = m_lastLinerWidth;
if (linerWidth <= 0) linerWidth = m_liner->sizeHint().width();
// Account for the splitter handle
linerWidth += m_splitter->handleWidth() - 4;
std::cout << "Frame width starts at " << this->width() << std::endl;
std::cout << "Right Panel width is " << m_liner->width() << std::endl;
// this->setUpdatesEnabled(false);
if (visible && !m_liner->isVisible())
{
// Expand the window to include the Right Panel
int w = this->width() + linerWidth;
m_liner->setVisible(true);
QList<int> sizes = m_splitter->sizes();
if (sizes[1] == 0)
{
sizes[1] = linerWidth;
m_splitter->setSizes(sizes);
}
this->resize(w, this->height());
}
else if (!visible && m_liner->isVisible())
{
// Shrink the window to exclude the Right Panel
int w = this->width() - linerWidth;
std::cout << "Shrinking to " << w << std::endl;
m_lastLinerWidth = m_liner->width();
m_liner->setVisible(false);
m_splitter->setStretchFactor(1, 0);
this->resize(w, this->height());
m_splitter->resize(w, this->height());
this->update();
this->resize(w, this->height());
}
else
{
// Toggle the visibility of the liner
m_liner->setVisible(visible);
}
this->setUpdatesEnabled(true);
std::cout << "Frame width of " << this->width() << std::endl;
}
Sounds to me like there are some internal Qt events that need to get propagated before it recognizes that you can resize the main window. If this is the case then I can think of two potential solutions:
Use a queued single shot timer to call the code that resizes your window down to 300px:
m_liner->hide();
QTimer::singleShot( 0, this, SLOT(resizeTo300px()) );
or, after you hide your widget you can try a call to processEvents() (this function has potentially dangerous side effects, so use with caution):
m_liner->hide();
QApplication::processEvents();
resize( w, height() );
Another potential solution would be to set the horizontal size policy of your widget to ignored when hiding it:
m_liner->hide();
m_liner->setSizePolicy( QSizePolicy::Ignored, QSizePolicy::Preferred );
resize( w, height() );
When showing your widget again, you'd need to adjust the size policy again.