QGIS API Documentation 3.99.0-Master (2fe06baccd8)
Loading...
Searching...
No Matches
qgslayouttable.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgslayouttable.cpp
3 ------------------
4 begin : November 2017
5 copyright : (C) 2017 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18#include "qgslayouttable.h"
19
20#include <set>
21
22#include "qgscolorutils.h"
24#include "qgsexpressionutils.h"
25#include "qgsfontutils.h"
26#include "qgslayoutframe.h"
30#include "qgslayoututils.h"
31#include "qgstextrenderer.h"
32
33#include "moc_qgslayouttable.cpp"
34
35//
36// QgsLayoutTableStyle
37//
38
39bool QgsLayoutTableStyle::writeXml( QDomElement &styleElem, QDomDocument &doc ) const
40{
41 Q_UNUSED( doc )
42 styleElem.setAttribute( QStringLiteral( "cellBackgroundColor" ), QgsColorUtils::colorToString( cellBackgroundColor ) );
43 styleElem.setAttribute( QStringLiteral( "enabled" ), enabled );
44 return true;
45}
46
47bool QgsLayoutTableStyle::readXml( const QDomElement &styleElem )
48{
49 cellBackgroundColor = QgsColorUtils::colorFromString( styleElem.attribute( QStringLiteral( "cellBackgroundColor" ), QStringLiteral( "255,255,255,255" ) ) );
50 enabled = ( styleElem.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ) != QLatin1String( "0" ) );
51 return true;
52}
53
54
55//
56// QgsLayoutTable
57//
58
64
66{
67 mColumns.clear();
68 mSortColumns.clear();
69
70 qDeleteAll( mCellStyles );
71 mCellStyles.clear();
72}
73
74bool QgsLayoutTable::writePropertiesToElement( QDomElement &elem, QDomDocument &doc, const QgsReadWriteContext &context ) const
75{
76 elem.setAttribute( QStringLiteral( "cellMargin" ), QString::number( mCellMargin ) );
77 elem.setAttribute( QStringLiteral( "emptyTableMode" ), QString::number( static_cast< int >( mEmptyTableMode ) ) );
78 elem.setAttribute( QStringLiteral( "emptyTableMessage" ), mEmptyTableMessage );
79 elem.setAttribute( QStringLiteral( "showEmptyRows" ), mShowEmptyRows );
80
81 QDomElement headerElem = doc.createElement( QStringLiteral( "headerTextFormat" ) );
82 const QDomElement headerTextElem = mHeaderTextFormat.writeXml( doc, context );
83 headerElem.appendChild( headerTextElem );
84 elem.appendChild( headerElem );
85 elem.setAttribute( QStringLiteral( "headerHAlignment" ), QString::number( static_cast< int >( mHeaderHAlignment ) ) );
86 elem.setAttribute( QStringLiteral( "headerMode" ), QString::number( static_cast< int >( mHeaderMode ) ) );
87
88 QDomElement contentElem = doc.createElement( QStringLiteral( "contentTextFormat" ) );
89 const QDomElement contentTextElem = mContentTextFormat.writeXml( doc, context );
90 contentElem.appendChild( contentTextElem );
91 elem.appendChild( contentElem );
92 elem.setAttribute( QStringLiteral( "gridStrokeWidth" ), QString::number( mGridStrokeWidth ) );
93 elem.setAttribute( QStringLiteral( "gridColor" ), QgsColorUtils::colorToString( mGridColor ) );
94 elem.setAttribute( QStringLiteral( "horizontalGrid" ), mHorizontalGrid );
95 elem.setAttribute( QStringLiteral( "verticalGrid" ), mVerticalGrid );
96 elem.setAttribute( QStringLiteral( "showGrid" ), mShowGrid );
97 elem.setAttribute( QStringLiteral( "backgroundColor" ), QgsColorUtils::colorToString( mBackgroundColor ) );
98 elem.setAttribute( QStringLiteral( "wrapBehavior" ), QString::number( static_cast< int >( mWrapBehavior ) ) );
99
100 // display columns
101 QDomElement displayColumnsElem = doc.createElement( QStringLiteral( "displayColumns" ) );
102 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
103 {
104 QDomElement columnElem = doc.createElement( QStringLiteral( "column" ) );
105 column.writeXml( columnElem, doc );
106 displayColumnsElem.appendChild( columnElem );
107 }
108 elem.appendChild( displayColumnsElem );
109 // sort columns
110 QDomElement sortColumnsElem = doc.createElement( QStringLiteral( "sortColumns" ) );
111 for ( const QgsLayoutTableColumn &column : std::as_const( mSortColumns ) )
112 {
113 QDomElement columnElem = doc.createElement( QStringLiteral( "column" ) );
114 column.writeXml( columnElem, doc );
115 sortColumnsElem.appendChild( columnElem );
116 }
117 elem.appendChild( sortColumnsElem );
118
119
120 //cell styles
121 QDomElement stylesElem = doc.createElement( QStringLiteral( "cellStyles" ) );
122 QMap< CellStyleGroup, QString >::const_iterator it = mCellStyleNames.constBegin();
123 for ( ; it != mCellStyleNames.constEnd(); ++it )
124 {
125 QString styleName = it.value();
126 QDomElement styleElem = doc.createElement( styleName );
127 QgsLayoutTableStyle *style = mCellStyles.value( it.key() );
128 if ( style )
129 {
130 style->writeXml( styleElem, doc );
131 stylesElem.appendChild( styleElem );
132 }
133 }
134 elem.appendChild( stylesElem );
135 return true;
136}
137
138bool QgsLayoutTable::readPropertiesFromElement( const QDomElement &itemElem, const QDomDocument &, const QgsReadWriteContext &context )
139{
140 mEmptyTableMode = QgsLayoutTable::EmptyTableMode( itemElem.attribute( QStringLiteral( "emptyTableMode" ), QStringLiteral( "0" ) ).toInt() );
141 mEmptyTableMessage = itemElem.attribute( QStringLiteral( "emptyTableMessage" ), tr( "No matching records" ) );
142 mShowEmptyRows = itemElem.attribute( QStringLiteral( "showEmptyRows" ), QStringLiteral( "0" ) ).toInt();
143
144 const QDomElement headerTextFormat = itemElem.firstChildElement( QStringLiteral( "headerTextFormat" ) );
145 if ( !headerTextFormat.isNull() )
146 {
147 QDomNodeList textFormatNodeList = headerTextFormat.elementsByTagName( QStringLiteral( "text-style" ) );
148 QDomElement textFormatElem = textFormatNodeList.at( 0 ).toElement();
149 mHeaderTextFormat.readXml( textFormatElem, context );
150 }
151 else
152 {
153 QFont headerFont;
154 if ( !QgsFontUtils::setFromXmlChildNode( headerFont, itemElem, QStringLiteral( "headerFontProperties" ) ) )
155 {
156 headerFont.fromString( itemElem.attribute( QStringLiteral( "headerFont" ), QString() ) );
157 }
158 QColor headerFontColor = QgsColorUtils::colorFromString( itemElem.attribute( QStringLiteral( "headerFontColor" ), QStringLiteral( "0,0,0,255" ) ) );
159 mHeaderTextFormat.setFont( headerFont );
160 if ( headerFont.pointSizeF() > 0 )
161 {
162 mHeaderTextFormat.setSize( headerFont.pointSizeF() );
164 }
165 else if ( headerFont.pixelSize() > 0 )
166 {
167 mHeaderTextFormat.setSize( headerFont.pixelSize() );
169 }
171 }
172
173 mHeaderHAlignment = QgsLayoutTable::HeaderHAlignment( itemElem.attribute( QStringLiteral( "headerHAlignment" ), QStringLiteral( "0" ) ).toInt() );
174 mHeaderMode = QgsLayoutTable::HeaderMode( itemElem.attribute( QStringLiteral( "headerMode" ), QStringLiteral( "0" ) ).toInt() );
175
176 const QDomElement contentTextFormat = itemElem.firstChildElement( QStringLiteral( "contentTextFormat" ) );
177 if ( !contentTextFormat.isNull() )
178 {
179 QDomNodeList textFormatNodeList = contentTextFormat.elementsByTagName( QStringLiteral( "text-style" ) );
180 QDomElement textFormatElem = textFormatNodeList.at( 0 ).toElement();
181 mContentTextFormat.readXml( textFormatElem, context );
182 }
183 else
184 {
185 QFont contentFont;
186 if ( !QgsFontUtils::setFromXmlChildNode( contentFont, itemElem, QStringLiteral( "contentFontProperties" ) ) )
187 {
188 contentFont.fromString( itemElem.attribute( QStringLiteral( "contentFont" ), QString() ) );
189 }
190 QColor contentFontColor = QgsColorUtils::colorFromString( itemElem.attribute( QStringLiteral( "contentFontColor" ), QStringLiteral( "0,0,0,255" ) ) );
192 if ( contentFont.pointSizeF() > 0 )
193 {
194 mContentTextFormat.setSize( contentFont.pointSizeF() );
196 }
197 else if ( contentFont.pixelSize() > 0 )
198 {
199 mContentTextFormat.setSize( contentFont.pixelSize() );
201 }
203 }
204
205 mCellMargin = itemElem.attribute( QStringLiteral( "cellMargin" ), QStringLiteral( "1.0" ) ).toDouble();
206 mGridStrokeWidth = itemElem.attribute( QStringLiteral( "gridStrokeWidth" ), QStringLiteral( "0.5" ) ).toDouble();
207 mHorizontalGrid = itemElem.attribute( QStringLiteral( "horizontalGrid" ), QStringLiteral( "1" ) ).toInt();
208 mVerticalGrid = itemElem.attribute( QStringLiteral( "verticalGrid" ), QStringLiteral( "1" ) ).toInt();
209 mShowGrid = itemElem.attribute( QStringLiteral( "showGrid" ), QStringLiteral( "1" ) ).toInt();
210 mGridColor = QgsColorUtils::colorFromString( itemElem.attribute( QStringLiteral( "gridColor" ), QStringLiteral( "0,0,0,255" ) ) );
211 mBackgroundColor = QgsColorUtils::colorFromString( itemElem.attribute( QStringLiteral( "backgroundColor" ), QStringLiteral( "255,255,255,0" ) ) );
212 mWrapBehavior = QgsLayoutTable::WrapBehavior( itemElem.attribute( QStringLiteral( "wrapBehavior" ), QStringLiteral( "0" ) ).toInt() );
213
214 //restore display column specifications
215 mColumns.clear();
216 QDomNodeList columnsList = itemElem.elementsByTagName( QStringLiteral( "displayColumns" ) );
217 if ( !columnsList.isEmpty() )
218 {
219 QDomElement columnsElem = columnsList.at( 0 ).toElement();
220 QDomNodeList columnEntryList = columnsElem.elementsByTagName( QStringLiteral( "column" ) );
221 for ( int i = 0; i < columnEntryList.size(); ++i )
222 {
223 QDomElement columnElem = columnEntryList.at( i ).toElement();
225 column.readXml( columnElem );
226 mColumns.append( column );
227 }
228 }
229 // sort columns
230 mSortColumns.clear();
231 QDomNodeList sortColumnsList = itemElem.elementsByTagName( QStringLiteral( "sortColumns" ) );
232 if ( !sortColumnsList.isEmpty() )
233 {
234 QDomElement columnsElem = sortColumnsList.at( 0 ).toElement();
235 QDomNodeList columnEntryList = columnsElem.elementsByTagName( QStringLiteral( "column" ) );
236 for ( int i = 0; i < columnEntryList.size(); ++i )
237 {
238 QDomElement columnElem = columnEntryList.at( i ).toElement();
240 column.readXml( columnElem );
241 mSortColumns.append( column );
242 }
243 }
244 else
245 {
246 // backward compatibility for QGIS < 3.14
247 // copy the display columns if sortByRank > 0 and then, sort them by rank
249 std::copy_if( mColumns.begin(), mColumns.end(), std::back_inserter( mSortColumns ), []( const QgsLayoutTableColumn & col ) {return col.sortByRank() > 0;} );
250 std::sort( mSortColumns.begin(), mSortColumns.end(), []( const QgsLayoutTableColumn & a, const QgsLayoutTableColumn & b ) {return a.sortByRank() < b.sortByRank();} );
252 }
253
254 //restore cell styles
255 QDomNodeList stylesList = itemElem.elementsByTagName( QStringLiteral( "cellStyles" ) );
256 if ( !stylesList.isEmpty() )
257 {
258 QDomElement stylesElem = stylesList.at( 0 ).toElement();
259
260 QMap< CellStyleGroup, QString >::const_iterator it = mCellStyleNames.constBegin();
261 for ( ; it != mCellStyleNames.constEnd(); ++it )
262 {
263 QString styleName = it.value();
264 QDomNodeList styleList = stylesElem.elementsByTagName( styleName );
265 if ( !styleList.isEmpty() )
266 {
267 QDomElement styleElem = styleList.at( 0 ).toElement();
268 QgsLayoutTableStyle *style = mCellStyles.value( it.key() );
269 if ( style )
270 style->readXml( styleElem );
271 }
272 }
273 }
274
275 emit changed();
276 return true;
277}
278
280{
281 return mTableSize;
282}
283
289
290int QgsLayoutTable::rowsVisible( QgsRenderContext &context, double frameHeight, int firstRow, bool includeHeader, bool includeEmptyRows ) const
291{
292 //calculate header height
293 double headerHeight = 0;
294 if ( includeHeader )
295 {
296 headerHeight = mMaxRowHeightMap.value( 0 ) + 2 * ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + 2 * mCellMargin;
297 }
298 else
299 {
300 //frame has no header text, just the stroke
301 headerHeight = ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
302 }
303
304 //remaining height available for content rows
305 double contentHeight = frameHeight - headerHeight;
306
307 double gridHeight = ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
308
309 int currentRow = firstRow;
310 while ( contentHeight > 0 && currentRow <= mTableContents.count() )
311 {
312 double currentRowHeight = mMaxRowHeightMap.value( currentRow + 1 ) + gridHeight + 2 * mCellMargin;
313 contentHeight -= currentRowHeight;
314 currentRow++;
315 }
316
317 if ( includeEmptyRows && contentHeight > 0 )
318 {
319 const QFontMetricsF emptyRowContentFontMetrics = QgsTextRenderer::fontMetrics( context, mContentTextFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE );
320 double rowHeight = ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + 2 * mCellMargin + emptyRowContentFontMetrics.ascent() / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ) / QgsTextRenderer::FONT_WORKAROUND_SCALE;
321 currentRow += std::max( std::floor( contentHeight / rowHeight ), 0.0 );
322 }
323
324 return currentRow - firstRow - 1;
325}
326
327int QgsLayoutTable::rowsVisible( QgsRenderContext &context, int frameIndex, int firstRow, bool includeEmptyRows ) const
328{
329 //get frame extent
330 if ( frameIndex >= frameCount() )
331 {
332 return 0;
333 }
334 QRectF frameExtent = frame( frameIndex )->extent();
335
336 bool includeHeader = false;
339 {
340 includeHeader = true;
341 }
342 return rowsVisible( context, frameExtent.height(), firstRow, includeHeader, includeEmptyRows );
343}
344
345QPair<int, int> QgsLayoutTable::rowRange( QgsRenderContext &context, const int frameIndex ) const
346{
347 //calculate row height
348 if ( frameIndex >= frameCount() )
349 {
350 //bad frame index
351 return qMakePair( 0, 0 );
352 }
353
354 //loop through all previous frames to calculate how many rows are visible in each
355 //as the entire height of a frame may not be utilized for content rows
356 int rowsAlreadyShown = 0;
357 for ( int idx = 0; idx < frameIndex; ++idx )
358 {
359 rowsAlreadyShown += rowsVisible( context, idx, rowsAlreadyShown, false );
360 }
361
362 //using zero based indexes
363 int firstVisible = std::min( rowsAlreadyShown, static_cast<int>( mTableContents.length() ) );
364 int possibleRowsVisible = rowsVisible( context, frameIndex, rowsAlreadyShown, false );
365 int lastVisible = std::min( firstVisible + possibleRowsVisible, static_cast<int>( mTableContents.length() ) );
366
367 return qMakePair( firstVisible, lastVisible );
368}
369
370void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &, const int frameIndex )
371{
372 bool emptyTable = mTableContents.length() == 0;
373 if ( emptyTable && mEmptyTableMode == QgsLayoutTable::HideTable )
374 {
375 //empty table set to hide table mode, so don't draw anything
376 return;
377 }
378
379 if ( !mLayout->renderContext().isPreviewRender() )
380 {
381 //exporting composition, so force an attribute refresh
382 //we do this in case vector layer has changed via an external source (e.g., another database user)
384 }
385
386 const bool prevTextFormatScaleFlag = context.renderContext().testFlag( Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering );
388
389 //calculate which rows to show in this frame
390 QPair< int, int > rowsToShow = rowRange( context.renderContext(), frameIndex );
391
392 double gridSizeX = mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0;
393 double gridSizeY = mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0;
394 double cellHeaderHeight = mMaxRowHeightMap[0] + 2 * mCellMargin;
396 QRectF cell;
397
398 //calculate whether a header is required
399 bool drawHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && frameIndex < 1 )
401 //calculate whether drawing table contents is required
402 bool drawContents = !( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage );
403
404 int numberRowsToDraw = rowsToShow.second - rowsToShow.first;
405 int numberEmptyRows = 0;
406 if ( drawContents && mShowEmptyRows )
407 {
408 numberRowsToDraw = rowsVisible( context.renderContext(), frameIndex, rowsToShow.first, true );
409 numberEmptyRows = numberRowsToDraw - rowsToShow.second + rowsToShow.first;
410 }
411 bool mergeCells = false;
412 if ( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage )
413 {
414 //draw a merged row for the empty table message
415 numberRowsToDraw++;
416 rowsToShow.second++;
417 mergeCells = true;
418 }
419
420 QPainter *p = context.renderContext().painter();
421 QgsScopedQPainterState painterState( p );
422 // painter is scaled to dots, so scale back to layout units
423 p->scale( context.renderContext().scaleFactor(), context.renderContext().scaleFactor() );
424
425 //draw the text
426 p->setPen( Qt::SolidLine );
427
428 double currentX = gridSizeX;
429 double currentY = gridSizeY;
430 if ( drawHeader )
431 {
432 //draw the headers
433 int col = 0;
434 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
435 {
436 auto headerCellScope = std::make_unique< QgsExpressionContextScope >();
437 headerCellScope->setVariable( QStringLiteral( "column_number" ), col + 1, true );
438 QgsExpressionContextScopePopper popper( context.renderContext().expressionContext(), headerCellScope.release() );
439
440 const QgsTextFormat headerFormat = textFormatForHeader( col );
441 //draw background
442 p->save();
443 p->setPen( Qt::NoPen );
444 p->setBrush( backgroundColor( -1, col ) );
445 p->drawRect( QRectF( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, cellHeaderHeight ) );
446 p->restore();
447
448 currentX += mCellMargin;
449
450 cell = QRectF( currentX, currentY, mMaxColumnWidthMap[col], cellHeaderHeight );
451
452 //calculate alignment of header
454 switch ( mHeaderHAlignment )
455 {
456 case FollowColumn:
457 headerAlign = QgsTextRenderer::convertQtHAlignment( column.hAlignment() );
458 break;
459 case HeaderLeft:
461 break;
462 case HeaderCenter:
464 break;
465 case HeaderRight:
467 break;
468 }
469
470 const QRectF textCell = QRectF( currentX, currentY + mCellMargin, mMaxColumnWidthMap[col], cellHeaderHeight - 2 * mCellMargin );
471
472 const QStringList str = column.heading().split( '\n' );
473
474 // scale to dots
475 {
477 QgsTextRenderer::drawText( QRectF( textCell.left() * context.renderContext().scaleFactor(),
478 textCell.top() * context.renderContext().scaleFactor(),
479 textCell.width() * context.renderContext().scaleFactor(),
480 textCell.height() * context.renderContext().scaleFactor() ), 0,
481 headerAlign, str, context.renderContext(), headerFormat, true, Qgis::TextVerticalAlignment::VerticalCenter,
483 );
484 }
485
486 currentX += mMaxColumnWidthMap[ col ];
487 currentX += mCellMargin;
488 currentX += gridSizeX;
489 col++;
490 }
491
492 currentY += cellHeaderHeight;
493 currentY += gridSizeY;
494 }
495
496 //now draw the body cells
497 int rowsDrawn = 0;
498 std::set< std::pair< int, int > > spannedCells;
499 if ( drawContents )
500 {
501 //draw the attribute values
502 for ( int row = rowsToShow.first; row < rowsToShow.second; ++row )
503 {
504 rowsDrawn++;
505 currentX = gridSizeX;
506 int col = 0;
507
508 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
509 {
510 ( void )column;
511
512 bool isSpanned = false;
513 QRectF fullCell;
514
515 double cellHeight = 0;
516 double cellWidth = 0;
517 const int rowsSpan = rowSpan( row, col );
518 const int colsSpan = columnSpan( row, col );
519 if ( spannedCells.find( std::make_pair( row, col ) ) != spannedCells.end() )
520 {
521 isSpanned = true;
522 }
523 else
524 {
525 for ( int spannedRow = row; spannedRow < row + rowsSpan; ++spannedRow )
526 {
527 cellHeight += mMaxRowHeightMap[spannedRow + 1] + 2 * mCellMargin
528 + ( spannedRow > row ? gridSizeY : 0 );
529 for ( int spannedCol = col; spannedCol < col + colsSpan; ++spannedCol )
530 {
531 spannedCells.insert( std::make_pair( spannedRow, spannedCol ) );
532 }
533 }
534 for ( int spannedCol = col; spannedCol < col + colsSpan; ++spannedCol )
535 {
536 cellWidth += mMaxColumnWidthMap[spannedCol] + 2 * mCellMargin
537 + ( spannedCol > col ? gridSizeX : 0 );
538 }
539 }
540
541 fullCell = QRectF( currentX, currentY, cellWidth, cellHeight );
542
543 if ( !isSpanned )
544 {
545 //draw background
546 p->save();
547 p->setPen( Qt::NoPen );
548 p->setBrush( backgroundColor( row, col, rowsSpan, colsSpan ) );
549 p->drawRect( fullCell );
550 p->restore();
551 }
552
553 // currentY = gridSize;
554 currentX += mCellMargin;
555
556 if ( !isSpanned )
557 {
558 QVariant cellContents = mTableContents.at( row ).at( col );
559 const QString localizedString { QgsExpressionUtils::toLocalizedString( cellContents ) };
560 const QStringList str = localizedString.split( '\n' );
561
562 QgsTextFormat cellFormat = textFormatForCell( row, col );
564 cellFormat.updateDataDefinedProperties( context.renderContext() );
565
566 p->save();
567 p->setClipRect( fullCell );
568 const QRectF textCell = QRectF( currentX, currentY + mCellMargin, cellWidth - 2 * mCellMargin, cellHeight - 2 * mCellMargin );
569
570 const QgsConditionalStyle style = conditionalCellStyle( row, col );
571 QColor foreColor = cellFormat.color();
572 if ( style.textColor().isValid() )
573 foreColor = style.textColor();
574
575 cellFormat.setColor( foreColor );
576
577 // scale to dots
578 {
580 QgsTextRenderer::drawText( QRectF( textCell.left() * context.renderContext().scaleFactor(),
581 textCell.top() * context.renderContext().scaleFactor(),
582 textCell.width() * context.renderContext().scaleFactor(),
583 textCell.height() * context.renderContext().scaleFactor() ), 0,
584 QgsTextRenderer::convertQtHAlignment( horizontalAlignmentForCell( row, col ) ), str, context.renderContext(), cellFormat, true,
587 }
588 p->restore();
589 }
590
591 currentX += mMaxColumnWidthMap[ col ];
592 currentX += mCellMargin;
593 currentX += gridSizeX;
594 col++;
595 }
596 currentY += mMaxRowHeightMap[row + 1] + 2 * mCellMargin;
597 currentY += gridSizeY;
598 }
599 }
600
601 if ( numberRowsToDraw > rowsDrawn )
602 {
603 p->save();
604 p->setPen( Qt::NoPen );
605
606 //draw background of empty rows
607 for ( int row = rowsDrawn; row < numberRowsToDraw; ++row )
608 {
609 currentX = gridSizeX;
610 int col = 0;
611
612 if ( mergeCells )
613 {
614 p->setBrush( backgroundColor( row + 10000, 0 ) );
615 p->drawRect( QRectF( gridSizeX, currentY, mTableSize.width() - 2 * gridSizeX, cellBodyHeightForEmptyRows ) );
616 }
617 else
618 {
619 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
620 {
621 Q_UNUSED( column )
622
623 //draw background
624
625 //we use a bit of a hack here - since we don't want these extra blank rows to match the firstrow/lastrow rule, add 10000 to row number
626 p->setBrush( backgroundColor( row + 10000, col ) );
627 p->drawRect( QRectF( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, cellBodyHeightForEmptyRows ) );
628
629 // currentY = gridSize;
630 currentX += mMaxColumnWidthMap[ col ] + 2 * mCellMargin;
631 currentX += gridSizeX;
632 col++;
633 }
634 }
635 currentY += cellBodyHeightForEmptyRows + gridSizeY;
636 }
637 p->restore();
638 }
639
640 //and the borders
641 if ( mShowGrid )
642 {
643 QPen gridPen;
644 gridPen.setWidthF( mGridStrokeWidth );
645 gridPen.setColor( mGridColor );
646 gridPen.setJoinStyle( Qt::MiterJoin );
647 gridPen.setCapStyle( Qt::FlatCap );
648 p->setPen( gridPen );
649 if ( mHorizontalGrid )
650 {
651 drawHorizontalGridLines( context, rowsToShow.first, rowsToShow.second + numberEmptyRows, drawHeader );
652 }
653 if ( mVerticalGrid )
654 {
655 drawVerticalGridLines( context, mMaxColumnWidthMap, rowsToShow.first, rowsToShow.second + numberEmptyRows, drawHeader, mergeCells );
656 }
657 }
658
659 //special case - no records and table is set to ShowMessage mode
660 if ( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage )
661 {
662 double messageX = gridSizeX + mCellMargin;
663 double messageY = gridSizeY + ( drawHeader ? cellHeaderHeight + gridSizeY : 0 );
664 cell = QRectF( messageX, messageY, mTableSize.width() - messageX, cellBodyHeightForEmptyRows );
665
666 // scale to dots
667 {
669 QgsTextRenderer::drawText( QRectF( cell.left() * context.renderContext().scaleFactor(),
670 cell.top() * context.renderContext().scaleFactor(),
671 cell.width() * context.renderContext().scaleFactor(),
672 cell.height() * context.renderContext().scaleFactor() ), 0,
674 }
675 }
676
678}
679
680void QgsLayoutTable::setCellMargin( const double margin )
681{
682 if ( qgsDoubleNear( margin, mCellMargin ) )
683 {
684 return;
685 }
686
687 mCellMargin = margin;
688
689 //since spacing has changed, we need to recalculate the table size
691
692 emit changed();
693}
694
696{
697 if ( mode == mEmptyTableMode )
698 {
699 return;
700 }
701
702 mEmptyTableMode = mode;
703
704 //since appearance has changed, we need to recalculate the table size
706
707 emit changed();
708}
709
710void QgsLayoutTable::setEmptyTableMessage( const QString &message )
711{
712 if ( message == mEmptyTableMessage )
713 {
714 return;
715 }
716
717 mEmptyTableMessage = message;
718
719 //since message has changed, we need to recalculate the table size
721
722 emit changed();
723}
724
725void QgsLayoutTable::setShowEmptyRows( const bool showEmpty )
726{
727 if ( showEmpty == mShowEmptyRows )
728 {
729 return;
730 }
731
732 mShowEmptyRows = showEmpty;
733 update();
734 emit changed();
735}
736
737void QgsLayoutTable::setHeaderFont( const QFont &font )
738{
739 mHeaderTextFormat.setFont( font );
740 if ( font.pointSizeF() > 0 )
741 {
742 mHeaderTextFormat.setSize( font.pointSizeF() );
744 }
745 else if ( font.pixelSize() > 0 )
746 {
747 mHeaderTextFormat.setSize( font.pixelSize() );
749 }
750
751 //since font attributes have changed, we need to recalculate the table size
753
754 emit changed();
755}
756
758{
759 return mHeaderTextFormat.toQFont();
760}
761
762void QgsLayoutTable::setHeaderFontColor( const QColor &color )
763{
764 if ( color == mHeaderTextFormat.color() )
765 {
766 return;
767 }
768
769 mHeaderTextFormat.setColor( color );
770 update();
771
772 emit changed();
773}
774
776{
777 return mHeaderTextFormat.color();
778}
779
781{
782 mHeaderTextFormat = format;
783
784 //since font attributes have changed, we need to recalculate the table size
786
787 emit changed();
788}
789
794
796{
797 if ( alignment == mHeaderHAlignment )
798 {
799 return;
800 }
801
802 mHeaderHAlignment = alignment;
803 update();
804
805 emit changed();
806}
807
809{
810 if ( mode == mHeaderMode )
811 {
812 return;
813 }
814
815 mHeaderMode = mode;
817
818 emit changed();
819}
820
821void QgsLayoutTable::setContentFont( const QFont &font )
822{
823 mContentTextFormat.setFont( font );
824 if ( font.pointSizeF() > 0 )
825 {
826 mContentTextFormat.setSize( font.pointSizeF() );
828 }
829 else if ( font.pixelSize() > 0 )
830 {
831 mContentTextFormat.setSize( font.pixelSize() );
833 }
834
835 //since font attributes have changed, we need to recalculate the table size
837
838 emit changed();
839}
840
842{
843 return mContentTextFormat.toQFont();
844}
845
846void QgsLayoutTable::setContentFontColor( const QColor &color )
847{
848 if ( color == mContentTextFormat.color() )
849 {
850 return;
851 }
852
853 mContentTextFormat.setColor( color );
854 update();
855
856 emit changed();
857}
858
860{
861 return mContentTextFormat.color();
862}
863
865{
866 mContentTextFormat = format;
867
868 //since spacing has changed, we need to recalculate the table size
870
871 emit changed();
872}
873
878
880{
881 if ( showGrid == mShowGrid )
882 {
883 return;
884 }
885
887 //since grid spacing has changed, we need to recalculate the table size
889
890 emit changed();
891}
892
893void QgsLayoutTable::setGridStrokeWidth( const double width )
894{
895 if ( qgsDoubleNear( width, mGridStrokeWidth ) )
896 {
897 return;
898 }
899
900 mGridStrokeWidth = width;
901 //since grid spacing has changed, we need to recalculate the table size
903
904 emit changed();
905}
906
907void QgsLayoutTable::setGridColor( const QColor &color )
908{
909 if ( color == mGridColor )
910 {
911 return;
912 }
913
914 mGridColor = color;
915 update();
916
917 emit changed();
918}
919
921{
923 {
924 return;
925 }
926
928 //since grid spacing has changed, we need to recalculate the table size
930
931 emit changed();
932}
933
935{
937 {
938 return;
939 }
940
942 //since grid spacing has changed, we need to recalculate the table size
944
945 emit changed();
946}
947
948void QgsLayoutTable::setBackgroundColor( const QColor &color )
949{
950 if ( color == mBackgroundColor )
951 {
952 return;
953 }
954
955 mBackgroundColor = color;
956 update();
957
958 emit changed();
959}
960
962{
963 if ( behavior == mWrapBehavior )
964 {
965 return;
966 }
967
968 mWrapBehavior = behavior;
970
971 emit changed();
972}
973
975{
976 //remove existing columns
978
979 // backward compatibility
980 // test if sorting is provided with the columns and call setSortColumns in such case
981 QgsLayoutTableSortColumns newSortColumns;
983 std::copy_if( mColumns.begin(), mColumns.end(), std::back_inserter( newSortColumns ), []( const QgsLayoutTableColumn & col ) {return col.sortByRank() > 0;} );
984 if ( !newSortColumns.isEmpty() )
985 {
986 std::sort( newSortColumns.begin(), newSortColumns.end(), []( const QgsLayoutTableColumn & a, const QgsLayoutTableColumn & b ) {return a.sortByRank() < b.sortByRank();} );
987 setSortColumns( newSortColumns );
988 }
990}
991
996
998{
999 if ( mCellStyles.contains( group ) )
1000 delete mCellStyles.take( group );
1001
1002 mCellStyles.insert( group, new QgsLayoutTableStyle( style ) );
1003}
1004
1006{
1007 if ( !mCellStyles.contains( group ) )
1008 return nullptr;
1009
1010 return mCellStyles.value( group );
1011}
1012
1013QMap<int, QString> QgsLayoutTable::headerLabels() const
1014{
1015 QMap<int, QString> headers;
1016
1017 int i = 0;
1018 for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1019 {
1020 headers.insert( i, col.heading() );
1021 i++;
1022 }
1023 return headers;
1024}
1025
1027{
1028 auto cellScope = std::make_unique< QgsExpressionContextScope >();
1029 cellScope->setVariable( QStringLiteral( "row_number" ), row + 1, true );
1030 cellScope->setVariable( QStringLiteral( "column_number" ), column + 1, true );
1031 return cellScope.release();
1032}
1033
1034int QgsLayoutTable::rowSpan( int, int ) const
1035{
1036 return 1;
1037}
1038
1039int QgsLayoutTable::columnSpan( int, int ) const
1040{
1041 return 1;
1042}
1043
1048
1050{
1051 Q_UNUSED( frameIndex )
1052 return QSizeF( mTableSize.width(), 0 );
1053}
1054
1056{
1059
1060 double height = 0;
1063 {
1064 //header required, force frame to be high enough for header
1065 for ( int col = 0; col < mColumns.size(); ++ col )
1066 {
1068 }
1069 }
1070 return QSizeF( 0, height );
1071}
1072
1074{
1075 mMaxColumnWidthMap.clear();
1076 mMaxRowHeightMap.clear();
1077 mTableContents.clear();
1078
1079 //get new contents
1081 {
1082 return;
1083 }
1084}
1085
1091
1092void QgsLayoutTable::initStyles()
1093{
1096 mCellStyles.insert( OddRows, new QgsLayoutTableStyle() );
1097 mCellStyles.insert( EvenRows, new QgsLayoutTableStyle() );
1100 mCellStyles.insert( HeaderRow, new QgsLayoutTableStyle() );
1101 mCellStyles.insert( FirstRow, new QgsLayoutTableStyle() );
1102 mCellStyles.insert( LastRow, new QgsLayoutTableStyle() );
1103
1104 mCellStyleNames.insert( OddColumns, QStringLiteral( "oddColumns" ) );
1105 mCellStyleNames.insert( EvenColumns, QStringLiteral( "evenColumns" ) );
1106 mCellStyleNames.insert( OddRows, QStringLiteral( "oddRows" ) );
1107 mCellStyleNames.insert( EvenRows, QStringLiteral( "evenRows" ) );
1108 mCellStyleNames.insert( FirstColumn, QStringLiteral( "firstColumn" ) );
1109 mCellStyleNames.insert( LastColumn, QStringLiteral( "lastColumn" ) );
1110 mCellStyleNames.insert( HeaderRow, QStringLiteral( "headerRow" ) );
1111 mCellStyleNames.insert( FirstRow, QStringLiteral( "firstRow" ) );
1112 mCellStyleNames.insert( LastRow, QStringLiteral( "lastRow" ) );
1113}
1114
1116{
1117 mMaxColumnWidthMap.clear();
1118
1119 //total number of cells (rows + 1 for header)
1120 int cols = mColumns.count();
1121 int cells = cols * ( mTableContents.count() + 1 );
1122 QVector< double > widths( cells );
1123
1124 double currentCellTextWidth;
1125
1128
1129 //first, go through all the column headers and calculate the sizes
1130 int i = 0;
1131 for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1132 {
1133 if ( col.width() > 0 )
1134 {
1135 //column has manually specified width
1136 widths[i] = col.width();
1137 }
1139 {
1140 auto headerCellScope = std::make_unique< QgsExpressionContextScope >();
1141 headerCellScope->setVariable( QStringLiteral( "column_number" ), i + 1, true );
1142 QgsExpressionContextScopePopper popper( context.expressionContext(), headerCellScope.release() );
1143
1144 //column width set to automatic, so check content size
1145 const QStringList multiLineSplit = col.heading().split( '\n' );
1146 currentCellTextWidth = QgsTextRenderer::textWidth( context, textFormatForHeader( i ), multiLineSplit ) / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
1147 widths[i] = currentCellTextWidth;
1148 }
1149 else
1150 {
1151 widths[i] = 0.0;
1152 }
1153 i++;
1154 }
1155
1156 //next, go through all the table contents and calculate the sizes
1157 QgsLayoutTableContents::const_iterator rowIt = mTableContents.constBegin();
1158 int row = 1;
1159 for ( ; rowIt != mTableContents.constEnd(); ++rowIt )
1160 {
1161 QgsLayoutTableRow::const_iterator colIt = rowIt->constBegin();
1162 int col = 0;
1163 for ( ; colIt != rowIt->constEnd(); ++colIt )
1164 {
1165 if ( mColumns.at( col ).width() <= 0 )
1166 {
1167 //column width set to automatic, so check content size
1168 const QStringList multiLineSplit = QgsExpressionUtils::toLocalizedString( *colIt ).split( '\n' );
1169
1170 QgsTextFormat cellFormat = textFormatForCell( row - 1, col );
1171 QgsExpressionContextScopePopper popper( context.expressionContext(), scopeForCell( row - 1, col ) );
1172 cellFormat.updateDataDefinedProperties( context );
1173
1174 currentCellTextWidth = QgsTextRenderer::textWidth( context, cellFormat, multiLineSplit ) / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
1175 widths[ row * cols + col ] = currentCellTextWidth;
1176 }
1177 else
1178 {
1179 widths[ row * cols + col ] = 0;
1180 }
1181
1182 col++;
1183 }
1184 row++;
1185 }
1186
1187 //calculate maximum
1188 for ( int col = 0; col < cols; ++col )
1189 {
1190 double maxColWidth = 0;
1191 for ( int row = 0; row < mTableContents.count() + 1; ++row )
1192 {
1193 maxColWidth = std::max( widths[ row * cols + col ], maxColWidth );
1194 }
1195 mMaxColumnWidthMap.insert( col, maxColWidth );
1196 }
1197
1198 return true;
1199}
1200
1202{
1203 mMaxRowHeightMap.clear();
1204
1205 //total number of cells (rows + 1 for header)
1206 int cols = mColumns.count();
1207 int cells = cols * ( mTableContents.count() + 1 );
1208 QVector< double > heights( cells );
1209
1212
1213 //first, go through all the column headers and calculate the sizes
1214 int i = 0;
1215 for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1216 {
1217 auto headerCellScope = std::make_unique< QgsExpressionContextScope >();
1218 headerCellScope->setVariable( QStringLiteral( "column_number" ), i + 1, true );
1219 QgsExpressionContextScopePopper popper( context.expressionContext(), headerCellScope.release() );
1220
1221 const QgsTextFormat cellFormat = textFormatForHeader( i );
1223 //height
1225 {
1226 heights[i] = 0;
1227 }
1228 else
1229 {
1230 heights[i] = QgsTextRenderer::textHeight( context,
1231 cellFormat,
1232 QStringList() << col.heading(), Qgis::TextLayoutMode::Rectangle,
1233 nullptr,
1236 )
1238 - headerDescentMm;
1239 }
1240 i++;
1241 }
1242
1243 //next, go through all the table contents and calculate the sizes
1244 QgsLayoutTableContents::const_iterator rowIt = mTableContents.constBegin();
1245 int row = 1;
1246 for ( ; rowIt != mTableContents.constEnd(); ++rowIt )
1247 {
1248 QgsLayoutTableRow::const_iterator colIt = rowIt->constBegin();
1249 int i = 0;
1250 for ( ; colIt != rowIt->constEnd(); ++colIt )
1251 {
1252 QgsTextFormat cellFormat = textFormatForCell( row - 1, i );
1253 QgsExpressionContextScopePopper popper( context.expressionContext(), scopeForCell( row - 1, i ) );
1254 cellFormat.updateDataDefinedProperties( context );
1256 const QString localizedString { QgsExpressionUtils::toLocalizedString( *colIt ) };
1257
1258 heights[ row * cols + i ] = QgsTextRenderer::textHeight( context,
1259 cellFormat,
1260 QStringList() << localizedString.split( '\n' ),
1262 nullptr,
1265 ) / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ) - contentDescentMm;
1266
1267 i++;
1268 }
1269 row++;
1270 }
1271
1272 //calculate maximum
1273 for ( int row = 0; row < mTableContents.count() + 1; ++row )
1274 {
1275 double maxRowHeight = 0;
1276 for ( int col = 0; col < cols; ++col )
1277 {
1278 maxRowHeight = std::max( heights[ row * cols + col ], maxRowHeight );
1279 }
1280 mMaxRowHeightMap.insert( row, maxRowHeight );
1281 }
1282
1283 return true;
1284}
1285
1287{
1288 //check how much space each column needs
1289 if ( !calculateMaxColumnWidths() )
1290 {
1291 return 0;
1292 }
1293
1294 //adapt frame to total width
1295 double totalWidth = 0;
1296 QMap<int, double>::const_iterator maxColWidthIt = mMaxColumnWidthMap.constBegin();
1297 for ( ; maxColWidthIt != mMaxColumnWidthMap.constEnd(); ++maxColWidthIt )
1298 {
1299 totalWidth += maxColWidthIt.value();
1300 }
1301 totalWidth += ( 2 * mMaxColumnWidthMap.size() * mCellMargin );
1302 totalWidth += ( mMaxColumnWidthMap.size() + 1 ) * ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1303
1304 return totalWidth;
1305}
1306
1308{
1309 //check how much space each row needs
1310 if ( !calculateMaxRowHeights() )
1311 {
1312 return 0;
1313 }
1314
1315 double height = 0;
1316
1319
1320 //loop through all existing frames to calculate how many rows are visible in each
1321 //as the entire height of a frame may not be utilized for content rows
1322 int rowsAlreadyShown = 0;
1323 int numberExistingFrames = frameCount();
1324 int rowsVisibleInLastFrame = 0;
1325 double heightOfLastFrame = 0;
1326 for ( int idx = 0; idx < numberExistingFrames; ++idx )
1327 {
1328 bool hasHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && idx == 0 )
1330 heightOfLastFrame = frame( idx )->rect().height();
1331 rowsVisibleInLastFrame = rowsVisible( context, heightOfLastFrame, rowsAlreadyShown, hasHeader, false );
1332 rowsAlreadyShown += rowsVisibleInLastFrame;
1333 height += heightOfLastFrame;
1334 if ( rowsAlreadyShown >= mTableContents.length() )
1335 {
1336 //shown entire contents of table, nothing remaining
1337 return height;
1338 }
1339 }
1340
1341 //calculate how many rows left to show
1342 int remainingRows = mTableContents.length() - rowsAlreadyShown;
1343
1344 if ( remainingRows <= 0 )
1345 {
1346 //no remaining rows
1347 return height;
1348 }
1349
1351 {
1352 QgsLayoutItemPage *page = mLayout->pageCollection()->page( mLayout->pageCollection()->pageCount() - 1 );
1353 if ( page )
1354 heightOfLastFrame = page->sizeWithUnits().height();
1355 }
1356
1357 bool hasHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && numberExistingFrames < 1 )
1359
1360 int numberFramesMissing = 0;
1361 while ( remainingRows > 0 )
1362 {
1363 numberFramesMissing++;
1364
1365 rowsVisibleInLastFrame = rowsVisible( context, heightOfLastFrame, rowsAlreadyShown, hasHeader, false );
1366 if ( rowsVisibleInLastFrame < 1 )
1367 {
1368 //if no rows are visible in the last frame, calculation of missing frames
1369 //is impossible. So just return total height of existing frames
1370 return height;
1371 }
1372
1373 rowsAlreadyShown += rowsVisibleInLastFrame;
1374 remainingRows = mTableContents.length() - rowsAlreadyShown;
1375 }
1376
1377 //rows remain unshown -- how many extra frames would we need to complete the table?
1378 //assume all added frames are same size as final frame
1379 height += heightOfLastFrame * numberFramesMissing;
1380 return height;
1381}
1382
1383void QgsLayoutTable::drawHorizontalGridLines( QgsLayoutItemRenderContext &context, int firstRow, int lastRow, bool drawHeaderLines ) const
1384{
1385 //horizontal lines
1386 if ( lastRow - firstRow < 1 && !drawHeaderLines )
1387 {
1388 return;
1389 }
1390
1391 QPainter *painter = context.renderContext().painter();
1392
1394 double halfGridStrokeWidth = ( mShowGrid ? mGridStrokeWidth : 0 ) / 2.0;
1395 double currentY = halfGridStrokeWidth;
1396 if ( drawHeaderLines )
1397 {
1398 painter->drawLine( QPointF( 0, currentY ), QPointF( mTableSize.width(), currentY ) );
1399 currentY += ( mShowGrid ? mGridStrokeWidth : 0 );
1400 currentY += mMaxRowHeightMap[0] + 2 * mCellMargin;
1401 }
1402
1403 QHash< QPair< int, int >, bool > skippedCellBottomBorders;
1404 for ( int row = 0; row < lastRow; ++row )
1405 {
1406 for ( int col = 0; col < mColumns.size(); ++col )
1407 {
1408 if ( skippedCellBottomBorders.constFind( qMakePair( row, col ) ) != skippedCellBottomBorders.constEnd() )
1409 continue;
1410
1411 const int rowsSpan = rowSpan( row, col );
1412 const int colsSpan = columnSpan( row, col );
1413 skippedCellBottomBorders.insert( qMakePair( row, col ), rowsSpan > 1 );
1414 for ( int rowDelta = 0; rowDelta < rowsSpan - 1; ++rowDelta )
1415 {
1416 for ( int colDelta = 0; colDelta < colsSpan; ++colDelta )
1417 {
1418 if ( rowDelta != 0 || colDelta != 0 )
1419 skippedCellBottomBorders.insert( qMakePair( row + rowDelta, col + colDelta ), true );
1420 }
1421 }
1422 }
1423 }
1424
1425 for ( int row = firstRow; row < lastRow; ++row )
1426 {
1427 double startX = 0;
1428 double endX = startX;
1429
1430 for ( int col = 0; col < mColumns.size(); ++col )
1431 {
1432 const double colWidth = mMaxColumnWidthMap.value( col ) + 2 * mCellMargin;
1433
1434 if ( skippedCellBottomBorders.value( qMakePair( row - 1, col ) ) )
1435 {
1436 // flush existing line
1437 if ( !qgsDoubleNear( startX, endX ) )
1438 {
1439 painter->drawLine( QPointF( startX, currentY ), QPointF( endX, currentY ) );
1440 }
1441 endX += colWidth;
1442 endX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1443 startX = endX;
1444 }
1445 else
1446 {
1447 endX += colWidth;
1448 endX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1449 }
1450 }
1451
1452 // flush last line
1453 if ( !qgsDoubleNear( startX, endX ) )
1454 {
1455 painter->drawLine( QPointF( startX, currentY ), QPointF( endX, currentY ) );
1456 }
1457
1458 currentY += ( mShowGrid ? mGridStrokeWidth : 0 );
1459 double rowHeight = row < mTableContents.count() ? mMaxRowHeightMap[row + 1] : cellBodyHeightForEmptyRows;
1460 currentY += ( rowHeight + 2 * mCellMargin );
1461 }
1462 painter->drawLine( QPointF( 0, currentY ), QPointF( mTableSize.width(), currentY ) );
1463}
1464
1465QColor QgsLayoutTable::backgroundColor( int row, int column, int rowSpan, int columnSpan ) const
1466{
1467 QColor color = mBackgroundColor;
1468 if ( QgsLayoutTableStyle *style = mCellStyles.value( OddColumns ) )
1469 if ( style->enabled && column % 2 == 0 )
1470 color = style->cellBackgroundColor;
1471 if ( QgsLayoutTableStyle *style = mCellStyles.value( EvenColumns ) )
1472 if ( style->enabled && column % 2 == 1 )
1473 color = style->cellBackgroundColor;
1474 if ( QgsLayoutTableStyle *style = mCellStyles.value( OddRows ) )
1475 if ( style->enabled && row % 2 == 0 )
1476 color = style->cellBackgroundColor;
1477 if ( QgsLayoutTableStyle *style = mCellStyles.value( EvenRows ) )
1478 if ( style->enabled && row % 2 == 1 )
1479 color = style->cellBackgroundColor;
1480 if ( QgsLayoutTableStyle *style = mCellStyles.value( FirstColumn ) )
1481 if ( style->enabled && column == 0 )
1482 color = style->cellBackgroundColor;
1483 if ( QgsLayoutTableStyle *style = mCellStyles.value( LastColumn ) )
1484 if ( style->enabled && ( column + columnSpan == mColumns.count() ) )
1485 color = style->cellBackgroundColor;
1486 if ( QgsLayoutTableStyle *style = mCellStyles.value( HeaderRow ) )
1487 if ( style->enabled && row == -1 )
1488 color = style->cellBackgroundColor;
1489 if ( QgsLayoutTableStyle *style = mCellStyles.value( FirstRow ) )
1490 if ( style->enabled && row == 0 )
1491 color = style->cellBackgroundColor;
1492 if ( QgsLayoutTableStyle *style = mCellStyles.value( LastRow ) )
1493 if ( style->enabled && ( row + rowSpan == mTableContents.count() ) )
1494 color = style->cellBackgroundColor;
1495
1496 if ( row >= 0 )
1497 {
1498 QgsConditionalStyle conditionalStyle = conditionalCellStyle( row, column );
1499 if ( conditionalStyle.backgroundColor().isValid() )
1500 color = conditionalStyle.backgroundColor();
1501 }
1502
1503 return color;
1504}
1505
1506void QgsLayoutTable::drawVerticalGridLines( QgsLayoutItemRenderContext &context, const QMap<int, double> &maxWidthMap, int firstRow, int lastRow, bool hasHeader, bool mergeCells ) const
1507{
1508 //vertical lines
1509 if ( lastRow - firstRow < 1 && !hasHeader )
1510 {
1511 return;
1512 }
1513
1514 QHash< QPair< int, int >, bool > skippedCellRightBorders;
1515 for ( int row = 0; row < lastRow; ++row )
1516 {
1517 for ( int col = 0; col < mColumns.size(); ++col )
1518 {
1519 if ( skippedCellRightBorders.constFind( qMakePair( row, col ) ) != skippedCellRightBorders.constEnd() )
1520 continue;
1521
1522 const int rowsSpan = rowSpan( row, col );
1523 const int colsSpan = columnSpan( row, col );
1524 skippedCellRightBorders.insert( qMakePair( row, col ), colsSpan > 1 );
1525
1526 for ( int colDelta = 0; colDelta < colsSpan - 1; ++colDelta )
1527 {
1528 for ( int rowDelta = 0; rowDelta < rowsSpan; ++rowDelta )
1529 {
1530 if ( rowDelta != 0 || colDelta != 0 )
1531 skippedCellRightBorders.insert( qMakePair( row + rowDelta, col + colDelta ), true );
1532 }
1533 }
1534 }
1535 }
1536
1537 QPainter *painter = context.renderContext().painter();
1538
1539 //calculate height of table within frame
1540 double tableHeight = 0;
1541 QList< double > rowHeights;
1542 if ( hasHeader )
1543 {
1544 rowHeights << mCellMargin * 2 + mMaxRowHeightMap[0];
1545 tableHeight += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + mCellMargin * 2 + mMaxRowHeightMap[0];
1546 }
1547 else
1548 {
1549 rowHeights << 0;
1550 }
1551 tableHeight += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
1552 double headerHeight = tableHeight;
1553
1555 for ( int row = firstRow; row < lastRow; ++row )
1556 {
1557 double rowHeight = row < mTableContents.count() ? mMaxRowHeightMap[row + 1] : cellBodyHeightForEmptyRows;
1558 rowHeights << rowHeight + mCellMargin * 2;
1559 tableHeight += rowHeight + ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + mCellMargin * 2;
1560 }
1561
1562 double currentX = ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 ) / 2.0;;
1563 // left border of table
1564 painter->drawLine( QPointF( currentX, 0 ), QPointF( currentX, tableHeight ) );
1565 currentX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1566 QMap<int, double>::const_iterator maxColWidthIt = maxWidthMap.constBegin();
1567 int col = 1;
1568 for ( ; maxColWidthIt != maxWidthMap.constEnd(); ++maxColWidthIt )
1569 {
1570 currentX += ( maxColWidthIt.value() + 2 * mCellMargin );
1571 if ( col == maxWidthMap.size() )
1572 {
1573 // right border of table, always drawn
1574 painter->drawLine( QPointF( currentX, 0 ), QPointF( currentX, tableHeight ) );
1575 }
1576 else
1577 {
1578 if ( !mergeCells )
1579 {
1580 double startY = 0;
1581 double endY = startY + ( hasHeader ? ( rowHeights.value( 0 ) + ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) ) : 0 );
1582 for ( int row = firstRow; row < lastRow; ++row )
1583 {
1584 const double rowHeight = rowHeights.value( row - firstRow + 1 );
1585 if ( skippedCellRightBorders.value( qMakePair( row, col - 1 ) ) )
1586 {
1587 // flush existing line
1588 if ( !qgsDoubleNear( startY, endY ) )
1589 {
1590 painter->drawLine( QPointF( currentX, startY ), QPointF( currentX, endY ) );
1591 }
1592 endY += rowHeight;
1593 endY += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
1594 startY = endY;
1595 }
1596 else
1597 {
1598 endY += rowHeight;
1599 endY += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
1600 }
1601 }
1602
1603 // flush last line
1604 if ( !qgsDoubleNear( startY, endY ) )
1605 {
1606 painter->drawLine( QPointF( currentX, startY ), QPointF( currentX, endY ) );
1607 }
1608 }
1609 else if ( hasHeader )
1610 {
1611 painter->drawLine( QPointF( currentX, 0 ), QPointF( currentX, headerHeight ) );
1612 }
1613 }
1614
1615 currentX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1616 col++;
1617 }
1618}
1619
1621{
1623
1624 //force recalculation of frame rects, so that they are set to the correct
1625 //fixed and minimum frame sizes
1627}
1628
1630{
1631 return ( contents.indexOf( row ) >= 0 );
1632}
1633
1635{
1636 return mContentTextFormat;
1637}
1638
1643
1644Qt::Alignment QgsLayoutTable::horizontalAlignmentForCell( int, int column ) const
1645{
1646 return mColumns.value( column ).hAlignment();
1647}
1648
1649Qt::Alignment QgsLayoutTable::verticalAlignmentForCell( int, int column ) const
1650{
1651 return mColumns.value( column ).vAlignment();
1652}
1653
@ Rectangle
Text within rectangle layout mode.
Definition qgis.h:2902
QFlags< TextRendererFlag > TextRendererFlags
Definition qgis.h:3411
@ Millimeters
Millimeters.
Definition qgis.h:5184
@ Points
Points (e.g., for font sizes).
Definition qgis.h:5188
@ Pixels
Pixels.
Definition qgis.h:5186
@ ApplyScalingWorkaroundForTextRendering
Whether a scaling workaround designed to stablise the rendering of small font sizes (or for painters ...
Definition qgis.h:2762
@ VerticalCenter
Center align.
Definition qgis.h:2963
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:2942
@ Center
Center align.
Definition qgis.h:2944
@ WrapLines
Automatically wrap long lines of text.
Definition qgis.h:3408
static QColor colorFromString(const QString &string)
Decodes a string into a color value.
static QString colorToString(const QColor &color)
Encodes a color into a string value.
Conditional styling for a rule.
QColor backgroundColor() const
The background color for style.
QColor textColor() const
The text color set for style.
RAII class to pop scope from an expression context on destruction.
Single scope for storing variables and functions for use within a QgsExpressionContext.
static bool setFromXmlChildNode(QFont &font, const QDomElement &element, const QString &childNode)
Sets the properties of a font to match the properties stored in an XML child node.
QRectF extent() const
Returns the visible portion of the multi frame's content which is shown in this frame,...
Item representing the paper in a layout.
Contains settings and helpers relating to a render of a QgsLayoutItem.
QgsRenderContext & renderContext()
Returns a reference to the context's render context.
QgsLayoutSize sizeWithUnits() const
Returns the item's current size, including units.
int frameCount() const
Returns the number of frames associated with this multiframe.
QgsLayoutMultiFrame(QgsLayout *layout)
Construct a new multiframe item, attached to the specified layout.
QgsLayoutFrame * frame(int index) const
Returns the child frame at a specified index from the multiframe.
@ ExtendToNextPage
Creates new full page frames on the following page(s) until the entire multiframe content is visible.
void refresh() override
Refreshes the multiframe, causing a recalculation of any property overrides.
virtual void recalculateFrameSizes()
Recalculates the portion of the multiframe item which is shown in each of its component frames.
void update()
Forces a redraw of all child frames.
int frameIndex(QgsLayoutFrame *frame) const
Returns the index of a frame within the multiframe.
void recalculateFrameRects()
Forces a recalculation of all the associated frame's scene rectangles.
const QgsLayout * layout() const
Returns the layout the object is attached to.
void changed()
Emitted when the object's properties change.
QPointer< QgsLayout > mLayout
double height() const
Returns the height of the size.
Stores properties of a column for a QgsLayoutTable.
bool readXml(const QDomElement &columnElem)
Reads the column's properties from xml.
Styling option for a layout table cell.
bool readXml(const QDomElement &styleElem)
Reads the style's properties from XML.
QColor cellBackgroundColor
Cell background color.
bool enabled
Whether the styling option is enabled.
bool writeXml(QDomElement &styleElem, QDomDocument &doc) const
Writes the style's properties to XML for storage.
bool mHorizontalGrid
True if grid should be shown.
QSizeF fixedFrameSize(int frameIndex=-1) const override
Returns the fixed size for a frame, if desired.
QgsLayoutTableColumns & columns()
Returns a reference to the list of QgsLayoutTableColumns shown in the table.
void setColumns(const QgsLayoutTableColumns &columns)
Replaces the columns in the table with a specified list of QgsLayoutTableColumns.
Q_DECL_DEPRECATED void setHeaderFontColor(const QColor &color)
Sets the color used to draw header text in the table.
QgsLayoutTable(QgsLayout *layout)
Constructor for QgsLayoutTable, belonging to the specified layout.
virtual Qt::Alignment horizontalAlignmentForCell(int row, int column) const
Returns the horizontal alignment to use for the cell at the specified row and column.
QColor backgroundColor() const
Returns the color used for the background of the table.
virtual QgsConditionalStyle conditionalCellStyle(int row, int column) const
Returns the conditional style to use for the cell at row, column.
void setHeaderHAlignment(HeaderHAlignment alignment)
Sets the horizontal alignment for table headers.
void setShowEmptyRows(bool showEmpty)
Sets whether empty rows should be drawn.
Q_DECL_DEPRECATED QFont headerFont() const
Returns the font used to draw header text in the table.
QMap< int, double > mMaxColumnWidthMap
Map of maximum width for each column.
void setVerticalGrid(bool verticalGrid)
Sets whether the grid's vertical lines should be drawn in the table.
virtual void refreshAttributes()
Refreshes the contents shown in the table by querying for new data.
void setGridColor(const QColor &color)
Sets the color used for grid lines in the table.
double totalWidth()
Returns total width of table contents.
bool horizontalGrid() const
Returns whether the grid's horizontal lines are drawn in the table.
Q_DECL_DEPRECATED void setContentFontColor(const QColor &color)
Sets the color used to draw text in table body cells.
void setCellMargin(double margin)
Sets the margin distance in mm between cell borders and their contents.
void render(QgsLayoutItemRenderContext &context, const QRectF &renderExtent, int frameIndex) override
Renders a portion of the multiframe's content into a render context.
Q_DECL_DEPRECATED void setHeaderFont(const QFont &font)
Sets the font used to draw header text in the table.
void drawVerticalGridLines(QgsLayoutItemRenderContext &context, const QMap< int, double > &maxWidthMap, int firstRow, int lastRow, bool hasHeader, bool mergeCells=false) const
Draws the vertical grid lines for the table.
QgsLayoutTableSortColumns & sortColumns()
Returns a reference to the list of QgsLayoutTableSortColumns shown in the table.
void setBackgroundColor(const QColor &color)
Sets the color used for background of table.
void setContentTextFormat(const QgsTextFormat &format)
Sets the format used to draw content text in the table.
QgsTextFormat contentTextFormat() const
Returns the format used to draw content text in the table.
QString mEmptyTableMessage
String to show in empty tables.
virtual int rowSpan(int row, int column) const
Returns the row span for the cell a row, column.
void setWrapBehavior(WrapBehavior behavior)
Sets the wrap behavior for the table, which controls how text within cells is automatically wrapped.
Q_DECL_DEPRECATED QColor headerFontColor() const
Returns the color used to draw header text in the table.
QPair< int, int > rowRange(QgsRenderContext &context, int frameIndex) const
Calculates a range of rows which should be visible in a given frame.
double mGridStrokeWidth
Width of grid lines.
EmptyTableMode mEmptyTableMode
Behavior for empty tables.
void setEmptyTableBehavior(EmptyTableMode mode)
Sets the behavior mode for empty tables with no content rows.
virtual QgsTextFormat textFormatForHeader(int column) const
Returns the text format to use for the header cell at the specified column.
void setHeaderMode(HeaderMode mode)
Sets the display mode for headers in the table.
Q_DECL_DEPRECATED void setContentFont(const QFont &font)
Sets the font used to draw text in table body cells.
WrapBehavior mWrapBehavior
bool verticalGrid() const
Returns whether the grid's vertical lines are drawn in the table.
QColor mBackgroundColor
Color for table background.
void recalculateTableSize()
Recalculates and updates the size of the table and all table frames.
void drawHorizontalGridLines(QgsLayoutItemRenderContext &context, int firstRow, int lastRow, bool drawHeaderLines) const
Draws the horizontal grid lines for the table.
virtual QgsExpressionContextScope * scopeForCell(int row, int column) const
Creates a new QgsExpressionContextScope for the cell at row, column.
Q_DECL_DEPRECATED QFont contentFont() const
Returns the font used to draw text in table body cells.
bool contentsContainsRow(const QgsLayoutTableContents &contents, const QgsLayoutTableRow &row) const
Checks whether a table contents contains a given row.
bool mShowGrid
True if grid should be shown.
CellStyleGroup
Row or column groups for cell styling.
@ FirstRow
Style first row only.
@ EvenColumns
Style even numbered columns.
@ EvenRows
Style even numbered rows.
@ HeaderRow
Style header row.
@ OddColumns
Style odd numbered columns.
@ FirstColumn
Style first column only.
@ LastColumn
Style last column only.
@ LastRow
Style last row only.
@ OddRows
Style odd numbered rows.
QgsTextFormat mHeaderTextFormat
void setHorizontalGrid(bool horizontalGrid)
Sets whether the grid's horizontal lines should be drawn in the table.
HeaderMode
Controls where headers are shown in the table.
@ FirstFrame
Header shown on first frame only.
@ AllFrames
Headers shown on all frames.
@ NoHeaders
No headers shown for table.
void setCellStyle(CellStyleGroup group, const QgsLayoutTableStyle &style)
Sets the cell style for a cell group.
QgsLayoutTableColumns mColumns
Columns to show in table.
const QgsLayoutTableStyle * cellStyle(CellStyleGroup group) const
Returns the cell style for a cell group.
void setShowGrid(bool showGrid)
Sets whether grid lines should be drawn in the table.
QgsTextFormat mContentTextFormat
virtual bool getTableContents(QgsLayoutTableContents &contents)=0
Fetches the contents used for the cells in the table.
virtual bool calculateMaxColumnWidths()
Calculates the maximum width of text shown in columns.
~QgsLayoutTable() override
QgsTextFormat headerTextFormat() const
Returns the format used to draw header text in the table.
HeaderMode mHeaderMode
Header display mode.
HeaderHAlignment mHeaderHAlignment
Alignment for table headers.
int rowsVisible(QgsRenderContext &context, double frameHeight, int firstRow, bool includeHeader, bool includeEmptyRows) const
Calculates how many content rows would be visible within a frame of the specified height.
void recalculateFrameSizes() override
bool mShowEmptyRows
True if empty rows should be shown in the table.
QColor mGridColor
Color for grid lines.
QSizeF minFrameSize(int frameIndex=-1) const override
Returns the minimum size for a frames, if desired.
double mCellMargin
Margin between cell borders and cell text.
virtual Qt::Alignment verticalAlignmentForCell(int row, int column) const
Returns the vertical alignment to use for the cell at the specified row and column.
Q_DECL_DEPRECATED QColor contentFontColor() const
Returns the color used to draw text in table body cells.
double totalHeight()
Returns total height of table contents.
QgsLayoutTableContents mTableContents
Contents to show in table.
bool writePropertiesToElement(QDomElement &elem, QDomDocument &doc, const QgsReadWriteContext &context) const override
Stores multiframe state within an XML DOM element.
void setEmptyTableMessage(const QString &message)
Sets the message for empty tables with no content rows.
virtual int columnSpan(int row, int column) const
Returns the column span for the cell a row, column.
void setSortColumns(const QgsLayoutTableSortColumns &sortColumns)
Replaces the sorting columns in the table with a specified list of QgsLayoutTableSortColumns.
void setHeaderTextFormat(const QgsTextFormat &format)
Sets the format used to draw header text in the table.
QgsLayoutTableSortColumns mSortColumns
Columns to sort the table.
bool showGrid() const
Returns whether grid lines are drawn in the table.
virtual QMap< int, QString > headerLabels() const
Returns the text used in the column headers for the table.
void setGridStrokeWidth(double width)
Sets the width in mm for grid lines in the table.
virtual QgsTextFormat textFormatForCell(int row, int column) const
Returns the text format to use for the cell at the specified row and column.
bool readPropertiesFromElement(const QDomElement &itemElem, const QDomDocument &doc, const QgsReadWriteContext &context) override
Sets multiframe state from a DOM element.
QgsLayoutTableContents & contents()
Returns the current contents of the table.
QSizeF totalSize() const override
Returns the total size of the multiframe's content, in layout units.
bool mVerticalGrid
True if grid should be shown.
virtual bool calculateMaxRowHeights()
Calculates the maximum height of text shown in rows.
WrapBehavior
Controls how long strings in the table are handled.
@ WrapText
Text which doesn't fit inside the cell is wrapped. Note that this only applies to text in columns wit...
HeaderHAlignment
Controls how headers are horizontally aligned in a table.
@ HeaderRight
Align headers right.
@ HeaderLeft
Align headers left.
@ HeaderCenter
Align headers to center.
@ FollowColumn
Header uses the same alignment as the column.
EmptyTableMode
Controls how empty tables are displayed.
@ HideTable
Hides entire table if empty.
@ ShowMessage
Shows preset message instead of table contents.
void refresh() override
QMap< int, double > mMaxRowHeightMap
Map of maximum height for each row.
QMap< CellStyleGroup, QgsLayoutTableStyle * > mCellStyles
static QgsRenderContext createRenderContextForLayout(QgsLayout *layout, QPainter *painter, double dpi=-1)
Creates a render context suitable for the specified layout and painter destination.
A container for the context for various read/write operations on objects.
Contains information about the context of a rendering operation.
double scaleFactor() const
Returns the scaling factor for the render to convert painter units to physical sizes.
double convertToPainterUnits(double size, Qgis::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
QPainter * painter()
Returns the destination QPainter for the render operation.
QgsExpressionContext & expressionContext()
Gets the expression context.
bool testFlag(Qgis::RenderContextFlag flag) const
Check whether a particular flag is enabled.
void setFlag(Qgis::RenderContextFlag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected).
Scoped object for saving and restoring a QPainter object's state.
Scoped object for temporary scaling of a QgsRenderContext for pixel based rendering.
Container for all settings relating to text rendering.
void setColor(const QColor &color)
Sets the color that text will be rendered in.
void updateDataDefinedProperties(QgsRenderContext &context)
Updates the format by evaluating current values of data defined properties.
QColor color() const
Returns the color that text will be rendered in.
static Qgis::TextVerticalAlignment convertQtVAlignment(Qt::Alignment alignment)
Converts a Qt vertical alignment flag to a Qgis::TextVerticalAlignment value.
static double textWidth(const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, QFontMetricsF *fontMetrics=nullptr)
Returns the width of a text based on a given format.
static void drawText(const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, bool drawAsOutlines=true, Qgis::TextVerticalAlignment vAlignment=Qgis::TextVerticalAlignment::Top, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Rectangle)
Draws text within a rectangle using the specified settings.
static QFontMetricsF fontMetrics(QgsRenderContext &context, const QgsTextFormat &format, double scaleFactor=1.0)
Returns the font metrics for the given text format, when rendered in the specified render context.
static double textHeight(const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Point, QFontMetricsF *fontMetrics=nullptr, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), double maxLineWidth=0)
Returns the height of a text based on a given format.
static constexpr double FONT_WORKAROUND_SCALE
Scale factor for upscaling font sizes and downscaling destination painter devices.
static Qgis::TextHorizontalAlignment convertQtHAlignment(Qt::Alignment alignment)
Converts a Qt horizontal alignment flag to a Qgis::TextHorizontalAlignment value.
QVector< QgsLayoutTableColumn > QgsLayoutTableColumns
List of column definitions for a QgsLayoutTable.
QVector< QgsLayoutTableColumn > QgsLayoutTableSortColumns
List of column definitions for sorting a QgsLayoutTable.
QVector< QgsLayoutTableRow > QgsLayoutTableContents
List of QgsLayoutTableRows, representing rows and column cell contents for a QgsLayoutTable.
QVector< QVariant > QgsLayoutTableRow
List of QVariants, representing a the contents of a single row in a QgsLayoutTable.
#define Q_NOWARN_DEPRECATED_POP
Definition qgis.h:7170
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:7169
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:6607