Calculate QGraphicsTextItem font size based on scale - c++

I have QGraphicsTextItem objects on a QGraphicsScene. The user can scale the QGraphicsTextItem objects by dragging the corners. (I am using a custom "transformation editor" to do this.) The user can also change the size of the QGraphicsTextItem by changing the font size from a property panel. What I would like to do is unify these so that when the user scales the object by dragging the corner with the mouse, behind the scenes it actually is calculating "What size font is necessary to make the resulting object fit the target size and keep the scale factor at 1.0?"
What I am doing now is letting the object scale as normal using QGraphicsItem::mouseMoveEvent and then triggering a FinalizeMapScale method in QGraphicsItem::mouseReleaseEvent once the mouse scale is complete. This method should then change the font to the appropriate size and set the scale back to 1.0.
I have a solution that appears to be working, but I'm not crazy about it. I'm relatively new to both Qt and C++, so would appreciate any comments or corrections.
Is there a better way to architect this whole thing?
Are there Qt methods that already do this?
Is my method on the right track but has some Qt or C++ errors?
Feel free to comment on my answer below on submit your own preferred solution. Thanks!
[EDIT] As requested in comment, here is the basics of the scaling code. We actually went a different direction with this, so this code (and the code below) is no longer being used. This code is in the mouseMoveEvent method, having previously set a "scaling_" flag to true in mousePressEvent if the mouse was clicked in the bottom-right "hot spot". Note that this code is in a decorator QGraphicsItem that holds a pointer to the target it is scaling. This abstraction was necessary for our project, but is probably overkill for most uses.
void TransformDecorator::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
...
if (scaling_) {
QGraphicsItem *target_item = target_->AsQGraphicsItem();
target_item->setTransformOriginPoint(0.0, 0.0);
QPointF origin_scene = mapToScene(target_item->transformOriginPoint());
QPointF scale_position_scene = mapToScene(event->pos());
qreal unscaled_width = target_item->boundingRect().width();
qreal scale_x = (scale_position_scene.x() - origin_scene.x()) / unscaled_width;
if (scale_x * unscaled_width < kMinimumSize) {
scale_x = kMinimumSize / unscaled_width;
}
target_item->setScale(scale_x);
} else {
QGraphicsObject::mouseMoveEvent(event);
}
}

Please no holy wars about the loop-with-exit construct. We're comfortable with it.
void MapTextElement::FinalizeMapScale() {
// scene_document_width is the width of the text document as it appears in
// the scene after scaling. After we are finished with this method, we want
// the document to be as close as possible to this width with a scale of 1.0.
qreal scene_document_width = document()->size().width() * scale();
QString text = toPlainText();
// Once the difference between scene_document_width and the calculated width
// is below this value, we accept the new font size.
const qreal acceptable_delta = 1.0;
// If the difference between scene_document_width and the calculated width is
// more than this value, we guess at the new font size by calculating a new
// scale factor. Once it is beneath this value, we creep up (or down) by tiny
// increments. Without this, we would sometimes incur long "back and forth"
// loops when using the scale factor.
const qreal creep_delta = 8.0;
const qreal creep_increment = 0.1;
QScopedPointer<QTextDocument> test_document(document()->clone());
QFont new_font = this->font();
qreal delta = 0.0;
// To prevent infinite loops, we store the font size values that we try.
// Because of the unpredictable (at least to me) relationship between font
// point size and rendering size, this was the only way I could get it to
// work reliably.
QList<qreal> attempted_font_sizes;
while (true) {
test_document->setDefaultFont(new_font);
delta = scene_document_width - test_document->size().width();
if (std::abs(delta) <= acceptable_delta ||
attempted_font_sizes.contains(new_font.pointSizeF())) {
break;
}
attempted_font_sizes.append(new_font.pointSizeF());
qreal new_font_size = 0.0;
if (std::abs(delta) <= creep_delta) {
new_font_size = delta > 0.0 ? new_font.pointSizeF() + creep_increment
: new_font.pointSizeF() - creep_increment;
} else {
new_font_size = new_font.pointSizeF()
* scene_document_width
/ test_document->size().width();
}
new_font.setPointSizeF(new_font_size);
}
this->setFont(new_font);
this->setScale(1.0);
}

