I'm trying to draw text on a widget at an angle which is non-perpendicular, i.e. between 0 and 90. Drawing the text itself is no issue, but the resulting text is very wiggly/unevenly drawn.
In the picture below, I'm drawing to lines of text at a 45 degree angle. The first line of text is many underscores ("_____"), and second line of text is "Multithreading". Underscores are drawn here instead of a line just to highlight the issue.
As you can see, the first line obviously shows the text is not evenly drawn. This is more subtle in the second line, but still visible.
Drawing at perpendicular angles (0, 90, 180, etc.) does not cause this effect. Any reason why this is happening?
I'm working on Windows 10 with Qt 5.7.0.
Minimal code example:
void MyWidget::paintEvent( QPaintEvent * /* event */ )
{
QFont font;
font.setPointSize( 16 );
font.setStyleStrategy( QFont::StyleStrategy::PreferAntialias );
setFont( font );
QImage image( size(), QImage::Format_ARGB32_Premultiplied );
QPainter imagePainter( &image );
imagePainter.initFrom( this );
imagePainter.setFont( font() );
imagePainter.setRenderHint( QPainter::Antialiasing, true );
imagePainter.eraseRect( rect() );
// Set logical origo in the middle of the image
m_window = QRect(
- width() / 2, // x
- height() / 2, // y
width(), // w
height() // h
);
imagePainter.setWindow( m_window );
m_viewport = QRect(
0, // x
0, // y
width(), // w
height() // h
);
imagePainter.setViewport( m_viewport );
draw( imagePainter );
imagePainter.end();
QPainter widgetPainter( this );
widgetPainter.drawImage( 0, 0, image );
}
void MyWidget::draw( QPainter & painter )
{
painter.save();
// Rotate anti-clockwise
painter.rotate( -m_degrees );
painter.drawText( m_window.top(), 0, tr( "Multithreads" ) );
painter.drawText( m_window.top(), 15, tr( "__________" ) );
painter.restore();
}
I found a workaround from this Qt bug ticket. In short, the fix is to draw the text as a QPainterPath rather than as text.
Example of the fix is
// Do this
QPainterPath glyphPath;
glyphPath.addText( x, y, painter.font(), text );
painter.fillPath( glyphPath, painter.pen().color() );
// instead of this
painter.drawText( x, y, text );
EDIT:
The difference can be seen below.
Before:
After:
Related
I need to draw triangle shape and rhombus shapes like this image.In this code which design triangle shape (figure 1) but I need to add this shape to text "TRI" . And I also need to implement this code to design rhombus shape like (figure 2). please help me to solve this.
Figure 1
void MainWindow::on_btnTri_clicked()
{
QPen redPen(Qt::black);
redPen.setWidth(2);
QRectF rect = QRectF(0, 0, 200, 200);
QPainterPath path;
path.moveTo(rect.left() + (rect.width() / 2), rect.top());
path.lineTo(rect.bottomLeft());
path.lineTo(rect.bottomRight());
path.lineTo(rect.left() + (rect.width() / 2), rect.top());
QGraphicsPathItem* item = ui->graphicsView->scene()->addPath(path, redPen);
item->setFlag(QGraphicsItem::ItemIsMovable, true);
item->setFlag(QGraphicsItem::ItemIsSelectable,true);
}
Figure 2 I use this code to design figure 2 But which cannot pass parameters to change there size,My figure 1 designed code I can able pass two parameters to QRectF(0, 0, para1, para2); this for change triangle's size.so I need to change this code to do the same thing using QPainterPath or any other way.
void MainWindow::on_btnRomb_clicked()
{
QPolygonF romb;
romb.append(QPointF(20,40));
romb.append(QPointF(0,20));
romb.append(QPointF(20,0));
romb.append(QPointF(40, 20));
QGraphicsPolygonItem* itemR = ui->graphicsView->scene()->addPolygon(romb);
itemR->setFlag(QGraphicsItem::ItemIsMovable);
}
you must use the addText() method of QPainterPath, to place it in the center you must calculate the width and height of the text for it QFontMetrics is used:
QPen redPen(Qt::black);
redPen.setWidth(2);
QRectF rect(0, 0, 200, 200);
QPainterPath path;
path.moveTo(rect.left() + (rect.width() / 2), rect.top());
path.lineTo(rect.bottomLeft());
path.lineTo(rect.bottomRight());
path.lineTo(rect.left() + (rect.width() / 2), rect.top());
path.moveTo(rect.center());
QFont font("Times", 20, QFont::Bold);
QFontMetrics fm(font);
QString text = "TRI";
QSize size = fm.size(Qt::TextSingleLine, text);
path.addText(rect.center()+ QPointF(-size.width()*0.5, size.height()*0.5), font, text);
QGraphicsPathItem *item = ui->graphicsView->scene()->addPath(path, redPen);
item->setFlag(QGraphicsItem::ItemIsMovable, true);
item->setFlag(QGraphicsItem::ItemIsSelectable,true);
For the case of the diamond you should only get the midpoints of each vertex:
QPainterPath path;
QRectF rect(0, 0 , 100, 100);
path.moveTo(rect.center().x(), rect.top());
path.lineTo(rect.right(), rect.center().y());
path.lineTo(rect.center().x(), rect.bottom());
path.lineTo(rect.left(), rect.center().y());
path.lineTo(rect.center().x(), rect.top());
QGraphicsPathItem* itemR = ui->graphicsView->scene()->addPath(path);
itemR->setFlag(QGraphicsItem::ItemIsMovable);
Symptoms
I'm having an issue with the recent version 2.0 of the Lato font in its "Regular" variant. This issue doesn't appear with Lato 1.0.
It seems that the underline is drawn 1px below the rectangle reported by DrawText() with the DT_CALCRECT flag.
In the following screenshots the calculated rectangle is indicated by the blue background. I've added 10px to the right of this rectangle so you can see the discrepancy to the position where the underline is drawn.
Lato 2.0 - underline is incorrectly drawn outside of rectangle reported by DT_CALCRECT:
Lato 1.0 - underline is correctly drawn inside of rectangle reported by DT_CALCRECT:
MCVE
can be reproduced on Win 10 and Win 7 (Win 8 not tested)
Download and install the Lato 2.0 Regular font (Lato-Regular.ttf).
Create a dialog based MFC application using the wizard and replace the OnPaint() handler of the dialog with the sample code.
Sample code for OnPaint():
CPaintDC dc{ this };
CRect rect; GetClientRect( &rect );
LOGFONTW lf{};
wcscpy_s( lf.lfFaceName, L"Lato" );
lf.lfHeight = 20 * 10; // tenths of a point
lf.lfWeight = FW_NORMAL;
lf.lfUnderline = TRUE;
CFont font;
VERIFY( font.CreatePointFontIndirect( &lf ) );
const int saved = dc.SaveDC();
dc.SelectObject( &font );
dc.SetBkMode( TRANSPARENT );
dc.SetTextColor( RGB( 0, 0, 0 ) );
const CString text = L"Hello Lato";
const DWORD dtFlags = 0;
// Calculate the size required by the text and fill this rectangle
CRect textRect = rect;
dc.DrawText( text, textRect, dtFlags | DT_CALCRECT );
textRect.right += 10;
dc.FillSolidRect( 0, 0, textRect.right, textRect.bottom, RGB( 180, 180, 255 ) );
// Actually draw the text
dc.DrawTextW( text, rect, dtFlags );
if( saved )
dc.RestoreDC( saved );
Question
Do you think this is an issue in my code, a bug in the OS or a bug in the font?
Unfortunately I can't just use Lato 1.0 as Lato 2.0 added support for many new languages my software needs to support.
There's a GDI's StrokePath API that can allow to simulate "stroking" of text using this method. I'll copy it here:
void CCanvas::DrawOutlineText( CDC& dc, const CString& Text )
{
const int RestorePoint = dc.SaveDC();
// Create new font
CFont NewFont;
NewFont.CreatePointFont( 700, TEXT( "Verdana" ), &dc );
// Use this font
dc.SelectObject( &NewFont );
// Brush for pen
LOGBRUSH lBrushForPen = { 0 };
lBrushForPen.lbColor = RGB( 200, 150, 100 );
lBrushForPen.lbHatch = HS_CROSS;
lBrushForPen.lbStyle = BS_SOLID;
// New pen for drawing outline text
CPen OutlinePen;
OutlinePen.CreatePen( PS_GEOMETRIC | PS_SOLID, 2, &lBrushForPen, 0, 0 );
// Use this pen
dc.SelectObject( &OutlinePen );
dc.SetBkMode( TRANSPARENT );
dc.BeginPath();
// This text is not drawn on screen, but instead each action is being
// recorded and stored internally as a path, since we called BeginPath
dc.TextOut( 20, 20, Text );
// Stop path
dc.EndPath();
// Now draw outline text
dc.StrokePath();
dc.RestoreDC( RestorePoint );
}
In my case I'm using DrawString function from GDI+ to draw text.
Does anyone know if there's an alternative to BeginPath, EndPath and StrokePath in GDI+ to simulate text stroking?
EDIT: Following the advice by jschroedl below, I tried the following:
CString str = L"Text";
Graphics grpx(dc.GetSafeHdc());
SolidBrush gdiBrush(Color(0xFF, 0xFF, 0, 0));
StringFormat gdiSF;
gdiSF.SetAlignment(StringAlignmentNear);
gdiSF.SetFormatFlags(StringFormatFlagsNoWrap | StringFormatFlagsNoFitBlackBox |
StringFormatFlagsNoFontFallback | StringFormatFlagsNoClip);
gdiSF.SetHotkeyPrefix(HotkeyPrefixNone);
gdiSF.SetTrimming(StringTrimmingNone);
grpx.SetTextRenderingHint(TextRenderingHintAntiAlias);
grpx.SetPixelOffsetMode(PixelOffsetModeNone);
grpx.SetInterpolationMode(InterpolationModeHighQualityBicubic);
GraphicsPath dd;
FontFamily gdiFF(L"Segoe UI");
const PointF pntF(0, 0);
dd.AddString(str, str.GetLength(), &gdiFF, FontStyleRegular, 58.0f, pntF, &gdiSF);
Pen penFF(Color(0xFF, 0xFF, 0, 0), 1.0f);
grpx.DrawPath(&penFF, &dd);
that produced quite a jagged outline (enlarged screenshot):
Any idea how to make it render with anti-aliasing?
I believe our code creates a GraphicsPath object, calls GraphicsPath::AddString() to get the path for the text and later draws the path with Graphics::DrawPath().
Update:
Based on the blocky text, I experimented and think this looks a bit smoother.
grpx.SetTextRenderingHint(TextRenderingHintClearTypeGridFit);
grpx.SetSmoothingMode(SmoothingModeHighQuality);
grpx.SetPixelOffsetMode(PixelOffsetModeHalf);
I've tried to fill circle with four images. Firstly , each photo brush in the same size ,hereafter scale final image with that size. But result is not what I want.
At the moment circle in foreground and photos on background, like here:
How to fill circle with photos and remove rectangle?
Here is my code:
QPixmap *CGlobalZone::profPicFromFourPics(QList<QPixmap> pixmapList)
{
QPixmap *avatar = NULL;
QImage roundedImage(CGlobalZone::AVATAR_WIDTH_M*2, CGlobalZone::AVATAR_HEIGHT_M*2, QImage::Format_ARGB32);
roundedImage.fill(Qt::transparent);
QBrush brush0(pixmapList[0]);
QBrush brush1(pixmapList[1]);
QBrush brush2(pixmapList[2]);
QBrush brush3(pixmapList[3]);
QPainter painter(&roundedImage);
QPen pen(QColor(176, 216, 242), 1);
painter.setRenderHint(QPainter::Antialiasing);
painter.setBrush(brush0);
painter.drawRect(0 , 0 , CGlobalZone::AVATAR_WIDTH_M , CGlobalZone::AVATAR_HEIGHT_M );
painter.setBrush(brush1);
painter.drawRect(CGlobalZone::AVATAR_WIDTH_M , 0 , CGlobalZone::AVATAR_WIDTH_M*2 , CGlobalZone::AVATAR_HEIGHT_M );
painter.setBrush(brush2);
painter.drawRect(CGlobalZone::AVATAR_WIDTH_M , CGlobalZone::AVATAR_HEIGHT_M , CGlobalZone::AVATAR_WIDTH_M*2 , CGlobalZone::AVATAR_HEIGHT_M*2 );
painter.setBrush(brush3);
painter.drawRect(0 , CGlobalZone::AVATAR_HEIGHT_M , CGlobalZone::AVATAR_WIDTH_M*2 , CGlobalZone::AVATAR_HEIGHT_M*2 );
painter.drawEllipse(0, 0, CGlobalZone::AVATAR_WIDTH_M*2-3 , CGlobalZone::AVATAR_HEIGHT_M*2-3 );
avatar = new QPixmap(QPixmap::fromImage(roundedImage).scaled(QSize(CGlobalZone::AVATAR_WIDTH_M, CGlobalZone::AVATAR_HEIGHT_M),
Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
return avatar;
}
I would do this in the following way (details in source comments):
// The avatar image. Should be four, but use one for demonstration.
QPixmap source("avatar.png");
// Initialize the avatar and bring it to a standard size.
// This step may be skipped if avatars have the same sizes.
const int width = CGlobalZone::AVATAR_WIDTH_M;
const int height = CGlobalZone::AVATAR_HEIGHT_M;
source = source.scaled(width, height);
// Set up the final image that contains four avatar images.
QPixmap target(2 * width, 2 * height);
target.fill(Qt::transparent);
QPainter painter(&target);
// Set clipped region (circle) in the center of the target image
QRegion r(QRect(width / 2, height / 2, width, height), QRegion::Ellipse);
painter.setClipRegion(r);
painter.drawPixmap(0, 0, source); // First avatar
painter.drawPixmap(width, 0, source); // Second avatar
painter.drawPixmap(0, height, source); // Third avatar
painter.drawPixmap(width, height, source); // Fourth avatar
target.save("test.png");
Use painter paths for the task. Rather than drawEllipse you should do
int dim = CGlobalZone::AVATAR_WIDTH_M*2;
QPainterPath entirePath;
QPainterPath ellipsePath;
entirePath.addRect(0, 0, dim, dim);
ellipsePath.addEllipse(0, 0, dim-3, dim-3);
QPainterPath outOfEllipse = entirePath.subtracted(ellipsePath);
painter.fillPath(outOfEllipse, QBrush(Qt::transparent));
edit: Because QPainterPath is for complex cases, you should use QRegion. After testing I found out there might small pixel errors when filling inside and outside the same path (in your case it is going to be fine).
I have a QGraphicsScene where I add a QPixmap composed of 4 images and the borders of each image.
I create a new QPixmap with the total size and then use a QPainter to draw each sub-image in the appropiate place in the bigger pixmap. After one sub-image is done, inmediately draw its borders (this may not be optimal but for now I don't mind).
Once the "final" pixmap is finished, I insert directly to the scene with
scene->addPixmap( total )
Here's the code for the pixmap composition:
QPixmap pixFromCube( PanoramicImages* lim ) const
{
const QSize img_size = getImageSize( lim );
const QSize pano_size( img_size.width() * 4, img_size.height() );
QPixmap toret( pano_size );
if( !toret.isNull() ) {
QPainter painter( &toret );
painter.setRenderHint( QPainter::Antialiasing );
int x( 0 );
QPixmap pix = lim->getCamera1Image();
if( !pix.isNull() ) {
painter.drawPixmap( 0, 0, pix.width(), pix.height(), pix );
drawPixBorder( painter, pix.rect() );
}
x += img_size.width();
pix = lim->getCamera2Image();
if( !pix.isNull() ) {
painter.drawPixmap( x, 0, pix.width(), pix.height(), pix );
drawPixBorder( painter, QRectF( x, 0, pix.width(),
pix.height() ) )
;
}
x += img_size.width();
pix = lim->getCamera3Image();
if( !pix.isNull() ) {
painter.drawPixmap( x, 0, pix.width(), pix.height(), pix );
drawPixBorder( painter, QRectF( x, 0, pix.width(),
pix.height() ) )
;
}
x += img_size.width();
pix = lim->getCamera4Image();
if( !pix.isNull() ) {
painter.drawPixmap( x, 0, pix.width(), pix.height(), pix );
drawPixBorder( painter, QRectF( x, 0, pix.width(),
pix.height() ) )
;
}
}
return toret;
}
And
void drawPixBorder( QPainter& painter, const QRectF rect ) const
{
const QBrush oldBrush = painter.brush();
const QPen oldPen = painter.pen();
QColor color( Qt::blue );
if( timer.isActive() ) {
color = Qt::green;
} else {
color = Qt::red;
}
const QBrush brush( color );
QPen pen( brush, 22 );
const QPointF points[ 5 ] = {
rect.topLeft(),
rect.topRight(),
rect.bottomRight(),
rect.bottomLeft(),
rect.topLeft()
};
painter.setBrush( brush );
painter.setPen( pen );
painter.drawPolyline( points, sizeof( points ) / sizeof( points[ 0 ] ) );
painter.setBrush( oldBrush );
painter.setPen( oldPen );
}
Here's the final pixmap when it's loaded for the first time:
And here after a few zoom-outs:
As you can see, at the right some of the borders are missing. When zooming back again to the inital position, the borders are displayed. If I use a smaller width for the lines (say, 5), the borders disappear sooner.
I've been reading other questions here and in the Qt Forums and tried some suggestions like:
pen.setCosmetic( true );
or
painter.setRenderHint( QPainter::NonCosmeticDefaultPen, false);
or:
painter.setRenderHint( QPainter::Antialiasing );
setting the pen width directly to 0
pen.setWidth( 0 )
and combinations.
Neither of them prevented the borders to disappear and using a bigger width just delays the problem.
Is there a way to show always the borders regardless of the zoom level?
Thanks to #Robert for his help. As he has stated in his answer, the solution was to draw directly in the scene, instead of doing it in the pixmap and then adding it.
For drawing in the scene, I decided to use a QPainterPath:
int x( 0 );
QPainterPath rectPath;
for( unsigned int i( 0 ); i < 4; ++i ) {
rectPath.addRect( QRectF( x, 0, width, height ) );
x += width;
}
QColor color( Qt::blue );
if( timer.isActive() ) {
color = Qt::green;
} else {
color = Qt::red;
}
scene->addPath( rectPath, QPen( color ) );
It is because the painter you are using to create the pixmap does not know anything about the transformations/scale of the graphic scene... A possible solutions would be to draw the rectangles within the scene and not directly to the pixmap.