[FEATURE][composer] Add option for controlling placement of rendered images inside a picture item (eg, top left, bottom right, etc)

This commit is contained in:
Nyall Dawson 2014-05-15 11:58:13 +10:00
parent 061941e119
commit fa0f0bd176
14 changed files with 369 additions and 55 deletions

View File

@ -117,6 +117,22 @@ class QgsComposerPicture: QgsComposerItem
*/
ResizeMode resizeMode() const;
/**Sets the picture's anchor point, which controls how it is placed
* within the picture item's frame.
* @param anchor anchor point for picture
* @note added in 2.3
* @see pictureAnchor
*/
void setPictureAnchor( QgsComposerItem::ItemPositionMode anchor );
/**Returns the picture's current anchor, which controls how it is placed
* within the picture item's frame.
* @returns anchor point for picture
* @note added in 2.3
* @see setPictureAnchor
*/
ItemPositionMode pictureAnchor() const;
/**Returns whether the picture item is using an expression for the image source.
* @returns true if the picture is using an expression for the source, false if
* it is using a single static file path for the source.

View File

@ -245,6 +245,29 @@ void QgsComposerPictureWidget::on_mResizeModeComboBox_currentIndexChanged( int i
//disable picture rotation for non-zoom modes
mRotationGroupBox->setEnabled( mPicture->resizeMode() == QgsComposerPicture::Zoom );
//disable anchor point control for certain zoom modes
if ( mPicture->resizeMode() == QgsComposerPicture::Zoom ||
mPicture->resizeMode() == QgsComposerPicture::Clip )
{
mAnchorPointComboBox->setEnabled( true );
}
else
{
mAnchorPointComboBox->setEnabled( false );
}
}
void QgsComposerPictureWidget::on_mAnchorPointComboBox_currentIndexChanged( int index )
{
if ( !mPicture )
{
return;
}
mPicture->beginCommand( tr( "Picture placement changed" ) );
mPicture->setPictureAnchor(( QgsComposerItem::ItemPositionMode )index );
mPicture->endCommand();
}
void QgsComposerPictureWidget::on_mRadioPath_clicked()
@ -422,12 +445,12 @@ void QgsComposerPictureWidget::setGuiElementValues()
mComposerMapComboBox->blockSignals( true );
mRotationFromComposerMapCheckBox->blockSignals( true );
mResizeModeComboBox->blockSignals( true );
mAnchorPointComboBox->blockSignals( true );
mRadioPath->blockSignals( true );
mRadioExpression->blockSignals( true );
mPictureExpressionLineEdit->blockSignals( true );
mPictureLineEdit->setText( mPicture->pictureFile() );
// QRectF pictureRect = mPicture->rect();
mPictureRotationSpinBox->setValue( mPicture->pictureRotation() );
refreshMapComboBox();
@ -455,6 +478,18 @@ void QgsComposerPictureWidget::setGuiElementValues()
//disable picture rotation for non-zoom modes
mRotationGroupBox->setEnabled( mPicture->resizeMode() == QgsComposerPicture::Zoom );
mAnchorPointComboBox->setCurrentIndex(( int )mPicture->pictureAnchor() );
//disable anchor point control for certain zoom modes
if ( mPicture->resizeMode() == QgsComposerPicture::Zoom ||
mPicture->resizeMode() == QgsComposerPicture::Clip )
{
mAnchorPointComboBox->setEnabled( true );
}
else
{
mAnchorPointComboBox->setEnabled( false );
}
mRadioPath->setChecked( !( mPicture->usePictureExpression() ) );
mRadioExpression->setChecked( mPicture->usePictureExpression() );
mPictureLineEdit->setEnabled( !( mPicture->usePictureExpression() ) );
@ -469,6 +504,7 @@ void QgsComposerPictureWidget::setGuiElementValues()
mPictureLineEdit->blockSignals( false );
mComposerMapComboBox->blockSignals( false );
mResizeModeComboBox->blockSignals( false );
mAnchorPointComboBox->blockSignals( false );
mRadioPath->blockSignals( false );
mRadioExpression->blockSignals( false );
mPictureExpressionLineEdit->blockSignals( false );

View File

@ -30,6 +30,7 @@ class QgsComposerPictureWidget: public QWidget, private Ui::QgsComposerPictureWi
Q_OBJECT
public:
QgsComposerPictureWidget( QgsComposerPicture* picture );
~QgsComposerPictureWidget();
@ -47,6 +48,7 @@ class QgsComposerPictureWidget: public QWidget, private Ui::QgsComposerPictureWi
void on_mRotationFromComposerMapCheckBox_stateChanged( int state );
void on_mComposerMapComboBox_activated( const QString & text );
void on_mResizeModeComboBox_currentIndexChanged( int index );
void on_mAnchorPointComboBox_currentIndexChanged( int index );
void on_mRadioPath_clicked();
void on_mRadioExpression_clicked();
void setPictureExpression();

View File

@ -38,6 +38,7 @@ QgsComposerPicture::QgsComposerPicture( QgsComposition *composition ) :
mPictureRotation( 0 ),
mRotationMap( 0 ),
mResizeMode( QgsComposerPicture::Zoom ),
mPictureAnchor( UpperLeft ),
mPictureExpr( 0 )
{
mPictureWidth = rect().width();
@ -50,6 +51,7 @@ QgsComposerPicture::QgsComposerPicture() : QgsComposerItem( 0 ),
mPictureRotation( 0 ),
mRotationMap( 0 ),
mResizeMode( QgsComposerPicture::Zoom ),
mPictureAnchor( UpperLeft ),
mPictureExpr( 0 )
{
mPictureHeight = rect().height();
@ -90,40 +92,99 @@ void QgsComposerPicture::paint( QPainter* painter, const QStyleOptionGraphicsIte
//int newDpi = ( painter->device()->logicalDpiX() + painter->device()->logicalDpiY() ) / 2;
//picture resizing
if ( mMode != Unknown )
{
double boundRectWidthMM;
double boundRectHeightMM;
double imageRectWidthMM;
double imageRectHeightMM;
QRect imageRect;
if ( mResizeMode == QgsComposerPicture::Zoom || mResizeMode == QgsComposerPicture::ZoomResizeFrame )
{
boundRectWidthMM = mPictureWidth;
boundRectHeightMM = mPictureHeight;
imageRectWidthMM = mImage.width();
imageRectHeightMM = mImage.height();
imageRect = QRect( 0, 0, mImage.width(), mImage.height() );
}
else if ( mResizeMode == QgsComposerPicture::Stretch )
{
boundRectWidthMM = rect().width();
boundRectHeightMM = rect().height();
imageRectWidthMM = mImage.width();
imageRectHeightMM = mImage.height();
imageRect = QRect( 0, 0, mImage.width(), mImage.height() );
}
else if ( mResizeMode == QgsComposerPicture::Clip )
{
boundRectWidthMM = rect().width();
boundRectHeightMM = rect().height();
int imageRectWidthPixels = mImage.width();
int imageRectHeightPixels = mImage.height();
imageRect = clippedImageRect( boundRectWidthMM, boundRectHeightMM ,
QSize( imageRectWidthPixels, imageRectHeightPixels ) );
}
else
{
boundRectWidthMM = rect().width();
boundRectHeightMM = rect().height();
imageRectWidthMM = rect().width() * mComposition->printResolution() / 25.4;
imageRectHeightMM = rect().height() * mComposition->printResolution() / 25.4;
imageRect = QRect( 0, 0, rect().width() * mComposition->printResolution() / 25.4,
rect().height() * mComposition->printResolution() / 25.4 );
}
painter->save();
//zoom mode - calculate anchor point and rotation
if ( mResizeMode == Zoom )
{
painter->translate( rect().width() / 2.0, rect().height() / 2.0 );
painter->rotate( mPictureRotation );
painter->translate( -boundRectWidthMM / 2.0, -boundRectHeightMM / 2.0 );
//TODO - allow placement modes with rotation set. for now, setting a rotation
//always places picture in center of frame
if ( mPictureRotation != 0 )
{
painter->translate( rect().width() / 2.0, rect().height() / 2.0 );
painter->rotate( mPictureRotation );
painter->translate( -boundRectWidthMM / 2.0, -boundRectHeightMM / 2.0 );
}
else
{
//shift painter to edge/middle of frame depending on placement
double diffX = rect().width() - boundRectWidthMM;
double diffY = rect().height() - boundRectHeightMM;
double dX = 0;
double dY = 0;
switch ( mPictureAnchor )
{
case UpperLeft:
case MiddleLeft:
case LowerLeft:
//nothing to do
break;
case UpperMiddle:
case Middle:
case LowerMiddle:
dX = diffX / 2.0;
break;
case UpperRight:
case MiddleRight:
case LowerRight:
dX = diffX;
break;
}
switch ( mPictureAnchor )
{
case UpperLeft:
case UpperMiddle:
case UpperRight:
//nothing to do
break;
case MiddleLeft:
case Middle:
case MiddleRight:
dY = diffY / 2.0;
break;
case LowerLeft:
case LowerMiddle:
case LowerRight:
dY = diffY;
break;
}
painter->translate( dX, dY );
}
}
if ( mMode == SVG )
@ -132,7 +193,7 @@ void QgsComposerPicture::paint( QPainter* painter, const QStyleOptionGraphicsIte
}
else if ( mMode == RASTER )
{
painter->drawImage( QRectF( 0, 0, boundRectWidthMM, boundRectHeightMM ), mImage, QRectF( 0, 0, imageRectWidthMM, imageRectHeightMM ) );
painter->drawImage( QRectF( 0, 0, boundRectWidthMM, boundRectHeightMM ), mImage, imageRect );
}
painter->restore();
@ -146,6 +207,64 @@ void QgsComposerPicture::paint( QPainter* painter, const QStyleOptionGraphicsIte
}
}
QRect QgsComposerPicture::clippedImageRect( double &boundRectWidthMM, double &boundRectHeightMM, QSize imageRectPixels )
{
int boundRectWidthPixels = boundRectWidthMM * mComposition->printResolution() / 25.4;
int boundRectHeightPixels = boundRectHeightMM * mComposition->printResolution() / 25.4;
//update boundRectWidth/Height so that they exactly match pixel bounds
boundRectWidthMM = boundRectWidthPixels * 25.4 / mComposition->printResolution();
boundRectHeightMM = boundRectHeightPixels * 25.4 / mComposition->printResolution();
//calculate part of image which fits in bounds
int leftClip = 0;
int topClip = 0;
//calculate left crop
switch ( mPictureAnchor )
{
case UpperLeft:
case MiddleLeft:
case LowerLeft:
leftClip = 0;
break;
case UpperMiddle:
case Middle:
case LowerMiddle:
leftClip = ( imageRectPixels.width() - boundRectWidthPixels ) / 2;
break;
case UpperRight:
case MiddleRight:
case LowerRight:
leftClip = imageRectPixels.width() - boundRectWidthPixels;
break;
}
//calculate top crop
switch ( mPictureAnchor )
{
case UpperLeft:
case UpperMiddle:
case UpperRight:
topClip = 0;
break;
case MiddleLeft:
case Middle:
case MiddleRight:
topClip = ( imageRectPixels.height() - boundRectHeightPixels ) / 2;
break;
case LowerLeft:
case LowerMiddle:
case LowerRight:
topClip = imageRectPixels.height() - boundRectHeightPixels;
break;
}
return QRect( leftClip, topClip, boundRectWidthPixels, boundRectHeightPixels );
}
void QgsComposerPicture::setPictureFile( const QString& path )
{
mSourceFile.setFileName( path );
@ -525,6 +644,8 @@ bool QgsComposerPicture::writeXML( QDomElement& elem, QDomDocument & doc ) const
composerPictureElem.setAttribute( "pictureWidth", QString::number( mPictureWidth ) );
composerPictureElem.setAttribute( "pictureHeight", QString::number( mPictureHeight ) );
composerPictureElem.setAttribute( "resizeMode", QString::number(( int )mResizeMode ) );
composerPictureElem.setAttribute( "anchorPoint", QString::number(( int )mPictureAnchor ) );
if ( mUseSourceExpression )
{
composerPictureElem.setAttribute( "useExpression", "true" );
@ -561,6 +682,7 @@ bool QgsComposerPicture::readXML( const QDomElement& itemElem, const QDomDocumen
mPictureWidth = itemElem.attribute( "pictureWidth", "10" ).toDouble();
mPictureHeight = itemElem.attribute( "pictureHeight", "10" ).toDouble();
mResizeMode = QgsComposerPicture::ResizeMode( itemElem.attribute( "resizeMode", "0" ).toInt() );
mPictureAnchor = QgsComposerItem::ItemPositionMode( itemElem.attribute( "anchorPoint", QString( QgsComposerItem::UpperLeft ) ).toInt() );
QDomNodeList composerItemList = itemElem.elementsByTagName( "ComposerItem" );
if ( composerItemList.size() > 0 )
@ -594,8 +716,6 @@ bool QgsComposerPicture::readXML( const QDomElement& itemElem, const QDomDocumen
QString fileName = QgsProject::instance()->readPath( itemElem.attribute( "file" ) );
mSourceFile.setFileName( fileName );
refreshPicture();
//picture rotation
if ( itemElem.attribute( "pictureRotation", "0" ).toDouble() != 0 )
{
@ -619,6 +739,8 @@ bool QgsComposerPicture::readXML( const QDomElement& itemElem, const QDomDocumen
QObject::connect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setRotation( double ) ) );
}
refreshPicture();
emit itemChanged();
return true;
}
@ -635,6 +757,12 @@ int QgsComposerPicture::rotationMap() const
}
}
void QgsComposerPicture::setPictureAnchor( QgsComposerItem::ItemPositionMode anchor )
{
mPictureAnchor = anchor;
update();
}
bool QgsComposerPicture::imageSizeConsideringRotation( double& width, double& height ) const
{
//kept for api compatibility with QGIS 2.0 - use mPictureRotation

View File

@ -141,6 +141,22 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem
*/
ResizeMode resizeMode() const { return mResizeMode; }
/**Sets the picture's anchor point, which controls how it is placed
* within the picture item's frame.
* @param anchor anchor point for picture
* @note added in 2.3
* @see pictureAnchor
*/
void setPictureAnchor( QgsComposerItem::ItemPositionMode anchor );
/**Returns the picture's current anchor, which controls how it is placed
* within the picture item's frame.
* @returns anchor point for picture
* @note added in 2.3
* @see setPictureAnchor
*/
ItemPositionMode pictureAnchor() const { return mPictureAnchor; }
/**Returns whether the picture item is using an expression for the image source.
* @returns true if the picture is using an expression for the source, false if
* it is using a single static file path for the source.
@ -275,6 +291,7 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem
double mPictureHeight;
ResizeMode mResizeMode;
QgsComposerItem::ItemPositionMode mPictureAnchor;
QgsExpression* mPictureExpr;
@ -286,6 +303,11 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem
/**sets up the picture item and connects to relevant signals*/
void init();
/**Returns part of a raster image which will be shown, given current picture
* anchor settings
*/
QRect clippedImageRect( double &boundRectWidthMM, double &boundRectHeightMM, QSize imageRectPixels );
};
#endif

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>312</width>
<width>315</width>
<height>572</height>
</rect>
</property>
@ -52,8 +52,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>310</width>
<height>549</height>
<width>297</width>
<height>589</height>
</rect>
</property>
<layout class="QVBoxLayout" name="mainLayout">
@ -69,13 +69,6 @@
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Image source</string>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
@ -109,36 +102,19 @@
</item>
</layout>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QRadioButton" name="mRadioExpression">
<property name="text">
<string>Expression</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="mPictureExpressionLineEdit"/>
</item>
<item>
<widget class="QToolButton" name="mPictureExpressionButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mIconExpression.svg</normaloff>:/images/themes/default/mIconExpression.svg</iconset>
</property>
</widget>
</item>
</layout>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Image source</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Placement</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
@ -176,6 +152,86 @@
</item>
</widget>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QRadioButton" name="mRadioExpression">
<property name="text">
<string>Expression</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="mPictureExpressionLineEdit"/>
</item>
<item>
<widget class="QToolButton" name="mPictureExpressionButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mIconExpression.svg</normaloff>:/images/themes/default/mIconExpression.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="0">
<widget class="QComboBox" name="mAnchorPointComboBox">
<item>
<property name="text">
<string>Top left</string>
</property>
</item>
<item>
<property name="text">
<string>Top center</string>
</property>
</item>
<item>
<property name="text">
<string>Top right</string>
</property>
</item>
<item>
<property name="text">
<string>Middle left</string>
</property>
</item>
<item>
<property name="text">
<string>Middle</string>
</property>
</item>
<item>
<property name="text">
<string>Middle right</string>
</property>
</item>
<item>
<property name="text">
<string>Bottom left</string>
</property>
</item>
<item>
<property name="text">
<string>Bottom center</string>
</property>
</item>
<item>
<property name="text">
<string>Bottom right</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -43,6 +43,10 @@ class TestQgsComposerPicture: public QObject
void pictureResizeZoomAndResize();
void pictureResizeFrameToImage();
void pictureClipAnchor();
void pictureClipAnchorOversize();
void pictureZoomAnchor();
void pictureSvgZoom();
void pictureSvgStretch();
void pictureSvgZoomAndResize();
@ -214,6 +218,56 @@ void TestQgsComposerPicture::pictureResizeFrameToImage()
mComposerPicture->setSceneRect( QRectF( 70, 70, 100, 100 ) );
}
void TestQgsComposerPicture::pictureClipAnchor()
{
//test picture anchor in Clip mode
mComposition->addComposerPicture( mComposerPicture );
mComposerPicture->setResizeMode( QgsComposerPicture::Clip );
mComposerPicture->setSceneRect( QRectF( 70, 70, 30, 50 ) );
mComposerPicture->setPictureAnchor( QgsComposerItem::LowerRight );
QgsCompositionChecker checker( "composerpicture_clip_anchor", mComposition );
QVERIFY( checker.testComposition( mReport, 0, 0 ) );
mComposition->removeItem( mComposerPicture );
mComposerPicture->setResizeMode( QgsComposerPicture::Zoom );
mComposerPicture->setPictureAnchor( QgsComposerItem::UpperLeft );
mComposerPicture->setSceneRect( QRectF( 70, 70, 100, 100 ) );
}
void TestQgsComposerPicture::pictureClipAnchorOversize()
{
//test picture anchor in Clip mode
mComposition->addComposerPicture( mComposerPicture );
mComposerPicture->setResizeMode( QgsComposerPicture::Clip );
mComposerPicture->setSceneRect( QRectF( 70, 70, 150, 120 ) );
mComposerPicture->setPictureAnchor( QgsComposerItem::LowerMiddle );
QgsCompositionChecker checker( "expected_composerpicture_clip_anchoroversize", mComposition );
QVERIFY( checker.testComposition( mReport, 0, 0 ) );
mComposition->removeItem( mComposerPicture );
mComposerPicture->setResizeMode( QgsComposerPicture::Zoom );
mComposerPicture->setPictureAnchor( QgsComposerItem::UpperLeft );
mComposerPicture->setSceneRect( QRectF( 70, 70, 100, 100 ) );
}
void TestQgsComposerPicture::pictureZoomAnchor()
{
//test picture anchor in Zoom mode
mComposition->addComposerPicture( mComposerPicture );
mComposerPicture->setResizeMode( QgsComposerPicture::Zoom );
mComposerPicture->setSceneRect( QRectF( 70, 10, 30, 100 ) );
mComposerPicture->setPictureAnchor( QgsComposerItem::LowerMiddle );
QgsCompositionChecker checker( "composerpicture_zoom_anchor", mComposition );
QVERIFY( checker.testComposition( mReport, 0, 100 ) );
mComposition->removeItem( mComposerPicture );
mComposerPicture->setPictureAnchor( QgsComposerItem::UpperLeft );
mComposerPicture->setSceneRect( QRectF( 70, 70, 100, 100 ) );
}
void TestQgsComposerPicture::pictureSvgZoom()
{
//test picture resize Zoom mode

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB