Skip to content

Commit fb2883c

Browse files
authored
Merge pull request #8242 from 3nids/final_dpi
[fix #17773] fix HiDPI in map canvas on mac
2 parents 345d25f + ff1face commit fb2883c

8 files changed

+119
-26
lines changed

python/core/auto_generated/qgsmapsettings.sip.in

+31-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ The rendering itself is done by QgsMapRendererJob subclasses.
2121
In order to set up QgsMapSettings instance, it is necessary to set at least
2222
few members: extent, output size and layers.
2323

24-
QgsMapSettings and QgsMapRendererJob (+subclasses) are intended to replace
25-
QgsMapRenderer class that existed before QGIS 2.4. The advantage of the new
26-
classes is that they separate the settings from the rendering and provide
27-
asynchronous API for map rendering.
24+
Some systems use high DPI scaling that is an alternative to the traditional
25+
DPI scaling. The operating system provides Qt with a scaling ratio and it
26+
scales window, event, and desktop geometry. The Cocoa platform plugin sets
27+
the scaling ratio as QWindow.devicePixelRatio().
28+
To properly render the map on such systems, the map settings device pixel
29+
ratio shall be set accordingly.
2830

2931
.. versionadded:: 2.4
3032
%End
@@ -58,6 +60,31 @@ Returns the size of the resulting map image
5860
void setOutputSize( QSize size );
5961
%Docstring
6062
Sets the size of the resulting map image
63+
%End
64+
65+
float devicePixelRatio() const;
66+
%Docstring
67+
Returns device pixel ratio
68+
Common values are 1 for normal-dpi displays and 2 for high-dpi "retina" displays.
69+
70+
.. versionadded:: 3.4
71+
%End
72+
73+
void setDevicePixelRatio( float dpr );
74+
%Docstring
75+
Sets the device pixel ratio
76+
Common values are 1 for normal-dpi displays and 2 for high-dpi "retina" displays.
77+
78+
.. versionadded:: 3.4
79+
%End
80+
81+
QSize deviceOutputSize() const;
82+
%Docstring
83+
Returns the device output size of the map canvas
84+
This is equivalent to the output size multiplicated
85+
by the device pixel ratio.
86+
87+
.. versionadded:: 3.4
6188
%End
6289

6390
double rotation() const;

src/core/qgsmaprenderercustompainterjob.cpp

+6-3
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,17 @@ void QgsMapRendererCustomPainterJob::start()
6060
prepareTime.start();
6161

6262
// clear the background
63-
mPainter->fillRect( 0, 0, mSettings.outputSize().width(), mSettings.outputSize().height(), mSettings.backgroundColor() );
63+
mPainter->fillRect( 0, 0, mSettings.deviceOutputSize().width(), mSettings.deviceOutputSize().height(), mSettings.backgroundColor() );
6464

6565
mPainter->setRenderHint( QPainter::Antialiasing, mSettings.testFlag( QgsMapSettings::Antialiasing ) );
6666

6767
#ifndef QT_NO_DEBUG
6868
QPaintDevice *paintDevice = mPainter->device();
69-
QString errMsg = QStringLiteral( "pre-set DPI not equal to painter's DPI (%1 vs %2)" ).arg( paintDevice->logicalDpiX() ).arg( mSettings.outputDpi() );
70-
Q_ASSERT_X( qgsDoubleNear( paintDevice->logicalDpiX(), mSettings.outputDpi() ), "Job::startRender()", errMsg.toLatin1().data() );
69+
QString errMsg = QStringLiteral( "pre-set DPI not equal to painter's DPI (%1 vs %2)" )
70+
.arg( paintDevice->logicalDpiX() )
71+
.arg( mSettings.outputDpi() );
72+
Q_ASSERT_X( qgsDoubleNear( paintDevice->logicalDpiX(), mSettings.outputDpi() ),
73+
"Job::startRender()", errMsg.toLatin1().data() );
7174
#endif
7275

7376
mLabelingEngineV2.reset();

src/core/qgsmaprendererjob.cpp

+9-7
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter *painter, QgsLabelingEn
313313
job.cached = true;
314314
job.imageInitialized = true;
315315
job.img = new QImage( mCache->cacheImage( ml->id() ) );
316+
job.img->setDevicePixelRatio( mSettings.devicePixelRatio() );
316317
job.renderer = nullptr;
317318
job.context.setPainter( nullptr );
318319
continue;
@@ -324,10 +325,9 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter *painter, QgsLabelingEn
324325
if ( mCache || !painter || needTemporaryImage( ml ) )
325326
{
326327
// Flattened image for drawing when a blending mode is set
327-
QImage *mypFlattenedImage = nullptr;
328-
mypFlattenedImage = new QImage( mSettings.outputSize().width(),
329-
mSettings.outputSize().height(),
330-
mSettings.outputImageFormat() );
328+
QImage *mypFlattenedImage = new QImage( mSettings.deviceOutputSize(),
329+
mSettings.outputImageFormat() );
330+
mypFlattenedImage->setDevicePixelRatio( mSettings.devicePixelRatio() );
331331
if ( mypFlattenedImage->isNull() )
332332
{
333333
mErrors.append( Error( ml->id(), tr( "Insufficient memory for image %1x%2" ).arg( mSettings.outputSize().width() ).arg( mSettings.outputSize().height() ) ) );
@@ -366,6 +366,7 @@ LabelRenderJob QgsMapRendererJob::prepareLabelingJob( QPainter *painter, QgsLabe
366366
job.cached = true;
367367
job.complete = true;
368368
job.img = new QImage( mCache->cacheImage( LABEL_CACHE_ID ) );
369+
Q_ASSERT( job.img->devicePixelRatio() == mSettings.devicePixelRatio() );
369370
job.context.setPainter( nullptr );
370371
}
371372
else
@@ -374,9 +375,9 @@ LabelRenderJob QgsMapRendererJob::prepareLabelingJob( QPainter *painter, QgsLabe
374375
{
375376
// Flattened image for drawing labels
376377
QImage *mypFlattenedImage = nullptr;
377-
mypFlattenedImage = new QImage( mSettings.outputSize().width(),
378-
mSettings.outputSize().height(),
378+
mypFlattenedImage = new QImage( mSettings.deviceOutputSize(),
379379
mSettings.outputImageFormat() );
380+
mypFlattenedImage->setDevicePixelRatio( mSettings.devicePixelRatio() );
380381
if ( mypFlattenedImage->isNull() )
381382
{
382383
mErrors.append( Error( QStringLiteral( "labels" ), tr( "Insufficient memory for label image %1x%2" ).arg( mSettings.outputSize().width() ).arg( mSettings.outputSize().height() ) ) );
@@ -447,7 +448,8 @@ void QgsMapRendererJob::cleanupLabelJob( LabelRenderJob &job )
447448

448449
QImage QgsMapRendererJob::composeImage( const QgsMapSettings &settings, const LayerRenderJobs &jobs, const LabelRenderJob &labelJob )
449450
{
450-
QImage image( settings.outputSize(), settings.outputImageFormat() );
451+
QImage image( settings.deviceOutputSize(), settings.outputImageFormat() );
452+
image.setDevicePixelRatio( settings.devicePixelRatio() );
451453
image.fill( settings.backgroundColor().rgba() );
452454

453455
QPainter painter( &image );

src/core/qgsmaprenderersequentialjob.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ QgsMapRendererSequentialJob::QgsMapRendererSequentialJob( const QgsMapSettings &
2525
{
2626
QgsDebugMsgLevel( QStringLiteral( "SEQUENTIAL construct" ), 5 );
2727

28-
mImage = QImage( mSettings.outputSize(), mSettings.outputImageFormat() );
28+
mImage = QImage( mSettings.deviceOutputSize(), mSettings.outputImageFormat() );
29+
mImage.setDevicePixelRatio( mSettings.devicePixelRatio() );
2930
mImage.setDotsPerMeterX( 1000 * settings.outputDpi() / 25.4 );
3031
mImage.setDotsPerMeterY( 1000 * settings.outputDpi() / 25.4 );
3132
mImage.fill( Qt::transparent );

src/core/qgsmapsettings.cpp

+17-2
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ void QgsMapSettings::updateDerived()
146146
}
147147
}
148148

149-
double myHeight = mSize.height();
150-
double myWidth = mSize.width();
149+
double myHeight = mSize.height() * mDevicePixelRatio;
150+
double myWidth = mSize.width() * mDevicePixelRatio;
151151

152152
if ( !myWidth || !myHeight )
153153
{
@@ -230,6 +230,21 @@ void QgsMapSettings::setOutputSize( QSize size )
230230
updateDerived();
231231
}
232232

233+
float QgsMapSettings::devicePixelRatio() const
234+
{
235+
return mDevicePixelRatio;
236+
}
237+
238+
void QgsMapSettings::setDevicePixelRatio( float dpr )
239+
{
240+
mDevicePixelRatio = dpr;
241+
}
242+
243+
QSize QgsMapSettings::deviceOutputSize() const
244+
{
245+
return outputSize() * mDevicePixelRatio;
246+
}
247+
233248
double QgsMapSettings::outputDpi() const
234249
{
235250
return mDpi;

src/core/qgsmapsettings.h

+29-4
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ class QgsMapRendererJob;
4848
* In order to set up QgsMapSettings instance, it is necessary to set at least
4949
* few members: extent, output size and layers.
5050
*
51-
* QgsMapSettings and QgsMapRendererJob (+subclasses) are intended to replace
52-
* QgsMapRenderer class that existed before QGIS 2.4. The advantage of the new
53-
* classes is that they separate the settings from the rendering and provide
54-
* asynchronous API for map rendering.
51+
* Some systems use high DPI scaling that is an alternative to the traditional
52+
* DPI scaling. The operating system provides Qt with a scaling ratio and it
53+
* scales window, event, and desktop geometry. The Cocoa platform plugin sets
54+
* the scaling ratio as QWindow::devicePixelRatio().
55+
* To properly render the map on such systems, the map settings device pixel
56+
* ratio shall be set accordingly.
5557
*
5658
* \since QGIS 2.4
5759
*/
@@ -81,6 +83,28 @@ class CORE_EXPORT QgsMapSettings
8183
//! Sets the size of the resulting map image
8284
void setOutputSize( QSize size );
8385

86+
/**
87+
* Returns device pixel ratio
88+
* Common values are 1 for normal-dpi displays and 2 for high-dpi "retina" displays.
89+
* \since QGIS 3.4
90+
*/
91+
float devicePixelRatio() const;
92+
93+
/**
94+
* Sets the device pixel ratio
95+
* Common values are 1 for normal-dpi displays and 2 for high-dpi "retina" displays.
96+
* \since QGIS 3.4
97+
*/
98+
void setDevicePixelRatio( float dpr );
99+
100+
/**
101+
* Returns the device output size of the map canvas
102+
* This is equivalent to the output size multiplicated
103+
* by the device pixel ratio.
104+
* \since QGIS 3.4
105+
*/
106+
QSize deviceOutputSize() const;
107+
84108
/**
85109
* Returns the rotation of the resulting map image, in degrees clockwise.
86110
* \see setRotation()
@@ -403,6 +427,7 @@ class CORE_EXPORT QgsMapSettings
403427
double mDpi;
404428

405429
QSize mSize;
430+
float mDevicePixelRatio = 1.0;
406431

407432
QgsRectangle mExtent;
408433

src/gui/qgsmapcanvas.cpp

+10-1
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ email : sherman at mrcc.com
3131
#include <QRect>
3232
#include <QTextStream>
3333
#include <QResizeEvent>
34+
#include <QScreen>
3435
#include <QString>
3536
#include <QStringList>
3637
#include <QWheelEvent>
38+
#include <QWindow>
3739

3840
#include "qgis.h"
3941
#include "qgssettings.h"
@@ -163,11 +165,17 @@ QgsMapCanvas::QgsMapCanvas( QWidget *parent )
163165

164166
QSize s = viewport()->size();
165167
mSettings.setOutputSize( s );
168+
mSettings.setDevicePixelRatio( devicePixelRatio() );
166169
setSceneRect( 0, 0, s.width(), s.height() );
167170
mScene->setSceneRect( QRectF( 0, 0, s.width(), s.height() ) );
168171

169172
moveCanvasContents( true );
170173

174+
// keep device pixel ratio up to date on screen or resolution change
175+
connect( window()->windowHandle(), &QWindow::screenChanged, this, [ = ]( QScreen * ) {mSettings.setDevicePixelRatio( devicePixelRatio() );} );
176+
if ( window()->windowHandle() )
177+
connect( window()->windowHandle()->screen(), &QScreen::physicalDotsPerInchChanged, [ = ]( qreal ) {mSettings.setDevicePixelRatio( devicePixelRatio() );} );
178+
171179
connect( &mMapUpdateTimer, &QTimer::timeout, this, &QgsMapCanvas::mapUpdateTimeout );
172180
mMapUpdateTimer.setInterval( 250 );
173181

@@ -682,7 +690,8 @@ QgsRectangle QgsMapCanvas::imageRect( const QImage &img, const QgsMapSettings &m
682690
// expects (encoding of position and size of the item)
683691
const QgsMapToPixel &m2p = mapSettings.mapToPixel();
684692
QgsPointXY topLeft = m2p.toMapCoordinates( 0, 0 );
685-
double res = m2p.mapUnitsPerPixel();
693+
Q_ASSERT( img.devicePixelRatio() == mapSettings.devicePixelRatio() );
694+
double res = m2p.mapUnitsPerPixel() / img.devicePixelRatioF();
686695
QgsRectangle rect( topLeft.x(), topLeft.y(), topLeft.x() + img.width()*res, topLeft.y() - img.height()*res );
687696
return rect;
688697
}

src/gui/qgsmapcanvasmap.cpp

+15-4
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,20 @@ QRectF QgsMapCanvasMap::boundingRect() const
6060

6161
void QgsMapCanvasMap::paint( QPainter *painter )
6262
{
63-
int w = std::round( mItemSize.width() ) - 2, h = std::round( mItemSize.height() ) - 2; // setRect() makes the size +2 :-(
64-
if ( mImage.size() != QSize( w, h ) )
63+
// setRect() makes the size +2 :-(
64+
int w = std::round( mItemSize.width() ) - 2;
65+
int h = std::round( mItemSize.height() ) - 2;
66+
67+
bool scale = false;
68+
if ( mImage.size() != QSize( w, h )*mImage.devicePixelRatioF() )
6569
{
66-
QgsDebugMsg( QStringLiteral( "map paint DIFFERENT SIZE: img %1,%2 item %3,%4" ).arg( mImage.width() ).arg( mImage.height() ).arg( w ).arg( h ) );
70+
QgsDebugMsg( QStringLiteral( "map paint DIFFERENT SIZE: img %1,%2 item %3,%4" )
71+
.arg( mImage.width() / mImage.devicePixelRatioF() )
72+
.arg( mImage.height() / mImage.devicePixelRatioF() )
73+
.arg( w ).arg( h ) );
6774
// This happens on zoom events when ::paint is called before
6875
// the renderer has completed
76+
scale = true;
6977
}
7078

7179
/*Offset between 0/0 and mRect.xMinimum/mRect.yMinimum.
@@ -83,7 +91,10 @@ void QgsMapCanvasMap::paint( QPainter *painter )
8391
painter->drawImage( QRectF( ul.x(), ul.y(), lr.x() - ul.x(), lr.y() - ul.y() ), imIt->first, QRect( 0, 0, imIt->first.width(), imIt->first.height() ) );
8492
}
8593

86-
painter->drawImage( QRect( 0, 0, w, h ), mImage );
94+
if ( scale )
95+
painter->drawImage( QRect( 0, 0, w, h ), mImage );
96+
else
97+
painter->drawImage( 0, 0, mImage );
8798

8899
// For debugging:
89100
#if 0

0 commit comments

Comments
 (0)