Another way to look at the problem is: Qt has scaled the font, what is the effective font size (as it appears to the user, not the font size set in the text item) that I need to display to the user as their choice of new font size? This is just an alternative, you still need a calculation similar to yours.
I have a similar problem. I have a text item that I want to be unit size (one pixel size) like my other unit graphic items (and then the user can scale them.) What font (setPointSize) needs to be set? (Also what setTextWidth and what setDocumentMargin?) The advantage of this design is that you don't need to treat the scaling of text items different than the scaling of any other shape of graphics item. (But I don't have it working yet.)
Also, a user interface issue: if the user changes the font size, does the item change size? Or does it stay the same size and the text wrap differently, leaving more or less blank space at the end of the text? When the user appends new text, does the font size change so all the text fits in the size of the shape, or does the shape size grow to accommodate more text? In other words, is it more like a flowchart app (where the shape size is fixed and the font shrinks), or like a word processor app (where the font size is constant and the shape (number of pages) grows?

Related

Qt text size in points

I'm trying to print an invoice document on A4 in millimetres instead of the default device units. Except that when changing the units to millimetres the pointsize of text on the printed document no longer matches up with the pointsize in for instance Word or Adobe Illustrator. I tried converting the point size to the corresponding pixel size, but they had issues.
QFont::SetPixelSize only takes an int so if the calculations falls below 1 it will trunctuate to 0
font.setPixelSize((9.0 * 72.0) / printer.resolution());
And the other method made the text the correct vertical size, but there are some artefacts:
int phys_w = printer.width();
font.setPointSizeF((9.0 / phys_w) * 210.0);
Where you can see the unusually large gaps between some characters. (Perhaps there is some of precision issue inside Qt its text drawing code?)
Here is a minimal example showing the issues:
QPrinter printer(QPrinter::HighResolution);
printer.setPageSize(QPrinter::A4);
printer.setOrientation(QPrinter::Portrait);
printer.setFullPage(true);
printer.setPageMargins(QMarginsF(0, 0, 0, 0));
printer.setOutputFormat(QPrinter::PdfFormat);
printer.setOutputFileName("invoice.pdf");
QPainter painter(&printer);
auto page_size = printer.pageRect(QPrinter::Unit::Millimeter);
painter.setWindow(page_size.toRect());
QFont font = painter.font();
// Either this
font.setPixelSize((9.0 * 72.0) / printer.resolution());
// or this
int phys_w = printer.width();
font.setPointSizeF((9.0 / phys_w) * 210.0);
painter.setFont(font);
painter.drawText(35, 46, "John Doe");
How can I have the positioning in Millimetres (or any arbitrary unit) and have the text size be correct in points (or some correct recalculation)?
This is on Qt 5.10.0 and Windows 10.
EDIT
In the end I opted to go for a 10x scale increase (so tenths of a millimetre) which fixed the kerning issues visible for setPointSizeF. Now the last issue I'm having with the scale is that of setting the width of a line and other shapes (QPen::setWidth) and I cant find a calculation so it's in millimetres.
EDIT
In the end the linewidth didn't need any recalculations. The final code is below:
QPrinter printer(QPrinter::HighResolution);
printer.setPageSize(QPrinter::A4);
printer.setOrientation(QPrinter::Portrait);
printer.setFullPage(true);
printer.setPageMargins(QMarginsF(0, 0, 0, 0));
printer.setOutputFormat(QPrinter::NativeFormat);
QPainter painter(&printer);
painter.setWindow(0, 0, 2100, 2970);
painter.setViewport(0, 0, printer.width(), printer.height());
QFont font(fontFamily, 0, weight, italic);
font.setPointSizeF(static_cast<float>(pixelSize) / printer.width() * 2100);
I think you're dividing where you should multiply and vice versa. Take a look with the units written out explicitly:
9 points * (1 inch / 72 points) * (printer.res pixels/inch)
multiplying the units, the numerator gets (points * inch * pixels) , denominator gets (points * inch) . Cancel out the like units and you get pixels in the numerator. So the calculation should be:
font.setPixelSize(9.0 / 72.0 * printer.resolution());
For your second problem,
QPen::setWidthF(w*printer.resolution()/25.4);
where w is your desired width in mm.
By making detailed observations (printing a long sentence) it's noticed that these gaps between characters are directly related to characters themselves, wide characters and Capital letters eat up the gap (J, s , r, c) while narrow characters leave more gap (i , l , t) .. this is just to say its not a random behavior.
In general this is known as kerning nicely explained here. To minimize this, Qt sets QFont kerning to true (default) QFont Kerning ... but the problem is that kerning is much dependent on the used font and pixel, and Qt kerning enabled sometimes has not effect as in this post, probably because setting Kerning to true
(Will apply kerning between adjacent glyphs. Note that OpenType GPOS
based kerning is currently not supported QRawFont::LayoutFlags
Which means that some font Ascent / Descent will still cause limitation how the gap is controlled.
Two solutions are considered below, the first is with still sticking to default painter font, yet you can stretch the characters to enforce reasonable gab:
font.setStretch(70); // value is experimental
I don't think this is a robust solution, unless one is limited to use specific font, while font itself is not important for invoice printing, the second attractive approach is to find out best font that meets requirements: render well with set resolution / little loss when drawn to PDF (from Qt) / and most important efficient for kerning!
"MS Gothic" (ttf) for example performs well for such setup, here how it performs (without stretch)
painter.setFont(QFont("MS Gothic"));
QFont font = painter.font();
int phys_w = printer.width();
font.setPointSizeF((9.0 / phys_w) * 210.0);
//font.setStretch(70);
painter.setFont(font);
The good thing about selecting a suitable font is that more control is possible (especially for invoice printing where small space is valuable).
For example the same can printed with less space by reducing the gap between letters:
font.setLetterSpacing(QFont::PercentageSpacing,65); // 65% gap of default
And one can go even for lower font size without loosing visual clearance.
sharing below some work while trying to clarify how printing small font size is affecting gap between letters. the idea is to draw each character separately inside QRect and at the same time draw equivalent QRectF on same area.
That's to see when font size is large QRectF are drawn fit next to each other, so also each character ... while when font size goes low the adjacent QRectFs will severely overlap and gaps between characters start to get disordered.
QPen pen=painter.pen();
pen.setWidth(0.1);
painter.setPen(pen);
QString td("John Doe");
auto spacer = font.pointSizeF(); // font size used to set width of QRect of each character.
spacer *=30.0; // large Rect width.
auto b = 35.0;
for (int i=0; i < td.length() ;i++ )
{
QRectF rectf(b+=spacer,47.0,4.0,4.0);
QRect rect(b, 47.0,4.0,4.0);
QString ch = td.at(i);
//painter.drawText(b+=spacer,46,ch);
painter.drawText(rect,Qt::AlignCenter,ch);
painter.drawRect(rectf);
}
painter.end();
Result for large font size:
Next make QRectF overlap:
spacer *=10.0;
Result, letters get less gaps and wide adjacent characters get narrow gap.

Wrapping text in GTK3 treeview

I have trouble getting TreeView in GTK3 to wrap text correctly.
I set it up to wrap in this way:
Gtk::TreeViewColumn* pColumn = mTreeView.get_column(2);
static_cast<Gtk::CellRendererText *>(pColumn->get_first_cell())
->property_wrap_mode().set_value(Pango::WRAP_WORD_CHAR);
static_cast<Gtk::CellRendererText *>(pColumn->get_first_cell())
->property_wrap_width().set_value(200);
This works, text is wrapped, but when I resize the window and make it bigger, there is a lot of ugly white-space above and below cell with long text. It seems, that GTK reserves height for cell based on wrap width. Which makes no sense to me.
I tried to get around with setting needed in signal_check_resize with calculating needed width like this:
Gtk::TreeViewColumn* pColumn = mTreeView.get_column(2);
auto width = this->get_allocated_width()
- mTreeView.get_column(0)->get_width()
- mTreeView.get_column(1)->get_width();
static_cast<Gtk::CellRendererText *>(pColumn->get_first_cell())
->property_wrap_width().set_value(width-100);
this->forceRecreateModel = true; //Needed to work
But this lets me only make window bigger. It cannot be shrinked, after it was resized.
The question is, how this is properly done?
I am using gtk3.20.3-1 and gtkmm3.20.1-1 on Arch linux.
EDIT: fixed typo in the title...
In the end I found how to do it.
In the setup of the window (for me constructor of the window derived class) it was necessary to set column to be AUTOSIZE in order to allow shrinking of the width.
//Last Column setup
{
mTreeView.append_column("Translation", mColumns.mEnglish);
Gtk::TreeViewColumn* pColumn = mTreeView.get_column(2);
pColumn->set_sizing(Gtk::TreeViewColumnSizing::TREE_VIEW_COLUMN_AUTOSIZE);
static_cast<Gtk::CellRendererText *>(pColumn->get_first_cell())
->property_wrap_mode().set_value(Pango::WRAP_WORD_CHAR);
}
Also there is needed to set correct wrap width on every resize. Without this, height of the row was as big as it would be necessary for currently set wrap_width with no regard on current width (resulting in big padding on the top, when stretched more and prohibiting to make window smaller).
This code was also in the constructor.
this->signal_check_resize().connect([this]()
{
//calculate remaining size
Gtk::TreeViewColumn* pColumn = mTreeView.get_column(2);
auto width = this->get_allocated_width()
- mTreeView.get_column(0)->get_width()
- mTreeView.get_column(1)->get_width()-30;
//minimum reasonable size for column
if(width < 150)
width = 150;
static_cast<Gtk::CellRendererText *>(pColumn->get_first_cell())
->property_wrap_width().set_value(width);
//debounce
static auto oldsize = 0;
{
oldsize = width;
//trigger redraw of mTreeView (by clearing and refilling Model,
//it is done in 100ms pulse)
this->mRedrawNeeded = true;
}
});
And maybe it is worth noting, that I have mTreeView encapsulated in Gtk::ScrolledWindow. So this is a chunk which comes before column setup. :)
//in class is: Gtk::ScrolledWindow mScrollForResults;
//scrolling area
mGrid.attach(mScrollForResults, 0,2,10,1);
mScrollForResults.set_hexpand();
mScrollForResults.set_policy(Gtk::PolicyType::POLICY_AUTOMATIC,
Gtk::PolicyType::POLICY_ALWAYS);
mScrollForResults.set_margin_top(10);
mScrollForResults.set_min_content_width(400);
mScrollForResults.set_min_content_height(200);
mScrollForResults.add(mTreeView);
//results treeView
mRefListStore = Gtk::ListStore::create(mColumns);
mTreeView.set_model(mRefListStore);
mTreeView.set_hexpand();
mTreeView.set_vexpand();

Scale QWidget height based on width

I have a QLabel that contains an image. I need to ensure that the image allways takes 100% of the QLabel width, but preserving aspect ration is just as important. This means I also need to increase QLabel height so that the image fits. Like on this image, the height must be fit:
I Qt designer I see no way to specify preferred aspect ratio, so I just tried to override resize event and try to force correct height upon resize:
void QPictureLabel::resizeEvent(QResizeEvent *event)
{
qDebug()<<"Resized.";
// Current image width in pixels
float pw = myPixmap.width();
// current label width in pixels
float my_width = width();
// ratio of label and image width can decide the new required label height
float new_h = (my_width/pw)*myPixmap.height();
// This is an attempt to prevent endless loop... didn't work out
if(new_h!=height()) {
// Force new height (that works)
resize(my_width, new_h);
// Tell the layout to move other elements (that doesn't work at all!)
updateGeometry();
}
}
Before adding the updateGeometry call it just looked like this:
As you can see (I highlighted it with red frame), the label indeed expanded as necessary. But other widgets do not care about that.
I added updateGeometry call, but the result was endless loop for some reason. I need the correct way to inform the layout that QLabel requires more space.
but the result was endless loop for some reason
This is documented, read documentation on resizeEvent(). Shf's solution should work for you.

QwtPlot automatically resizes during AutoScale

I'm using libqwt-6.1.1 to display variable data in a QwtPlot element (as histogram) using auto scale for y-axis. Depending on the actual data and the number of tics, the autoscale-run considers reasonable, the minimum height of the whole QwtPlot element often increases. This kills my whole layout design and frequently makes the window larger than the screen.
Now 2 questions:
1) How can I prevent the QwtPlot from being automatically enlarged?
2) How does the minimum size calculation actually work? (I got lost somewhere in QwtPlotLayout::activate() while trying to follow the calculation steps.)
Finally, the solution was simpler than expected ...
Overloading QwtPlot::sizeHint() and QwtPlot::minimumSizeHint() (which are virtual, fortunately) making both return an small constant value did the trick, i.e.
QSize myPlot::sizeHint() const { return QSize(100, 100); }
QSize myPlot::minimumSizeHint() const { return QSize(100, 100); }
The factor that has an effect on the height and depends on auto scaling is the number of major ticks of the vertical scales ( tickcount * font height ).
The calculation of the ticks depends on the given stepSize. When the stepSize is set to 0 ( = default setting ) it is calculated from the maxMajor setting ( default: 8 ), what is an upper limit for the number of the major ticks.
So f.e. you could use a stepSize to 0 and set a reasonable maxMajor setting for your use case. Then use a QLayout setup, where the plot gets not always shrinked to its minimum height.

Set slider size of QScrollBar correspond with content

I am doing a software with a drawing surface that represent a plot (like a sin function) (A child of QWidget) and I would like to have a QScrollBar acting like a QScrollArea. So if my drawing widget show show 750 dots (my plot is made of dots), but there are 1000 dots, I would like the slider of the ScrollBar to fill 75% of the available space.
I can't use a QScrollArea because the scroll is proportionnal with the size of the widget it contains. In my case, the scroll must be proportionnal with the number of dots on the screen. I know how to get my ratio, but I don't know how to setup correctly the QScrollBar
Example: I edited the value of PageStep, but I don't understand how this work. I can set pageStep to 100 with a range of [0,99] and it will fill the half of the QScrollBar.
My interface:
QWidget (Vertical Layout) //Main Widget
Drawing Surface (Child of QWidget)
QScrollBar (Horizontal)
Well, I think I am able to do something with this:
http://harmattan-dev.nokia.com/docs/library/html/qt4/qscrollbar.html
The relationship between a document length, the range of values used in a scroll bar, and the page step is simple in many common situations. The scroll bar's range of values is determined by subtracting a chosen page step from some value representing the length of the document. In such cases, the following equation is useful: document length = maximum() - minimum() + pageStep().
So in my case the length is the number of dots and I can set minimum() to 0. So, as you can see on the picture, to do something like QScrollArea. The proportion is: PercentageVisible = PageStep / Length and another equation is Length = PageStep + Max.
I have two equations, two missing values (PageStep and Maximum) and two known values (PercentageVisible and Length).
Example: I have 1024 dots, but only 75% of them are shown.
0.75 = PageStep / 1024 ----------> PageStep = 768
1024 = Max + 768 ----------------> Max = 256
You can try it in your software and it will works. I know there's not so much people that will needs to reproduce this because QScrollArea will do the job in most of the case.
By example, this code is in a slot reacting from a resize event:
ui.sbarRange->setPageStep(u64SampleCount * dRatio);
ui.sbarRange->setMaximum(u64SampleCount - ui.sbarRange->pageStep());
You can create a new QWidget subclass and reimplement sizeHint and paintEvent. In the paintEvent you can use event->rect() to determine which area is currently exposed and needs to be drawn. Note that paintEvent must be fast if you don't want your window to freeze. Also you need to put created widget in QScrollArea.
Here is a simple example that draws a sinusoid:
class SinWidget : public QWidget {
public:
QSize sizeHint() const {
return QSize(10000, 200);
}
void paintEvent(QPaintEvent* event) {
QPainter painter(this);
for(int x = event->rect().left(); x <= event->rect().right(); x++) {
painter.drawPoint(x, 100.0 + qSin(0.05 * x) * 20.0);
}
}
};
QScrollArea area;
area.setWidget(new SinWidget());
area.show();
This example will work fine with very large widget size (e.g. 100 000 pixels). So full repaint or memory allocation doesn't happen. You only need to keep your paintEvent fast.