QPainter: adding padding - c++

Is there a way to easily add padding to a QPainter area?
The whole idea is to have a border, within the drawable area, where I can't draw so that when I draw a line from (0, 0) to (10, 10) I'm actually drawing at (0 + padding, 0 + padding) to (10 + padding, 10 + padding). The padding border should be seen though.
Assuming a QPainterĀ is created as:
QPainter painter(aWidget); // aWIdget is a Widget*
and a padding integer variable. Now let's consider the area that is drawable of the widget as "A". How can I get to have a drawable area "B", so that B has:
B_width = A_width - 2 * padding;
B_height = A_height - 2 * padding;
and what would have been at QPoint(padding, padding) in A would now be at QPoint(0, 0) in B?
I began to implement it by my own (which is going to be painful), but I was wondering if there way an easier and "pre-made" way to do this in Qt? Maybe Transformations?
Thanks.

Yes, doing a transform would be the best way. If you apply a transformation, then all subsequent draw calls will be transformed by that transform. For example applying a translation of (5,5) would make a line (0,0) to (10,0) become (5,5) to (15,5).
The QPainter documentation can be found here and if you look near the bottom you will see a translate method. That's exactly what you're looking for.
painter.translate(5, 5); // that should do it
Edit:
To allow the draw calls to only edit a specific part of the surface use QPainter's setClipRect method.
painter.setClipRect(5, 5, originalWidth - 5, originalHeight - 5);

You can set the window area too. If you take a look in documentation of QPainter, you will se two interesting methods: setWindow (which you can transform your printable area into custom coordinates) and setViewport (which you can use to limit your printable area to a given rect).

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.

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.

Uneven Circles in Connect 4 Board

I'm in the process of creating a 2P Connect 4 game, but I can't seem to get the circular areas to place tokens spaced evenly.
Here's the code that initializes the positions of each circle:
POINT tilePos;
for (int i = 0; i < Board::Dims::MAXX; ++i)
{
tileXY.push_back (std::vector<POINT> (Board::Dims::MAXY)); //add column
for (int j = 0; j < Board::Dims::MAXY; ++j)
{
tilePos.x = boardPixelDims.left + (i + 1./2) * (boardPixelDims.width / Board::Dims::MAXX);
tilePos.y = boardPixelDims.top + (j + 1./2) * (boardPixelDims.height / Board::Dims::MAXY);
tileXY.at (i).push_back (tilePos); //add circle in column
}
}
I use a 2D vector of POINTs, tileXY, to store the positions. Recall the board is 7 circles wide by 6 circles high.
My logic is such that the first circle starts (for X) at:
left + width / #circles * 0 + width / #circles / 2
and increases by width / #circles each time, which is easy to picture for smaller numbers of circles.
Later, I draw the circles like this:
for (const std::vector<POINT> &col : _tileXY)
{
for (const POINT pos : col)
{
if (g.FillEllipse (&red, (int)(pos.x - CIRCLE_RADIUS), pos.y - CIRCLE_RADIUS, CIRCLE_RADIUS, CIRCLE_RADIUS) != Gdiplus::Status::Ok)
MessageBox (_windows.gameWindow, "FillEllipse failed.", 0, MB_SYSTEMMODAL);
}
}
Those loops iterate through each element of the vector and draws each circle in red (to stand out at the moment). The int conversion is to disambiguate the function call. The first two arguments after the brush are the top-left corner, and CIRCLE_RADIUS is 50.
The problem is that my board looks like this (sorry if it hurts your eyes a bit):
As you can see, the circles are too far up and left. They're also too small, but that's easily fixed. I tried changing some ints to doubles, but ultimately ended up with this being the closest I ever got to the real pattern. The expanded formula (expanding (i + 1./2)) for the positions looks the same as well.
Have I missed a small detail, or is my whole logic behind it off?
Edit:
As requested, types:
tilePos.x: POINT (the windows API one, type used is LONG)
boardPixelDims.*: double
Board::Dims::MAXX/MAXY: enum values (integral, contain 7 and 6 respectively)
Depending on whether CIRCLE_SIZE is intended as radius or diameter, two of your parameters seem to be wrong in the FillEllipse call. If it's a diameter, then you should be setting location to pos.x - CIRCLE_SIZE/2 and pos.y - CIRCLE_SIZE/2. If it's a radius, then the height and width paramters should each be 2*CIRCLE_SIZE rather than CIRCLE_SIZE.
Update - since you changed the variable name to CIRCLE_RADIUS, the latter solution is now obviously the correct one.
The easiest way I remember what arguments the shape related functions take is to always think in rectangles. FillEllipse will just draw an ellipse to fill the rectangle you give it. x, y, width and height.
A simple experiment to practice with is if you change your calls to FillRect, get everything positioned okay, and then change them to FillEllipse.

Qt round rectangle, why corners are different?

