Skip to content
Permalink
Browse files

Merge pull request #8242 from 3nids/final_dpi

 [fix #17773] fix HiDPI in map canvas on mac
  • Loading branch information
3nids committed Oct 19, 2018
2 parents 345d25f + ff1face commit fb2883c79cdb65ea5bf05c7073c7b7cd92615331
@@ -21,10 +21,12 @@ The rendering itself is done by QgsMapRendererJob subclasses.
In order to set up QgsMapSettings instance, it is necessary to set at least
few members: extent, output size and layers.

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

.. versionadded:: 2.4
%End
@@ -58,6 +60,31 @@ Returns the size of the resulting map image
void setOutputSize( QSize size );
%Docstring
Sets the size of the resulting map image
%End

float devicePixelRatio() const;
%Docstring
Returns device pixel ratio
Common values are 1 for normal-dpi displays and 2 for high-dpi "retina" displays.

.. versionadded:: 3.4
%End

void setDevicePixelRatio( float dpr );
%Docstring
Sets the device pixel ratio
Common values are 1 for normal-dpi displays and 2 for high-dpi "retina" displays.

.. versionadded:: 3.4
%End

QSize deviceOutputSize() const;
%Docstring
Returns the device output size of the map canvas
This is equivalent to the output size multiplicated
by the device pixel ratio.

.. versionadded:: 3.4
%End

double rotation() const;
@@ -60,14 +60,17 @@ void QgsMapRendererCustomPainterJob::start()
prepareTime.start();

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

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

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

mLabelingEngineV2.reset();
@@ -313,6 +313,7 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter *painter, QgsLabelingEn
job.cached = true;
job.imageInitialized = true;
job.img = new QImage( mCache->cacheImage( ml->id() ) );
job.img->setDevicePixelRatio( mSettings.devicePixelRatio() );
job.renderer = nullptr;
job.context.setPainter( nullptr );
continue;
@@ -324,10 +325,9 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter *painter, QgsLabelingEn
if ( mCache || !painter || needTemporaryImage( ml ) )
{
// Flattened image for drawing when a blending mode is set
QImage *mypFlattenedImage = nullptr;
mypFlattenedImage = new QImage( mSettings.outputSize().width(),
mSettings.outputSize().height(),
mSettings.outputImageFormat() );
QImage *mypFlattenedImage = new QImage( mSettings.deviceOutputSize(),
mSettings.outputImageFormat() );
mypFlattenedImage->setDevicePixelRatio( mSettings.devicePixelRatio() );
if ( mypFlattenedImage->isNull() )
{
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
job.cached = true;
job.complete = true;
job.img = new QImage( mCache->cacheImage( LABEL_CACHE_ID ) );
Q_ASSERT( job.img->devicePixelRatio() == mSettings.devicePixelRatio() );
job.context.setPainter( nullptr );
}
else
@@ -374,9 +375,9 @@ LabelRenderJob QgsMapRendererJob::prepareLabelingJob( QPainter *painter, QgsLabe
{
// Flattened image for drawing labels
QImage *mypFlattenedImage = nullptr;
mypFlattenedImage = new QImage( mSettings.outputSize().width(),
mSettings.outputSize().height(),
mypFlattenedImage = new QImage( mSettings.deviceOutputSize(),
mSettings.outputImageFormat() );
mypFlattenedImage->setDevicePixelRatio( mSettings.devicePixelRatio() );
if ( mypFlattenedImage->isNull() )
{
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 )

QImage QgsMapRendererJob::composeImage( const QgsMapSettings &settings, const LayerRenderJobs &jobs, const LabelRenderJob &labelJob )
{
QImage image( settings.outputSize(), settings.outputImageFormat() );
QImage image( settings.deviceOutputSize(), settings.outputImageFormat() );
image.setDevicePixelRatio( settings.devicePixelRatio() );
image.fill( settings.backgroundColor().rgba() );

QPainter painter( &image );
@@ -25,7 +25,8 @@ QgsMapRendererSequentialJob::QgsMapRendererSequentialJob( const QgsMapSettings &
{
QgsDebugMsgLevel( QStringLiteral( "SEQUENTIAL construct" ), 5 );

mImage = QImage( mSettings.outputSize(), mSettings.outputImageFormat() );
mImage = QImage( mSettings.deviceOutputSize(), mSettings.outputImageFormat() );
mImage.setDevicePixelRatio( mSettings.devicePixelRatio() );
mImage.setDotsPerMeterX( 1000 * settings.outputDpi() / 25.4 );
mImage.setDotsPerMeterY( 1000 * settings.outputDpi() / 25.4 );
mImage.fill( Qt::transparent );
@@ -146,8 +146,8 @@ void QgsMapSettings::updateDerived()
}
}

double myHeight = mSize.height();
double myWidth = mSize.width();
double myHeight = mSize.height() * mDevicePixelRatio;
double myWidth = mSize.width() * mDevicePixelRatio;

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

float QgsMapSettings::devicePixelRatio() const
{
return mDevicePixelRatio;
}

void QgsMapSettings::setDevicePixelRatio( float dpr )
{
mDevicePixelRatio = dpr;
}

QSize QgsMapSettings::deviceOutputSize() const
{
return outputSize() * mDevicePixelRatio;
}

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

/**
* Returns device pixel ratio
* Common values are 1 for normal-dpi displays and 2 for high-dpi "retina" displays.
* \since QGIS 3.4
*/
float devicePixelRatio() const;

/**
* Sets the device pixel ratio
* Common values are 1 for normal-dpi displays and 2 for high-dpi "retina" displays.
* \since QGIS 3.4
*/
void setDevicePixelRatio( float dpr );

/**
* Returns the device output size of the map canvas
* This is equivalent to the output size multiplicated
* by the device pixel ratio.
* \since QGIS 3.4
*/
QSize deviceOutputSize() const;

/**
* Returns the rotation of the resulting map image, in degrees clockwise.
* \see setRotation()
@@ -403,6 +427,7 @@ class CORE_EXPORT QgsMapSettings
double mDpi;

QSize mSize;
float mDevicePixelRatio = 1.0;

QgsRectangle mExtent;

@@ -31,9 +31,11 @@ email : sherman at mrcc.com
#include <QRect>
#include <QTextStream>
#include <QResizeEvent>
#include <QScreen>
#include <QString>
#include <QStringList>
#include <QWheelEvent>
#include <QWindow>

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

QSize s = viewport()->size();
mSettings.setOutputSize( s );
mSettings.setDevicePixelRatio( devicePixelRatio() );
setSceneRect( 0, 0, s.width(), s.height() );
mScene->setSceneRect( QRectF( 0, 0, s.width(), s.height() ) );

moveCanvasContents( true );

// keep device pixel ratio up to date on screen or resolution change
connect( window()->windowHandle(), &QWindow::screenChanged, this, [ = ]( QScreen * ) {mSettings.setDevicePixelRatio( devicePixelRatio() );} );
if ( window()->windowHandle() )
connect( window()->windowHandle()->screen(), &QScreen::physicalDotsPerInchChanged, [ = ]( qreal ) {mSettings.setDevicePixelRatio( devicePixelRatio() );} );

connect( &mMapUpdateTimer, &QTimer::timeout, this, &QgsMapCanvas::mapUpdateTimeout );
mMapUpdateTimer.setInterval( 250 );

@@ -682,7 +690,8 @@ QgsRectangle QgsMapCanvas::imageRect( const QImage &img, const QgsMapSettings &m
// expects (encoding of position and size of the item)
const QgsMapToPixel &m2p = mapSettings.mapToPixel();
QgsPointXY topLeft = m2p.toMapCoordinates( 0, 0 );
double res = m2p.mapUnitsPerPixel();
Q_ASSERT( img.devicePixelRatio() == mapSettings.devicePixelRatio() );
double res = m2p.mapUnitsPerPixel() / img.devicePixelRatioF();
QgsRectangle rect( topLeft.x(), topLeft.y(), topLeft.x() + img.width()*res, topLeft.y() - img.height()*res );
return rect;
}
@@ -60,12 +60,20 @@ QRectF QgsMapCanvasMap::boundingRect() const

void QgsMapCanvasMap::paint( QPainter *painter )
{
int w = std::round( mItemSize.width() ) - 2, h = std::round( mItemSize.height() ) - 2; // setRect() makes the size +2 :-(
if ( mImage.size() != QSize( w, h ) )
// setRect() makes the size +2 :-(
int w = std::round( mItemSize.width() ) - 2;
int h = std::round( mItemSize.height() ) - 2;

bool scale = false;
if ( mImage.size() != QSize( w, h )*mImage.devicePixelRatioF() )
{
QgsDebugMsg( QStringLiteral( "map paint DIFFERENT SIZE: img %1,%2 item %3,%4" ).arg( mImage.width() ).arg( mImage.height() ).arg( w ).arg( h ) );
QgsDebugMsg( QStringLiteral( "map paint DIFFERENT SIZE: img %1,%2 item %3,%4" )
.arg( mImage.width() / mImage.devicePixelRatioF() )
.arg( mImage.height() / mImage.devicePixelRatioF() )
.arg( w ).arg( h ) );
// This happens on zoom events when ::paint is called before
// the renderer has completed
scale = true;
}

/*Offset between 0/0 and mRect.xMinimum/mRect.yMinimum.
@@ -83,7 +91,10 @@ void QgsMapCanvasMap::paint( QPainter *painter )
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() ) );
}

painter->drawImage( QRect( 0, 0, w, h ), mImage );
if ( scale )
painter->drawImage( QRect( 0, 0, w, h ), mImage );
else
painter->drawImage( 0, 0, mImage );

// For debugging:
#if 0

0 comments on commit fb2883c

Please sign in to comment.
You can’t perform that action at this time.