get the exact height of QTextDocument in pixels - qt

I need to get the actual height of QTextDocument in order to be able to set the containing QPlainTextEdit to a minimum height (while keeping its width constant) so that it shows the whole content without the vertical scrollbar. I tried to follow this question (closed with with accepted answer) How do I determine the height of a QTextDocument? but it does not do what it promises.
A piece of code:
from PyQt5.QtWidgets import QApplication, QPlainTextEdit
app = QApplication([])
w = QPlainTextEdit()
w.setPlainText("Hello!")
print(w.document().size())
w.setPlainText("Hello!\nHello again!")
print(w.document().size())
prints out:
PyQt5.QtCore.QSizeF(35.0, 1.0)
PyQt5.QtCore.QSizeF(64.0, 2.0)
It seems that the width is measured correctly in pixels but the height just shows the number of lines instead of pixels. I think multiplying it with font pixel metric height does not help because there can be mixed formatting (in general it can be a rich text / HTML) and line spacing, document margins and maybe some other complicated stuff based on implementation details... etc.
So is there a way out?

So I finally found a solution but it is really ugly. If anyone knows anything better, please publish it.
from PyQt5.QtWidgets import QApplication, QPlainTextEdit
app = QApplication([])
w = QPlainTextEdit()
# test various formatting
w.appendHtml("<h1>Hello!</h1>")
w.appendHtml("<b>Hello!</b>")
w.appendPlainText("Hello!")
doc = w.document()
layout = doc.documentLayout()
h = 0
b = doc.begin()
while b != doc.end():
h += layout.blockBoundingRect(b).height()
b = b.next()
# magic formula: I do not know why the document margin is already
# once included in the height of the last block, and I do not know
# why there must be the number 1 at the end... but it works
w.setFixedHeight(h + doc.documentMargin() + 2 * w.frameWidth() + 1)
w.show()
app.exec_()
So this should show the box without scroll bar. If you decrease the height by 1, the scroll bar appears. This should work with any number of lines, document margins, frame widths, formatting etc. Hopefully.

Shot in the dark without testing
Have you looked # pageSize?
From the docs:
This property holds the page size that should be used for laying out the document
The units are determined by the underlying paint device. The size is
measured in logical pixels when painting to the screen, and in points
(1/72 inch) when painting to a printer.
By default, for a newly-created, empty document, this property
contains an undefined size.
If you set the pageSize, as directed by the other thread, I'd expect you'd get the value out in the pixels that QPlainTextEdit::setMinimumHeight needs.

Related

Qt FontMetrics boundingrect vs Geometry rect

QString sText1 = "Sample Text890\nSample Text 890";
QString sText2 = "Sample Text890 Sample Text 890";
label1_->setText(sText1);
label2_->setText(sText2);
label1_->setWordWrap(false);
label2_->setWordWrap(false);
label1_->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
label2_->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
Numbered boxes show metrics/information of corresponding labels just above them.
case 1: BoundingRect width is NOT equal to label widthx2
BoundingRectHeight is equal to label widthx2
case 2: BoundingRect width and height matches with label's width and height
case 3: No clue how boundingrect and label geometry are related!!
case 4: No clue how boundingrect and label geometry are related!!
case 5:
QString sText1 = "Sample Text890\nSample Text 890";
QString sText2 = "Sample Text890 Sample Text 890";
label1_->setWordWrap(true);
label2_->setWordWrap(true);
Question: I'm confused how font's bounding rect and label's geometry are related.
EDIT: I have updated case 5 and case 6 with label word wrap TRUE.
From the boundingRect(text) documentation:
Newline characters are processed as normal characters, not as linebreaks.
So, considering the above, case 1 and 2 have the same height because, since the new line character does not create a new line. The width is different because a new line character has a different width.
Case 3 and 4 have the same bounding rect, which is the smallest possible width based on the given rect and text option. Since the base rect has no width and the option is to wrap words, you'll get a rectangle that is wide as the "longest" word (based on the font), while the height depends on the widths of the other words in the given text: you'll get a line for each word, unless two or more words can fit the above maximum width; the height depends on the line height multiplied the final line count.
Consider the original strings:
sText1 = 'Sample Text890\nSample Text 890'
sText1 = 'Sample Text890 Sample Text 890'
Case 1
The processed string is actually:
Sample Text890*Sample Text 890
Note: " * " refers to the newline character, it's not actually the * character.
The width is that of the whole string.
The negative y is because the drawing considers the origin (0, 0) for the baseline: y is the negative fontMetrics.ascent().
In this case, the QLabel has a different height because it does consider the newline character.
Case 2
The processed string is used as it is (one line), so the label size matches the font metrics. The result is probably as using a QRect at (0, 0), sized with horizontalAdvance() and height().
Case 3 and 4
Since the source rectangle has no width and word wrapping is enabled, the text will be laid out in order to fit it by extending the width until wrapping is possible; the final processed text becomes the following:
Sample
Text890
Sample
Text
890
Which makes sense, since the height for case 1 and 2 is 41 and the height of case 3 and 4 is about 5 times that value.
Note that results may change depending on the font, and do not depend on the length of the string. For instance, consider a peculiar font that has numbers that are extremely wide (about 3 times a normal character); the resulting text could be this:
Sample
Text890
Sample Text
890
That would be because 890 is very wide with that peculiar font, and the line Text890 becomes wider than Sample Text, which then will fit in a single line:
Relations with the QLabel size
By default, QLabel does not wrap text (see the wordWrap property), so case 3 and 4 are not related because you explicitly specified that option for the font metrics. That said, you can get consistent result if you understand how text layout works.
For instance, to get a consistent bounding rect, use a very big rectangle as source:
QRect rect1 = fontMetrics.boundingRect(QRect(0, 0, 2000, 2000), Qt::TextWordWrap, sText1);
Which will return a size equal to the label basic sizeHint(). Obviously, since the source rectangle is that big, the word wrap option is useless, and you'll get the same result using 0 instead of Qt::TextWordWrap.
On the other hand, you can have the same result of the bounding rect that you got with the empty QRect, using the minimumSizeHint() and with wrapping enabled:
label.setWordWrap(true)
QSize minWidth = label.minimumSizeHint().width()
QRect boundingRect(0, 0, minWidth, label.heightForWidth(minWidth))
Remember, though, that using word wrapping in QLabel can have counter intuitive and unwanted results; while those results might seem unexpected, they actually are expected (see the note about layout issues: Qt layout management is not the same as a webpage, and the priority is always for all widgets, even if it's for the sake of a label. If the label must support wrapping but it's also placed in a complex layout, you need to explicitly set a reasonable minimum size (width, height, or both) whenever the layout also contains widgets that can adapt their size based on the overall available size; see the related note below.
Final considerations
QLabel uses parts of the Qt rich text processing framework, specifically QTextDocument, its document layout and the basic QTextLayout; unfortunately, probably due to performance reasons, all those components are private for QLabel; the only way to reliably compute a QLabel size (in the rare case for which this should be really needed) is to have deep knowledge of the above aspects;
QFontMetrics and QTextLayout are closely related: the former uses the latter to compute the size of laid out text (boundingRect(QRect, flags, text)), and vice versa for computing basic glyph sizes;
Qt layout managers will try their best to fit a word-wrapped label in the layout, but, as explained above, results may vary;
word-wrapped text should not be part of a layout: while this might be considered a Qt limitation, it's almost always a bad choice from the UX perspective (remember, a program is not a webpage, which is scrollable by nature); those texts should probably be put in a scroll area, so eventually consider using a QTextEdit or QPlainTextEdit set as readOnly and eventually a transparent background to make it look "like a label";
any padding/border/margin set with setContentsMargins() or QSS (Qt style sheets) must be added manually when trying to use font metrics;

How can I find the minimum printer margins?

Problems:
I am printing custom size scenes, the printing must work on a variety of printers, standard or with custom sizes, or roll-fed (particularly this). Some of the custom printers are edge-to-edge.
The user-defined canvas may or may not match the printer paper size.... If the image is smaller than the paper, some printers will center it, others (like HP) print it on top left.
On some printers I can set "Custom" paper, others do not support it.
If the printer has minimum margins, some printers seem to clip, others to render from the top-left margin, and ma or may not clip on image size.
I would like to handle the clipping and margins myself and send to the printer the image as it should fit on "page".
m_printer->setPaperSize(QPrinter::Custom); //gives
QPrinter::setPaperSize: Illegal paper size 30
Assuming that the following works,
m_printer->setPaperSize(canvasRectangle.size(), QPrinter::Point);
getting the marked paper size in cups still returns the default marked in the ppd (Letter, w4h4, ...) (though I can print or cut that size)
What I need:
I need to find, for the (selected/custom) paper/page, the minimum margins.
I thought I could get the margins by just asking for them
qreal left, right, top, bottom;
m_printer->getPageMargins(&left, &top, &right, &bottom, QPrinter::Point);
qDebug() << left << right << top << bottom;
But regardless of printer (I tried HP, PDF and a custom edge-to-edge printer) I got 10 10 10 10.
I thought I would set them to 0 first... I got back 0. (but printing still used some tiny margins, which it either clipped or moved over depending on device, except for the edge-to-edge printers - so while I got no error setting margins to 0 when 0 is impossible, QPrinter told me it set margin to 0 successfully.)
Right now I am trying to make this work in Linux, using cups (and Qt 4.8) - I looked in the ppd of the various printers, but what I see, as ImageableArea for different provided sizes, is that each size has different margins - so that defies the minimum margins idea.
I imagined that the minimum margins (for maximum printable area) should not depend on the paper chosen, but on the printer geometry.
I considered getting the cups ppd option values for ImageableArea - but getting it for the "default" paper size doesn't seem useful if I am not using that paper size - and for custom paper size, there is a range so I don't know what I can get from it.
Also - I can't even seem able to get the cups option for ImageableArea:
const cups_dest_t* pDest = &m_pPrinters[iPrinterNumber];
for (int i = 0; i < pDest->num_options; ++i)
if (strcmp(pDest->options[i].name, pName) == 0)
// I can show options like "PageSize", "PageRegion" but not "ImageableArea"
I am struggling to understand this...
How can I find, using either Qt or cups, the minimum possible printer margins ?

QPainter::drawImage prints different size than QImage::save and print from Photoshop

I'm scaling a QImage, currently as so (I understand there may be more elegant ways):
img.setDotsPerMeterX(img.dotsPerMeterX() * 2);
img.setDotsPerMeterY(img.dotsPerMeterY() * 2);
When I save:
img.save("c:\\users\\me\\desktop\\test.jpg");
and subsequently open and print the image from Photoshop, it is, as expected, half of the physical size of the same image without the scaling applied.
However, when I simply print the scaled QImage, directly from code:
myQPainter.drawImage(0,0,img);
the image prints at the original physical size - not scaled to half the physical size.
I'm using the same printer in each case; and, as far as I can tell, the settings are consistent between both print cases.
Am I misunderstanding something? The end goal is to successfully scale and print the scaled image directly from code.
If we look at the documentation for setDotsPerMeterX it states: -
Together with dotsPerMeterY(), this number defines the intended scale and aspect ratio of the image, and determines the scale at which QPainter will draw graphics on the image. It does not change the scale or aspect ratio of the image when it is rendered on other paint devices.
I expect that the reason for the latter case being the original size is that the image has already been drawn before the call to the functions to set the dots per meter. Or alternatively, set the dots per meter on the original image, before loading its content.
In contrast, when saving, it appears that the device which you save to is copying the values you have set for the dots per meter on the image, then drawing to that device.
I would expect creating a second QImage, setting its dots per meter, then copying from the original to that second image, it would achieve the result you're looking for. Alternatively, you may just be able to set the dots per meter before loading the content on the original QImage.

Tkinter Grid Columnspan ignored

Consider the following python script
#!/usr/bin/env python
from Tkinter import Tk, Label
width = SOME_VALUE_HERE
root = Tk()
label1 = Label(root, text='1 columns wide')
label2 = Label(root, text='%i columns wide' % width)
label1.grid()
label2.grid(row=0,column=1,columnspan=width)
root.mainloop()
When I run this, no matter what value is set for 'SOME_VALUE_HERE', both labels take up half the window, regardless of whether or not Grid.columnconfigure is called, or the sticky parameter is used in grid().
Unless I've overlooked something, I would have thought that setting the columnspan would force the second label to be 'SOME_VALUE_HERE' times as wide as the first.
Have I misunderstood how grid works? How would I go about achieving this behavior?
By default, empty grid column are zero width, so you described the following table. Grid geometry manager will by default try to optimize the screen real estate used by your application. It will integrate all the constraint and produce the fittest layout.
+---------------+---------------++++
| 0 | 1 |||| <-- 2,3,4 empty, 0 width
+---------------+---------------++++
| 1 column wide | 4 column wide |
+---------------+---------------++++
To provide strict proportional column width, you have to use the uniform option of columnconfigure. uniform takes an arbitrary value to designate the group of the column that share these proportions, and the weight argument is used to properly handle widget resizing.
label1.grid(row=0, column=0)
label2.grid(row=0,column=1, columnspan=width)
for i in range(width+1):
root.grid_columnconfigure(i, weight=1, uniform="foo")
Note that with only these two labels, you could achieve the same layout by adjusting the width of column 1. Differences will occur still while you populate column 2,3,4...
label2.grid(row=0,column=1) #no columnspan
root.grid_columnconfigure(0, weight=1, uniform="foo")
root.grid_columnconfigure(1, weight=width, uniform="foo")
When you put something in column 1 with a columnspan of two (or more) that means it will be in column 1 and column 2 (etc). However, if there is nothing controlling the width of a column, that column will have a width of zero. You need to force column 2 to have a widtheither by putting something in there, giving it a minsize, or forcing uniform columns.
When I look at your code, I can't guess how wide you think column 2 should be, and neither can the computer.
I had a similar problem only to discover that the elements are limited by the widest widget. We can safely say that Tkinter is configured to make your app uniform in that it should be a regular repeating square/triangular structure. Solution to override default options.
With the Tkinter's automatic optimization in mind, play with the width and height of largest widget (grid box) and relate the other boxes to it proportionally.
Using the above method use columnspan to adjust the width.
Configure the widths by use of columnconfigure()

How to get the length of the left/right offsets in a QSlider?

I'm trying to make a subclass of QSlider that allows the user to insert "bookmarks" so they can remember significant locations on the slider. I've run into a problem painting the tabs on the slider - using QStyle.sliderPositionFromValue, I get a value but it is slightly inaccurate. If I'm on the left side of the slider, the tab is painted too far left, and on the right side it is painted too far right. I believe this is because QSlider.width() returns the width of the whole object, including the small offsets at the left and right. So width() might return 630 pixels, when the length of the slider itself is really 615.
This is the code I'm using to get the pixel position and draw a line across the slider.
pos = QStyle.sliderPositionFromValue(self.minimum(),self.maximum(),sliderIndex,self.width())
painter.drawLine(pos,0,pos,self.height())
I've been looking at the QT Source here starting on line 2699 and it seems like I need to be using the PixelMetric class from QStyle. I've tried using:
self.style().pixelMetric(QStyle.PM_SliderSpaceAvailable)
But that returns 0, which is clearly not the value I need.
I'd appreciate any advice.
Edit: As suggested in the comments, I changed the call to:
self.style().pixelMetric(QStyle.PM_SliderSpaceAvailable, QStyleOption(1,QStyleOption.SO_Slider),self)
This however, returns -14, which also doesn't match for the value of the offsets (I tried using self.width()-14 but the offset remains.

Resources