I try to draw a round rectangle with drawRoundedRect method directly in a QPixmap (no render engine involve here exept pure Qt one ...), I double check the size of the rectangle versus the size of my pixmap :
Pixmap : QSize(50, 73)
Rectangle: QRect(0,0 48x11)
See plenty of space ...
EDIT: some code
pixmap = QPixmap(50,73); //example size that match my case
QRectF rect(0,0,48,11);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::TextAntialiasing);
painter.setWorldMatrixEnabled(false);
painter.setPen(QPen()); //no pen
painter.setBrush(QBrush(color));
painter.drawRoundedRect(rect, 2.0, 2.0);
I disabled world transformation ...
I set set transformation to unity ...
I tried several radius (1.0,2.0,3.0,4.0) ...
I change pen width, brush color ...
But it always ends with a rectamgle with 4 diferent corners ! Like that :
I directly ouptut the pixmap to a file to be sure I wasn't scraping it during the display ... same shape.
Anyone know about Qt round rectangle with small radius ? I saw somthing about it a long time ago but I don't remenber how to deal with it !
It looks like you're not using anti-aliasing (i.e. the QPainter::Antialiasing render hint). This is a Qt quirk that occurs without it. From what I've seen/heard, the Qt devs aren't terribly concerned with fixing this (most people want anti-aliasing anyway).
The work-around (besides just using anti-aliasing) is to draw the rect yourself with QPainter::drawLine() and QPainter::drawArc(). You might have to play with numbers until it looks right -- straight calculations tend to come out a pixel or two off. Also, you might find that even with this method the lower right corner is never exactly the same as the other corners.
If you're feeling mildly ambitious, you could try fixing this and submitting a patch to Qt.
Update: Arc drawing results changed in Qt 5. In my experience, it's a big improvement.
I know this is an old problem but for Qt5 users calling setRenderHint(QPainter::Qt4CompatiblePainting); on the QPainter seems to solve the problem.
Edit:
I found a solution for generating a perfect rounded rectangle together with border color and it looks the same as the rounded rectangles used by QPushButton's border for example. This is how I implemented the paintEvent to achieve this:
void MyButtonGroup::paintEvent(QPaintEvent * e)
{
int borderSize = 5;
QColor borderColor = Qt::red;
QColor backgroundColor = Qt::blue;
int borderRadius = 3;
QPen pen;
pen.setWidth(borderSize);
pen.setColor(borderColor);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(pen);
QRectF rect(rect().x() + borderSize / 2,
rect().y() + borderSize / 2,
rect().width() - borderSize,
rect().height() - borderSize);
if(borderSize % 2 == 0)
{
painter.drawRoundedRect(rect,
borderSize,
borderSize);
}
else
{
painter.drawRoundedRect(rect.translated(0.5, 0.5),
borderRadius,
borderRadius);
}
QBrush brush(backgroundColor);
pen.setBrush(brush);
painter.setBrush(brush);
if(borderSize % 2 == 0)
{
painter.drawRoundedRect(rect,
borderRadius,
borderRadius);
}
else
{
painter.drawRoundedRect(rect.translated(0.5, 0.5),
borderRadius,
borderRadius);
}
QWidget::paintEvent(e);
}
I'm posting this because I found it a bit hard to achieve this result:
Try adding half a pixel offset (e.g.: rect.translated(0.5,0.5) ):
QRectF rect(0,0,48,11);
painter.setRenderHint(QPainter::Antialiasing,false);
painter.drawRoundedRect( rect.translated(0.5,0.5), 2.0, 2.0 );
I suppose this has to do with the coordinate system placing an integer value between two pixels.
If you draw with antialiasing and use a pen of 1 pixel width then drawing at exact integer coordinates results in lines of 2 pixel width instead.
Only with this 0.5 pixel offset you'll get lines that are exactly 1 pixel wide.
QRectF rect(0,0,48,11);
painter.setRenderHint(QPainter::Antialiasing,true);
painter.setBrush(Qt::NoBrush);
painter.setPen( Qt::white );
painter.drawRoundedRect( rect.translated(0.5,0.5), 2.0,2.0 );
Best way do draw RoundRect is Path.
http://developer.nokia.com/community/wiki/Qt_rounded_rect_widget
void fillRoundRect(QPainter& painter, QRect r, int radius)
{
painter.setRenderHint(QPainter::Antialiasing,true);
QPainterPath rounded_rect;
rounded_rect.addRoundRect(r, radius, radius);
painter.setClipPath(rounded_rect);
painter.fillPath(rounded_rect,painter.brush());
painter.drawPath(rounded_rect);
}
try to play with render hints
1) disable antiAliasing;
2) enable SmoothPixmapTransform
but still no guarantee that it will help.
I have tried all tips from answers here but nothing works for me. But based on these code snippets I have found following solution:
As default set m_pPainter->setRenderHint(QPainter::Qt4CompatiblePainting, true) and only for rounded rectangles with width%2==0 disable it.
QRect rect = ConvertRectangle(rectangle);
int nPenWidth = m_pPainter->pen().width();
if ( nPenWidth % 2 == 0 )
m_pPainter->setRenderHint(QPainter::Qt4CompatiblePainting, false);
m_pPainter->drawRoundedRect(rect, dbRadiusX, dbRadiusY);
if ( nPenWidth % 2 == 0 )
m_pPainter->setRenderHint(QPainter::Qt4CompatiblePainting, true);

Calculate QGraphicsTextItem font size based on scale

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?