QGIS API Documentation 4.0.0-Norrköping (1ddcee3d0e4)
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 <QString>
34
35#include "moc_qgslayouttable.cpp"
36
37using namespace Qt::StringLiterals;
38
39//
40// QgsLayoutTableStyle
41//
42
43bool QgsLayoutTableStyle::writeXml( QDomElement &styleElem, QDomDocument &doc ) const
44{
45 Q_UNUSED( doc )
46 styleElem.setAttribute( u"cellBackgroundColor"_s, QgsColorUtils::colorToString( cellBackgroundColor ) );
47 styleElem.setAttribute( u"enabled"_s, enabled );
48 return true;
49}
50
51bool QgsLayoutTableStyle::readXml( const QDomElement &styleElem )
52{
53 cellBackgroundColor = QgsColorUtils::colorFromString( styleElem.attribute( u"cellBackgroundColor"_s, u"255,255,255,255"_s ) );
54 enabled = ( styleElem.attribute( u"enabled"_s, u"0"_s ) != "0"_L1 );
55 return true;
56}
57
58
59//
60// QgsLayoutTable
61//
62
68
70{
71 mColumns.clear();
72 mSortColumns.clear();
73
74 qDeleteAll( mCellStyles );
75 mCellStyles.clear();
76}
77
78bool QgsLayoutTable::writePropertiesToElement( QDomElement &elem, QDomDocument &doc, const QgsReadWriteContext &context ) const
79{
80 elem.setAttribute( u"cellMargin"_s, QString::number( mCellMargin ) );
81 elem.setAttribute( u"emptyTableMode"_s, QString::number( static_cast< int >( mEmptyTableMode ) ) );
82 elem.setAttribute( u"emptyTableMessage"_s, mEmptyTableMessage );
83 elem.setAttribute( u"showEmptyRows"_s, mShowEmptyRows );
84
85 QDomElement headerElem = doc.createElement( u"headerTextFormat"_s );
86 const QDomElement headerTextElem = mHeaderTextFormat.writeXml( doc, context );
87 headerElem.appendChild( headerTextElem );
88 elem.appendChild( headerElem );
89 elem.setAttribute( u"headerHAlignment"_s, QString::number( static_cast< int >( mHeaderHAlignment ) ) );
90 elem.setAttribute( u"headerMode"_s, QString::number( static_cast< int >( mHeaderMode ) ) );
91
92 QDomElement contentElem = doc.createElement( u"contentTextFormat"_s );
93 const QDomElement contentTextElem = mContentTextFormat.writeXml( doc, context );
94 contentElem.appendChild( contentTextElem );
95 elem.appendChild( contentElem );
96 elem.setAttribute( u"gridStrokeWidth"_s, QString::number( mGridStrokeWidth ) );
97 elem.setAttribute( u"gridColor"_s, QgsColorUtils::colorToString( mGridColor ) );
98 elem.setAttribute( u"horizontalGrid"_s, mHorizontalGrid );
99 elem.setAttribute( u"verticalGrid"_s, mVerticalGrid );
100 elem.setAttribute( u"showGrid"_s, mShowGrid );
101 elem.setAttribute( u"backgroundColor"_s, QgsColorUtils::colorToString( mBackgroundColor ) );
102 elem.setAttribute( u"wrapBehavior"_s, QString::number( static_cast< int >( mWrapBehavior ) ) );
103
104 // display columns
105 QDomElement displayColumnsElem = doc.createElement( u"displayColumns"_s );
106 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
107 {
108 QDomElement columnElem = doc.createElement( u"column"_s );
109 column.writeXml( columnElem, doc );
110 displayColumnsElem.appendChild( columnElem );
111 }
112 elem.appendChild( displayColumnsElem );
113 // sort columns
114 QDomElement sortColumnsElem = doc.createElement( u"sortColumns"_s );
115 for ( const QgsLayoutTableColumn &column : std::as_const( mSortColumns ) )
116 {
117 QDomElement columnElem = doc.createElement( u"column"_s );
118 column.writeXml( columnElem, doc );
119 sortColumnsElem.appendChild( columnElem );
120 }
121 elem.appendChild( sortColumnsElem );
122
123
124 //cell styles
125 QDomElement stylesElem = doc.createElement( u"cellStyles"_s );
126 QMap< CellStyleGroup, QString >::const_iterator it = mCellStyleNames.constBegin();
127 for ( ; it != mCellStyleNames.constEnd(); ++it )
128 {
129 QString styleName = it.value();
130 QDomElement styleElem = doc.createElement( styleName );
131 QgsLayoutTableStyle *style = mCellStyles.value( it.key() );
132 if ( style )
133 {
134 style->writeXml( styleElem, doc );
135 stylesElem.appendChild( styleElem );
136 }
137 }
138 elem.appendChild( stylesElem );
139 return true;
140}
141
142bool QgsLayoutTable::readPropertiesFromElement( const QDomElement &itemElem, const QDomDocument &, const QgsReadWriteContext &context )
143{
144 mEmptyTableMode = QgsLayoutTable::EmptyTableMode( itemElem.attribute( u"emptyTableMode"_s, u"0"_s ).toInt() );
145 mEmptyTableMessage = itemElem.attribute( u"emptyTableMessage"_s, tr( "No matching records" ) );
146 mShowEmptyRows = itemElem.attribute( u"showEmptyRows"_s, u"0"_s ).toInt();
147
148 const QDomElement headerTextFormat = itemElem.firstChildElement( u"headerTextFormat"_s );
149 if ( !headerTextFormat.isNull() )
150 {
151 QDomNodeList textFormatNodeList = headerTextFormat.elementsByTagName( u"text-style"_s );
152 QDomElement textFormatElem = textFormatNodeList.at( 0 ).toElement();
153 mHeaderTextFormat.readXml( textFormatElem, context );
154 }
155 else
156 {
157 QFont headerFont;
158 if ( !QgsFontUtils::setFromXmlChildNode( headerFont, itemElem, u"headerFontProperties"_s ) )
159 {
160 headerFont.fromString( itemElem.attribute( u"headerFont"_s, QString() ) );
161 }
162 QColor headerFontColor = QgsColorUtils::colorFromString( itemElem.attribute( u"headerFontColor"_s, u"0,0,0,255"_s ) );
163 mHeaderTextFormat.setFont( headerFont );
164 if ( headerFont.pointSizeF() > 0 )
165 {
166 mHeaderTextFormat.setSize( headerFont.pointSizeF() );
168 }
169 else if ( headerFont.pixelSize() > 0 )
170 {
171 mHeaderTextFormat.setSize( headerFont.pixelSize() );
173 }
175 }
176
177 mHeaderHAlignment = QgsLayoutTable::HeaderHAlignment( itemElem.attribute( u"headerHAlignment"_s, u"0"_s ).toInt() );
178 mHeaderMode = QgsLayoutTable::HeaderMode( itemElem.attribute( u"headerMode"_s, u"0"_s ).toInt() );
179
180 const QDomElement contentTextFormat = itemElem.firstChildElement( u"contentTextFormat"_s );
181 if ( !contentTextFormat.isNull() )
182 {
183 QDomNodeList textFormatNodeList = contentTextFormat.elementsByTagName( u"text-style"_s );
184 QDomElement textFormatElem = textFormatNodeList.at( 0 ).toElement();
185 mContentTextFormat.readXml( textFormatElem, context );
186 }
187 else
188 {
189 QFont contentFont;
190 if ( !QgsFontUtils::setFromXmlChildNode( contentFont, itemElem, u"contentFontProperties"_s ) )
191 {
192 contentFont.fromString( itemElem.attribute( u"contentFont"_s, QString() ) );
193 }
194 QColor contentFontColor = QgsColorUtils::colorFromString( itemElem.attribute( u"contentFontColor"_s, u"0,0,0,255"_s ) );
196 if ( contentFont.pointSizeF() > 0 )
197 {
198 mContentTextFormat.setSize( contentFont.pointSizeF() );
200 }
201 else if ( contentFont.pixelSize() > 0 )
202 {
203 mContentTextFormat.setSize( contentFont.pixelSize() );
205 }
207 }
208
209 mCellMargin = itemElem.attribute( u"cellMargin"_s, u"1.0"_s ).toDouble();
210 mGridStrokeWidth = itemElem.attribute( u"gridStrokeWidth"_s, u"0.5"_s ).toDouble();
211 mHorizontalGrid = itemElem.attribute( u"horizontalGrid"_s, u"1"_s ).toInt();
212 mVerticalGrid = itemElem.attribute( u"verticalGrid"_s, u"1"_s ).toInt();
213 mShowGrid = itemElem.attribute( u"showGrid"_s, u"1"_s ).toInt();
214 mGridColor = QgsColorUtils::colorFromString( itemElem.attribute( u"gridColor"_s, u"0,0,0,255"_s ) );
215 mBackgroundColor = QgsColorUtils::colorFromString( itemElem.attribute( u"backgroundColor"_s, u"255,255,255,0"_s ) );
216 mWrapBehavior = QgsLayoutTable::WrapBehavior( itemElem.attribute( u"wrapBehavior"_s, u"0"_s ).toInt() );
217
218 //restore display column specifications
219 mColumns.clear();
220 QDomNodeList columnsList = itemElem.elementsByTagName( u"displayColumns"_s );
221 if ( !columnsList.isEmpty() )
222 {
223 QDomElement columnsElem = columnsList.at( 0 ).toElement();
224 QDomNodeList columnEntryList = columnsElem.elementsByTagName( u"column"_s );
225 for ( int i = 0; i < columnEntryList.size(); ++i )
226 {
227 QDomElement columnElem = columnEntryList.at( i ).toElement();
229 column.readXml( columnElem );
230 mColumns.append( column );
231 }
232 }
233 // sort columns
234 mSortColumns.clear();
235 QDomNodeList sortColumnsList = itemElem.elementsByTagName( u"sortColumns"_s );
236 if ( !sortColumnsList.isEmpty() )
237 {
238 QDomElement columnsElem = sortColumnsList.at( 0 ).toElement();
239 QDomNodeList columnEntryList = columnsElem.elementsByTagName( u"column"_s );
240 for ( int i = 0; i < columnEntryList.size(); ++i )
241 {
242 QDomElement columnElem = columnEntryList.at( i ).toElement();
244 column.readXml( columnElem );
245 mSortColumns.append( column );
246 }
247 }
248 else
249 {
250 // backward compatibility for QGIS < 3.14
251 // copy the display columns if sortByRank > 0 and then, sort them by rank
253 std::copy_if( mColumns.begin(), mColumns.end(), std::back_inserter( mSortColumns ), []( const QgsLayoutTableColumn &col ) { return col.sortByRank() > 0; } );
254 std::sort( mSortColumns.begin(), mSortColumns.end(), []( const QgsLayoutTableColumn &a, const QgsLayoutTableColumn &b ) { return a.sortByRank() < b.sortByRank(); } );
256 }
257
258 //restore cell styles
259 QDomNodeList stylesList = itemElem.elementsByTagName( u"cellStyles"_s );
260 if ( !stylesList.isEmpty() )
261 {
262 QDomElement stylesElem = stylesList.at( 0 ).toElement();
263
264 QMap< CellStyleGroup, QString >::const_iterator it = mCellStyleNames.constBegin();
265 for ( ; it != mCellStyleNames.constEnd(); ++it )
266 {
267 QString styleName = it.value();
268 QDomNodeList styleList = stylesElem.elementsByTagName( styleName );
269 if ( !styleList.isEmpty() )
270 {
271 QDomElement styleElem = styleList.at( 0 ).toElement();
272 QgsLayoutTableStyle *style = mCellStyles.value( it.key() );
273 if ( style )
274 style->readXml( styleElem );
275 }
276 }
277 }
278
279 emit changed();
280 return true;
281}
282
284{
285 return mTableSize;
286}
287
293
294int QgsLayoutTable::rowsVisible( QgsRenderContext &context, double frameHeight, int firstRow, bool includeHeader, bool includeEmptyRows ) const
295{
296 //calculate header height
297 double headerHeight = 0;
298 if ( includeHeader )
299 {
300 headerHeight = mMaxRowHeightMap.value( 0 ) + 2 * ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + 2 * mCellMargin;
301 }
302 else
303 {
304 //frame has no header text, just the stroke
305 headerHeight = ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
306 }
307
308 //remaining height available for content rows
309 double contentHeight = frameHeight - headerHeight;
310
311 double gridHeight = ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
312
313 int currentRow = firstRow;
314 while ( contentHeight > 0 && currentRow <= mTableContents.count() )
315 {
316 double currentRowHeight = mMaxRowHeightMap.value( currentRow + 1 ) + gridHeight + 2 * mCellMargin;
317 contentHeight -= currentRowHeight;
318 currentRow++;
319 }
320
321 if ( includeEmptyRows && contentHeight > 0 )
322 {
323 const QFontMetricsF emptyRowContentFontMetrics = QgsTextRenderer::fontMetrics( context, mContentTextFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE );
324 double rowHeight = ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 )
325 + 2 * mCellMargin
326 + emptyRowContentFontMetrics.ascent() / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ) / QgsTextRenderer::FONT_WORKAROUND_SCALE;
327 currentRow += std::max( std::floor( contentHeight / rowHeight ), 0.0 );
328 }
329
330 return currentRow - firstRow - 1;
331}
332
333int QgsLayoutTable::rowsVisible( QgsRenderContext &context, int frameIndex, int firstRow, bool includeEmptyRows ) const
334{
335 //get frame extent
336 if ( frameIndex >= frameCount() )
337 {
338 return 0;
339 }
340 QRectF frameExtent = frame( frameIndex )->extent();
341
342 bool includeHeader = false;
344 {
345 includeHeader = true;
346 }
347 return rowsVisible( context, frameExtent.height(), firstRow, includeHeader, includeEmptyRows );
348}
349
350QPair<int, int> QgsLayoutTable::rowRange( QgsRenderContext &context, const int frameIndex ) const
351{
352 //calculate row height
353 if ( frameIndex >= frameCount() )
354 {
355 //bad frame index
356 return qMakePair( 0, 0 );
357 }
358
359 //loop through all previous frames to calculate how many rows are visible in each
360 //as the entire height of a frame may not be utilized for content rows
361 int rowsAlreadyShown = 0;
362 for ( int idx = 0; idx < frameIndex; ++idx )
363 {
364 rowsAlreadyShown += rowsVisible( context, idx, rowsAlreadyShown, false );
365 }
366
367 //using zero based indexes
368 int firstVisible = std::min( rowsAlreadyShown, static_cast<int>( mTableContents.length() ) );
369 int possibleRowsVisible = rowsVisible( context, frameIndex, rowsAlreadyShown, false );
370 int lastVisible = std::min( firstVisible + possibleRowsVisible, static_cast<int>( mTableContents.length() ) );
371
372 return qMakePair( firstVisible, lastVisible );
373}
374
375void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &, const int frameIndex )
376{
377 bool emptyTable = mTableContents.length() == 0;
378 if ( emptyTable && mEmptyTableMode == QgsLayoutTable::HideTable )
379 {
380 //empty table set to hide table mode, so don't draw anything
381 return;
382 }
383
384 if ( !mLayout->renderContext().isPreviewRender() )
385 {
386 //exporting composition, so force an attribute refresh
387 //we do this in case vector layer has changed via an external source (e.g., another database user)
389 }
390
391 const bool prevTextFormatScaleFlag = context.renderContext().testFlag( Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering );
393
394 //calculate which rows to show in this frame
395 QPair< int, int > rowsToShow = rowRange( context.renderContext(), frameIndex );
396
397 double gridSizeX = mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0;
398 double gridSizeY = mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0;
399 double cellHeaderHeight = mMaxRowHeightMap[0] + 2 * mCellMargin;
400 double cellBodyHeightForEmptyRows = QgsTextRenderer::fontMetrics( context.renderContext(), mContentTextFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE ).ascent()
403 + 2 * mCellMargin;
404 QRectF cell;
405
406 //calculate whether a header is required
407 bool drawHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && frameIndex < 1 ) || ( mHeaderMode == QgsLayoutTable::AllFrames ) );
408 //calculate whether drawing table contents is required
409 bool drawContents = !( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage );
410
411 int numberRowsToDraw = rowsToShow.second - rowsToShow.first;
412 int numberEmptyRows = 0;
413 if ( drawContents && mShowEmptyRows )
414 {
415 numberRowsToDraw = rowsVisible( context.renderContext(), frameIndex, rowsToShow.first, true );
416 numberEmptyRows = numberRowsToDraw - rowsToShow.second + rowsToShow.first;
417 }
418 bool mergeCells = false;
419 if ( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage )
420 {
421 //draw a merged row for the empty table message
422 numberRowsToDraw++;
423 rowsToShow.second++;
424 mergeCells = true;
425 }
426
427 QPainter *p = context.renderContext().painter();
428 QgsScopedQPainterState painterState( p );
429 // painter is scaled to dots, so scale back to layout units
430 p->scale( context.renderContext().scaleFactor(), context.renderContext().scaleFactor() );
431
432 //draw the text
433 p->setPen( Qt::SolidLine );
434
435 double currentX = gridSizeX;
436 double currentY = gridSizeY;
437 if ( drawHeader )
438 {
439 //draw the headers
440 int col = 0;
441 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
442 {
443 auto headerCellScope = std::make_unique< QgsExpressionContextScope >();
444 headerCellScope->setVariable( u"column_number"_s, col + 1, true );
445 QgsExpressionContextScopePopper popper( context.renderContext().expressionContext(), headerCellScope.release() );
446
447 const QgsTextFormat headerFormat = textFormatForHeader( col );
448 //draw background
449 p->save();
450 p->setPen( Qt::NoPen );
451 p->setBrush( backgroundColor( -1, col ) );
452 p->drawRect( QRectF( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, cellHeaderHeight ) );
453 p->restore();
454
455 currentX += mCellMargin;
456
457 cell = QRectF( currentX, currentY, mMaxColumnWidthMap[col], cellHeaderHeight );
458
459 //calculate alignment of header
461 switch ( mHeaderHAlignment )
462 {
463 case FollowColumn:
464 headerAlign = QgsTextRenderer::convertQtHAlignment( column.hAlignment() );
465 break;
466 case HeaderLeft:
468 break;
469 case HeaderCenter:
471 break;
472 case HeaderRight:
474 break;
475 }
476
477 const QRectF textCell = QRectF( currentX, currentY + mCellMargin, mMaxColumnWidthMap[col], cellHeaderHeight - 2 * mCellMargin );
478
479 const QStringList str = column.heading().split( '\n' );
480
481 // scale to dots
482 {
485 QRectF(
486 textCell.left() * context.renderContext().scaleFactor(),
487 textCell.top() * context.renderContext().scaleFactor(),
488 textCell.width() * context.renderContext().scaleFactor(),
489 textCell.height() * context.renderContext().scaleFactor()
490 ),
491 0,
492 headerAlign,
493 str,
494 context.renderContext(),
495 headerFormat,
496 true,
499 );
500 }
501
502 currentX += mMaxColumnWidthMap[col];
503 currentX += mCellMargin;
504 currentX += gridSizeX;
505 col++;
506 }
507
508 currentY += cellHeaderHeight;
509 currentY += gridSizeY;
510 }
511
512 //now draw the body cells
513 int rowsDrawn = 0;
514 std::set< std::pair< int, int > > spannedCells;
515 if ( drawContents )
516 {
517 //draw the attribute values
518 for ( int row = rowsToShow.first; row < rowsToShow.second; ++row )
519 {
520 rowsDrawn++;
521 currentX = gridSizeX;
522 int col = 0;
523
524 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
525 {
526 ( void ) column;
527
528 bool isSpanned = false;
529 QRectF fullCell;
530
531 double cellHeight = 0;
532 double cellWidth = 0;
533 const int rowsSpan = rowSpan( row, col );
534 const int colsSpan = columnSpan( row, col );
535 if ( spannedCells.find( std::make_pair( row, col ) ) != spannedCells.end() )
536 {
537 isSpanned = true;
538 }
539 else
540 {
541 for ( int spannedRow = row; spannedRow < row + rowsSpan; ++spannedRow )
542 {
543 cellHeight += mMaxRowHeightMap[spannedRow + 1] + 2 * mCellMargin + ( spannedRow > row ? gridSizeY : 0 );
544 for ( int spannedCol = col; spannedCol < col + colsSpan; ++spannedCol )
545 {
546 spannedCells.insert( std::make_pair( spannedRow, spannedCol ) );
547 }
548 }
549 for ( int spannedCol = col; spannedCol < col + colsSpan; ++spannedCol )
550 {
551 cellWidth += mMaxColumnWidthMap[spannedCol] + 2 * mCellMargin + ( spannedCol > col ? gridSizeX : 0 );
552 }
553 }
554
555 fullCell = QRectF( currentX, currentY, cellWidth, cellHeight );
556
557 if ( !isSpanned )
558 {
559 //draw background
560 p->save();
561 p->setPen( Qt::NoPen );
562 p->setBrush( backgroundColor( row, col, rowsSpan, colsSpan ) );
563 p->drawRect( fullCell );
564 p->restore();
565 }
566
567 // currentY = gridSize;
568 currentX += mCellMargin;
569
570 if ( !isSpanned )
571 {
572 QVariant cellContents = mTableContents.at( row ).at( col );
573 const QString localizedString { QgsExpressionUtils::toLocalizedString( cellContents ) };
574 const QStringList str = localizedString.split( '\n' );
575
576 QgsTextFormat cellFormat = textFormatForCell( row, col );
578 cellFormat.updateDataDefinedProperties( context.renderContext() );
579
580 p->save();
581 p->setClipRect( fullCell );
582 const QRectF textCell = QRectF( currentX, currentY + mCellMargin, cellWidth - 2 * mCellMargin, cellHeight - 2 * mCellMargin );
583
584 const QgsConditionalStyle style = conditionalCellStyle( row, col );
585 QColor foreColor = cellFormat.color();
586 if ( style.textColor().isValid() )
587 foreColor = style.textColor();
588
589 cellFormat.setColor( foreColor );
590
591 // scale to dots
592 {
595 QRectF(
596 textCell.left() * context.renderContext().scaleFactor(),
597 textCell.top() * context.renderContext().scaleFactor(),
598 textCell.width() * context.renderContext().scaleFactor(),
599 textCell.height() * context.renderContext().scaleFactor()
600 ),
601 0,
603 str,
604 context.renderContext(),
605 cellFormat,
606 true,
609 );
610 }
611 p->restore();
612 }
613
614 currentX += mMaxColumnWidthMap[col];
615 currentX += mCellMargin;
616 currentX += gridSizeX;
617 col++;
618 }
619 currentY += mMaxRowHeightMap[row + 1] + 2 * mCellMargin;
620 currentY += gridSizeY;
621 }
622 }
623
624 if ( numberRowsToDraw > rowsDrawn )
625 {
626 p->save();
627 p->setPen( Qt::NoPen );
628
629 //draw background of empty rows
630 for ( int row = rowsDrawn; row < numberRowsToDraw; ++row )
631 {
632 currentX = gridSizeX;
633 int col = 0;
634
635 if ( mergeCells )
636 {
637 p->setBrush( backgroundColor( row + 10000, 0 ) );
638 p->drawRect( QRectF( gridSizeX, currentY, mTableSize.width() - 2 * gridSizeX, cellBodyHeightForEmptyRows ) );
639 }
640 else
641 {
642 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
643 {
644 Q_UNUSED( column )
645
646 //draw background
647
648 //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
649 p->setBrush( backgroundColor( row + 10000, col ) );
650 p->drawRect( QRectF( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, cellBodyHeightForEmptyRows ) );
651
652 // currentY = gridSize;
653 currentX += mMaxColumnWidthMap[col] + 2 * mCellMargin;
654 currentX += gridSizeX;
655 col++;
656 }
657 }
658 currentY += cellBodyHeightForEmptyRows + gridSizeY;
659 }
660 p->restore();
661 }
662
663 //and the borders
664 if ( mShowGrid )
665 {
666 QPen gridPen;
667 gridPen.setWidthF( mGridStrokeWidth );
668 gridPen.setColor( mGridColor );
669 gridPen.setJoinStyle( Qt::MiterJoin );
670 gridPen.setCapStyle( Qt::FlatCap );
671 p->setPen( gridPen );
672 if ( mHorizontalGrid )
673 {
674 drawHorizontalGridLines( context, rowsToShow.first, rowsToShow.second + numberEmptyRows, drawHeader );
675 }
676 if ( mVerticalGrid )
677 {
678 drawVerticalGridLines( context, mMaxColumnWidthMap, rowsToShow.first, rowsToShow.second + numberEmptyRows, drawHeader, mergeCells );
679 }
680 }
681
682 //special case - no records and table is set to ShowMessage mode
683 if ( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage )
684 {
685 double messageX = gridSizeX + mCellMargin;
686 double messageY = gridSizeY + ( drawHeader ? cellHeaderHeight + gridSizeY : 0 );
687 cell = QRectF( messageX, messageY, mTableSize.width() - messageX, cellBodyHeightForEmptyRows );
688
689 // scale to dots
690 {
693 QRectF(
694 cell.left() * context.renderContext().scaleFactor(),
695 cell.top() * context.renderContext().scaleFactor(),
696 cell.width() * context.renderContext().scaleFactor(),
697 cell.height() * context.renderContext().scaleFactor()
698 ),
699 0,
701 QStringList() << mEmptyTableMessage,
702 context.renderContext(),
704 true,
706 );
707 }
708 }
709
711}
712
713void QgsLayoutTable::setCellMargin( const double margin )
714{
715 if ( qgsDoubleNear( margin, mCellMargin ) )
716 {
717 return;
718 }
719
720 mCellMargin = margin;
721
722 //since spacing has changed, we need to recalculate the table size
724
725 emit changed();
726}
727
729{
730 if ( mode == mEmptyTableMode )
731 {
732 return;
733 }
734
735 mEmptyTableMode = mode;
736
737 //since appearance has changed, we need to recalculate the table size
739
740 emit changed();
741}
742
743void QgsLayoutTable::setEmptyTableMessage( const QString &message )
744{
745 if ( message == mEmptyTableMessage )
746 {
747 return;
748 }
749
750 mEmptyTableMessage = message;
751
752 //since message has changed, we need to recalculate the table size
754
755 emit changed();
756}
757
758void QgsLayoutTable::setShowEmptyRows( const bool showEmpty )
759{
760 if ( showEmpty == mShowEmptyRows )
761 {
762 return;
763 }
764
765 mShowEmptyRows = showEmpty;
766 update();
767 emit changed();
768}
769
770void QgsLayoutTable::setHeaderFont( const QFont &font )
771{
772 mHeaderTextFormat.setFont( font );
773 if ( font.pointSizeF() > 0 )
774 {
775 mHeaderTextFormat.setSize( font.pointSizeF() );
777 }
778 else if ( font.pixelSize() > 0 )
779 {
780 mHeaderTextFormat.setSize( font.pixelSize() );
782 }
783
784 //since font attributes have changed, we need to recalculate the table size
786
787 emit changed();
788}
789
791{
792 return mHeaderTextFormat.toQFont();
793}
794
795void QgsLayoutTable::setHeaderFontColor( const QColor &color )
796{
797 if ( color == mHeaderTextFormat.color() )
798 {
799 return;
800 }
801
802 mHeaderTextFormat.setColor( color );
803 update();
804
805 emit changed();
806}
807
809{
810 return mHeaderTextFormat.color();
811}
812
814{
815 mHeaderTextFormat = format;
816
817 //since font attributes have changed, we need to recalculate the table size
819
820 emit changed();
821}
822
827
829{
830 if ( alignment == mHeaderHAlignment )
831 {
832 return;
833 }
834
835 mHeaderHAlignment = alignment;
836 update();
837
838 emit changed();
839}
840
842{
843 if ( mode == mHeaderMode )
844 {
845 return;
846 }
847
848 mHeaderMode = mode;
850
851 emit changed();
852}
853
854void QgsLayoutTable::setContentFont( const QFont &font )
855{
856 mContentTextFormat.setFont( font );
857 if ( font.pointSizeF() > 0 )
858 {
859 mContentTextFormat.setSize( font.pointSizeF() );
861 }
862 else if ( font.pixelSize() > 0 )
863 {
864 mContentTextFormat.setSize( font.pixelSize() );
866 }
867
868 //since font attributes have changed, we need to recalculate the table size
870
871 emit changed();
872}
873
875{
876 return mContentTextFormat.toQFont();
877}
878
879void QgsLayoutTable::setContentFontColor( const QColor &color )
880{
881 if ( color == mContentTextFormat.color() )
882 {
883 return;
884 }
885
886 mContentTextFormat.setColor( color );
887 update();
888
889 emit changed();
890}
891
893{
894 return mContentTextFormat.color();
895}
896
898{
899 mContentTextFormat = format;
900
901 //since spacing has changed, we need to recalculate the table size
903
904 emit changed();
905}
906
911
913{
914 if ( showGrid == mShowGrid )
915 {
916 return;
917 }
918
920 //since grid spacing has changed, we need to recalculate the table size
922
923 emit changed();
924}
925
926void QgsLayoutTable::setGridStrokeWidth( const double width )
927{
928 if ( qgsDoubleNear( width, mGridStrokeWidth ) )
929 {
930 return;
931 }
932
933 mGridStrokeWidth = width;
934 //since grid spacing has changed, we need to recalculate the table size
936
937 emit changed();
938}
939
940void QgsLayoutTable::setGridColor( const QColor &color )
941{
942 if ( color == mGridColor )
943 {
944 return;
945 }
946
947 mGridColor = color;
948 update();
949
950 emit changed();
951}
952
954{
956 {
957 return;
958 }
959
961 //since grid spacing has changed, we need to recalculate the table size
963
964 emit changed();
965}
966
968{
970 {
971 return;
972 }
973
975 //since grid spacing has changed, we need to recalculate the table size
977
978 emit changed();
979}
980
981void QgsLayoutTable::setBackgroundColor( const QColor &color )
982{
983 if ( color == mBackgroundColor )
984 {
985 return;
986 }
987
988 mBackgroundColor = color;
989 update();
990
991 emit changed();
992}
993
995{
996 if ( behavior == mWrapBehavior )
997 {
998 return;
999 }
1000
1001 mWrapBehavior = behavior;
1003
1004 emit changed();
1005}
1006
1008{
1009 //remove existing columns
1010 mColumns = columns;
1011
1012 // backward compatibility
1013 // test if sorting is provided with the columns and call setSortColumns in such case
1014 QgsLayoutTableSortColumns newSortColumns;
1016 std::copy_if( mColumns.begin(), mColumns.end(), std::back_inserter( newSortColumns ), []( const QgsLayoutTableColumn &col ) { return col.sortByRank() > 0; } );
1017 if ( !newSortColumns.isEmpty() )
1018 {
1019 std::sort( newSortColumns.begin(), newSortColumns.end(), []( const QgsLayoutTableColumn &a, const QgsLayoutTableColumn &b ) { return a.sortByRank() < b.sortByRank(); } );
1020 setSortColumns( newSortColumns );
1021 }
1023}
1024
1029
1031{
1032 if ( mCellStyles.contains( group ) )
1033 delete mCellStyles.take( group );
1034
1035 mCellStyles.insert( group, new QgsLayoutTableStyle( style ) );
1036}
1037
1039{
1040 if ( !mCellStyles.contains( group ) )
1041 return nullptr;
1042
1043 return mCellStyles.value( group );
1044}
1045
1046QMap<int, QString> QgsLayoutTable::headerLabels() const
1047{
1048 QMap<int, QString> headers;
1049
1050 int i = 0;
1051 for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1052 {
1053 headers.insert( i, col.heading() );
1054 i++;
1055 }
1056 return headers;
1057}
1058
1060{
1061 auto cellScope = std::make_unique< QgsExpressionContextScope >();
1062 cellScope->setVariable( u"row_number"_s, row + 1, true );
1063 cellScope->setVariable( u"column_number"_s, column + 1, true );
1064 return cellScope.release();
1065}
1066
1067int QgsLayoutTable::rowSpan( int, int ) const
1068{
1069 return 1;
1070}
1071
1072int QgsLayoutTable::columnSpan( int, int ) const
1073{
1074 return 1;
1075}
1076
1081
1083{
1084 Q_UNUSED( frameIndex )
1085 return QSizeF( mTableSize.width(), 0 );
1086}
1087
1089{
1092
1093 double height = 0;
1095 {
1096 //header required, force frame to be high enough for header
1097 for ( int col = 0; col < mColumns.size(); ++col )
1098 {
1099 height = std::max(
1100 height,
1101 2 * ( mShowGrid ? mGridStrokeWidth : 0 )
1102 + 2 * mCellMargin
1106 );
1107 }
1108 }
1109 return QSizeF( 0, height );
1110}
1111
1113{
1114 mMaxColumnWidthMap.clear();
1115 mMaxRowHeightMap.clear();
1116 mTableContents.clear();
1117
1118 //get new contents
1120 {
1121 return;
1122 }
1123}
1124
1130
1131void QgsLayoutTable::initStyles()
1132{
1135 mCellStyles.insert( OddRows, new QgsLayoutTableStyle() );
1136 mCellStyles.insert( EvenRows, new QgsLayoutTableStyle() );
1139 mCellStyles.insert( HeaderRow, new QgsLayoutTableStyle() );
1140 mCellStyles.insert( FirstRow, new QgsLayoutTableStyle() );
1141 mCellStyles.insert( LastRow, new QgsLayoutTableStyle() );
1142
1143 mCellStyleNames.insert( OddColumns, u"oddColumns"_s );
1144 mCellStyleNames.insert( EvenColumns, u"evenColumns"_s );
1145 mCellStyleNames.insert( OddRows, u"oddRows"_s );
1146 mCellStyleNames.insert( EvenRows, u"evenRows"_s );
1147 mCellStyleNames.insert( FirstColumn, u"firstColumn"_s );
1148 mCellStyleNames.insert( LastColumn, u"lastColumn"_s );
1149 mCellStyleNames.insert( HeaderRow, u"headerRow"_s );
1150 mCellStyleNames.insert( FirstRow, u"firstRow"_s );
1151 mCellStyleNames.insert( LastRow, u"lastRow"_s );
1152}
1153
1155{
1156 mMaxColumnWidthMap.clear();
1157
1158 //total number of cells (rows + 1 for header)
1159 int cols = mColumns.count();
1160 int cells = cols * ( mTableContents.count() + 1 );
1161 QVector< double > widths( cells );
1162
1163 double currentCellTextWidth;
1164
1167
1168 //first, go through all the column headers and calculate the sizes
1169 int i = 0;
1170 for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1171 {
1172 if ( col.width() > 0 )
1173 {
1174 //column has manually specified width
1175 widths[i] = col.width();
1176 }
1178 {
1179 auto headerCellScope = std::make_unique< QgsExpressionContextScope >();
1180 headerCellScope->setVariable( u"column_number"_s, i + 1, true );
1181 QgsExpressionContextScopePopper popper( context.expressionContext(), headerCellScope.release() );
1182
1183 //column width set to automatic, so check content size
1184 const QStringList multiLineSplit = col.heading().split( '\n' );
1185 currentCellTextWidth = QgsTextRenderer::textWidth( context, textFormatForHeader( i ), multiLineSplit ) / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
1186 widths[i] = currentCellTextWidth;
1187 }
1188 else
1189 {
1190 widths[i] = 0.0;
1191 }
1192 i++;
1193 }
1194
1195 //next, go through all the table contents and calculate the sizes
1196 QgsLayoutTableContents::const_iterator rowIt = mTableContents.constBegin();
1197 int row = 1;
1198 for ( ; rowIt != mTableContents.constEnd(); ++rowIt )
1199 {
1200 QgsLayoutTableRow::const_iterator colIt = rowIt->constBegin();
1201 int col = 0;
1202 for ( ; colIt != rowIt->constEnd(); ++colIt )
1203 {
1204 if ( mColumns.at( col ).width() <= 0 )
1205 {
1206 //column width set to automatic, so check content size
1207 const QStringList multiLineSplit = QgsExpressionUtils::toLocalizedString( *colIt ).split( '\n' );
1208
1209 QgsTextFormat cellFormat = textFormatForCell( row - 1, col );
1210 QgsExpressionContextScopePopper popper( context.expressionContext(), scopeForCell( row - 1, col ) );
1211 cellFormat.updateDataDefinedProperties( context );
1212
1213 currentCellTextWidth = QgsTextRenderer::textWidth( context, cellFormat, multiLineSplit ) / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
1214 widths[row * cols + col] = currentCellTextWidth;
1215 }
1216 else
1217 {
1218 widths[row * cols + col] = 0;
1219 }
1220
1221 col++;
1222 }
1223 row++;
1224 }
1225
1226 //calculate maximum
1227 for ( int col = 0; col < cols; ++col )
1228 {
1229 double maxColWidth = 0;
1230 for ( int row = 0; row < mTableContents.count() + 1; ++row )
1231 {
1232 maxColWidth = std::max( widths[row * cols + col], maxColWidth );
1233 }
1234 mMaxColumnWidthMap.insert( col, maxColWidth );
1235 }
1236
1237 return true;
1238}
1239
1241{
1242 mMaxRowHeightMap.clear();
1243
1244 //total number of cells (rows + 1 for header)
1245 int cols = mColumns.count();
1246 int cells = cols * ( mTableContents.count() + 1 );
1247 QVector< double > heights( cells );
1248
1251
1252 //first, go through all the column headers and calculate the sizes
1253 int i = 0;
1254 for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1255 {
1256 auto headerCellScope = std::make_unique< QgsExpressionContextScope >();
1257 headerCellScope->setVariable( u"column_number"_s, i + 1, true );
1258 QgsExpressionContextScopePopper popper( context.expressionContext(), headerCellScope.release() );
1259
1260 const QgsTextFormat cellFormat = textFormatForHeader( i );
1261 const double headerDescentMm = QgsTextRenderer::fontMetrics( context, cellFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE ).descent()
1264 //height
1266 {
1267 heights[i] = 0;
1268 }
1269 else
1270 {
1271 heights[i] = QgsTextRenderer::textHeight(
1272 context,
1273 cellFormat,
1274 QStringList() << col.heading(),
1276 nullptr,
1280 - headerDescentMm;
1281 }
1282 i++;
1283 }
1284
1285 //next, go through all the table contents and calculate the sizes
1286 QgsLayoutTableContents::const_iterator rowIt = mTableContents.constBegin();
1287 int row = 1;
1288 for ( ; rowIt != mTableContents.constEnd(); ++rowIt )
1289 {
1290 QgsLayoutTableRow::const_iterator colIt = rowIt->constBegin();
1291 int i = 0;
1292 for ( ; colIt != rowIt->constEnd(); ++colIt )
1293 {
1294 QgsTextFormat cellFormat = textFormatForCell( row - 1, i );
1295 QgsExpressionContextScopePopper popper( context.expressionContext(), scopeForCell( row - 1, i ) );
1296 cellFormat.updateDataDefinedProperties( context );
1297 const double contentDescentMm = QgsTextRenderer::fontMetrics( context, cellFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE ).descent()
1300 const QString localizedString { QgsExpressionUtils::toLocalizedString( *colIt ) };
1301
1302 heights[row * cols + i] = QgsTextRenderer::textHeight(
1303 context,
1304 cellFormat,
1305 QStringList() << localizedString.split( '\n' ),
1307 nullptr,
1311 - contentDescentMm;
1312
1313 i++;
1314 }
1315 row++;
1316 }
1317
1318 //calculate maximum
1319 for ( int row = 0; row < mTableContents.count() + 1; ++row )
1320 {
1321 double maxRowHeight = 0;
1322 for ( int col = 0; col < cols; ++col )
1323 {
1324 maxRowHeight = std::max( heights[row * cols + col], maxRowHeight );
1325 }
1326 mMaxRowHeightMap.insert( row, maxRowHeight );
1327 }
1328
1329 return true;
1330}
1331
1333{
1334 //check how much space each column needs
1335 if ( !calculateMaxColumnWidths() )
1336 {
1337 return 0;
1338 }
1339
1340 //adapt frame to total width
1341 double totalWidth = 0;
1342 QMap<int, double>::const_iterator maxColWidthIt = mMaxColumnWidthMap.constBegin();
1343 for ( ; maxColWidthIt != mMaxColumnWidthMap.constEnd(); ++maxColWidthIt )
1344 {
1345 totalWidth += maxColWidthIt.value();
1346 }
1347 totalWidth += ( 2 * mMaxColumnWidthMap.size() * mCellMargin );
1348 totalWidth += ( mMaxColumnWidthMap.size() + 1 ) * ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1349
1350 return totalWidth;
1351}
1352
1354{
1355 //check how much space each row needs
1356 if ( !calculateMaxRowHeights() )
1357 {
1358 return 0;
1359 }
1360
1361 double height = 0;
1362
1365
1366 //loop through all existing frames to calculate how many rows are visible in each
1367 //as the entire height of a frame may not be utilized for content rows
1368 int rowsAlreadyShown = 0;
1369 int numberExistingFrames = frameCount();
1370 int rowsVisibleInLastFrame = 0;
1371 double heightOfLastFrame = 0;
1372 for ( int idx = 0; idx < numberExistingFrames; ++idx )
1373 {
1374 bool hasHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && idx == 0 ) || ( mHeaderMode == QgsLayoutTable::AllFrames ) );
1375 heightOfLastFrame = frame( idx )->rect().height();
1376 rowsVisibleInLastFrame = rowsVisible( context, heightOfLastFrame, rowsAlreadyShown, hasHeader, false );
1377 rowsAlreadyShown += rowsVisibleInLastFrame;
1378 height += heightOfLastFrame;
1379 if ( rowsAlreadyShown >= mTableContents.length() )
1380 {
1381 //shown entire contents of table, nothing remaining
1382 return height;
1383 }
1384 }
1385
1386 //calculate how many rows left to show
1387 int remainingRows = mTableContents.length() - rowsAlreadyShown;
1388
1389 if ( remainingRows <= 0 )
1390 {
1391 //no remaining rows
1392 return height;
1393 }
1394
1396 {
1397 QgsLayoutItemPage *page = mLayout->pageCollection()->page( mLayout->pageCollection()->pageCount() - 1 );
1398 if ( page )
1399 heightOfLastFrame = page->sizeWithUnits().height();
1400 }
1401
1402 bool hasHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && numberExistingFrames < 1 ) || ( mHeaderMode == QgsLayoutTable::AllFrames ) );
1403
1404 int numberFramesMissing = 0;
1405 while ( remainingRows > 0 )
1406 {
1407 numberFramesMissing++;
1408
1409 rowsVisibleInLastFrame = rowsVisible( context, heightOfLastFrame, rowsAlreadyShown, hasHeader, false );
1410 if ( rowsVisibleInLastFrame < 1 )
1411 {
1412 //if no rows are visible in the last frame, calculation of missing frames
1413 //is impossible. So just return total height of existing frames
1414 return height;
1415 }
1416
1417 rowsAlreadyShown += rowsVisibleInLastFrame;
1418 remainingRows = mTableContents.length() - rowsAlreadyShown;
1419 }
1420
1421 //rows remain unshown -- how many extra frames would we need to complete the table?
1422 //assume all added frames are same size as final frame
1423 height += heightOfLastFrame * numberFramesMissing;
1424 return height;
1425}
1426
1427void QgsLayoutTable::drawHorizontalGridLines( QgsLayoutItemRenderContext &context, int firstRow, int lastRow, bool drawHeaderLines ) const
1428{
1429 //horizontal lines
1430 if ( lastRow - firstRow < 1 && !drawHeaderLines )
1431 {
1432 return;
1433 }
1434
1435 QPainter *painter = context.renderContext().painter();
1436
1437 double cellBodyHeightForEmptyRows = QgsTextRenderer::fontMetrics( context.renderContext(), mContentTextFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE ).ascent()
1440 double halfGridStrokeWidth = ( mShowGrid ? mGridStrokeWidth : 0 ) / 2.0;
1441 double currentY = halfGridStrokeWidth;
1442 if ( drawHeaderLines )
1443 {
1444 painter->drawLine( QPointF( 0, currentY ), QPointF( mTableSize.width(), currentY ) );
1445 currentY += ( mShowGrid ? mGridStrokeWidth : 0 );
1446 currentY += mMaxRowHeightMap[0] + 2 * mCellMargin;
1447 }
1448
1449 QHash< QPair< int, int >, bool > skippedCellBottomBorders;
1450 for ( int row = 0; row < lastRow; ++row )
1451 {
1452 for ( int col = 0; col < mColumns.size(); ++col )
1453 {
1454 if ( skippedCellBottomBorders.constFind( qMakePair( row, col ) ) != skippedCellBottomBorders.constEnd() )
1455 continue;
1456
1457 const int rowsSpan = rowSpan( row, col );
1458 const int colsSpan = columnSpan( row, col );
1459 skippedCellBottomBorders.insert( qMakePair( row, col ), rowsSpan > 1 );
1460 for ( int rowDelta = 0; rowDelta < rowsSpan - 1; ++rowDelta )
1461 {
1462 for ( int colDelta = 0; colDelta < colsSpan; ++colDelta )
1463 {
1464 if ( rowDelta != 0 || colDelta != 0 )
1465 skippedCellBottomBorders.insert( qMakePair( row + rowDelta, col + colDelta ), true );
1466 }
1467 }
1468 }
1469 }
1470
1471 for ( int row = firstRow; row < lastRow; ++row )
1472 {
1473 double startX = 0;
1474 double endX = startX;
1475
1476 for ( int col = 0; col < mColumns.size(); ++col )
1477 {
1478 const double colWidth = mMaxColumnWidthMap.value( col ) + 2 * mCellMargin;
1479
1480 if ( skippedCellBottomBorders.value( qMakePair( row - 1, col ) ) )
1481 {
1482 // flush existing line
1483 if ( !qgsDoubleNear( startX, endX ) )
1484 {
1485 painter->drawLine( QPointF( startX, currentY ), QPointF( endX, currentY ) );
1486 }
1487 endX += colWidth;
1488 endX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1489 startX = endX;
1490 }
1491 else
1492 {
1493 endX += colWidth;
1494 endX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1495 }
1496 }
1497
1498 // flush last line
1499 if ( !qgsDoubleNear( startX, endX ) )
1500 {
1501 painter->drawLine( QPointF( startX, currentY ), QPointF( endX, currentY ) );
1502 }
1503
1504 currentY += ( mShowGrid ? mGridStrokeWidth : 0 );
1505 double rowHeight = row < mTableContents.count() ? mMaxRowHeightMap[row + 1] : cellBodyHeightForEmptyRows;
1506 currentY += ( rowHeight + 2 * mCellMargin );
1507 }
1508 painter->drawLine( QPointF( 0, currentY ), QPointF( mTableSize.width(), currentY ) );
1509}
1510
1511QColor QgsLayoutTable::backgroundColor( int row, int column, int rowSpan, int columnSpan ) const
1512{
1513 QColor color = mBackgroundColor;
1514 if ( QgsLayoutTableStyle *style = mCellStyles.value( OddColumns ) )
1515 if ( style->enabled && column % 2 == 0 )
1516 color = style->cellBackgroundColor;
1517 if ( QgsLayoutTableStyle *style = mCellStyles.value( EvenColumns ) )
1518 if ( style->enabled && column % 2 == 1 )
1519 color = style->cellBackgroundColor;
1520 if ( QgsLayoutTableStyle *style = mCellStyles.value( OddRows ) )
1521 if ( style->enabled && row % 2 == 0 )
1522 color = style->cellBackgroundColor;
1523 if ( QgsLayoutTableStyle *style = mCellStyles.value( EvenRows ) )
1524 if ( style->enabled && row % 2 == 1 )
1525 color = style->cellBackgroundColor;
1526 if ( QgsLayoutTableStyle *style = mCellStyles.value( FirstColumn ) )
1527 if ( style->enabled && column == 0 )
1528 color = style->cellBackgroundColor;
1529 if ( QgsLayoutTableStyle *style = mCellStyles.value( LastColumn ) )
1530 if ( style->enabled && ( column + columnSpan == mColumns.count() ) )
1531 color = style->cellBackgroundColor;
1532 if ( QgsLayoutTableStyle *style = mCellStyles.value( HeaderRow ) )
1533 if ( style->enabled && row == -1 )
1534 color = style->cellBackgroundColor;
1535 if ( QgsLayoutTableStyle *style = mCellStyles.value( FirstRow ) )
1536 if ( style->enabled && row == 0 )
1537 color = style->cellBackgroundColor;
1538 if ( QgsLayoutTableStyle *style = mCellStyles.value( LastRow ) )
1539 if ( style->enabled && ( row + rowSpan == mTableContents.count() ) )
1540 color = style->cellBackgroundColor;
1541
1542 if ( row >= 0 )
1543 {
1544 QgsConditionalStyle conditionalStyle = conditionalCellStyle( row, column );
1545 if ( conditionalStyle.backgroundColor().isValid() )
1546 color = conditionalStyle.backgroundColor();
1547 }
1548
1549 return color;
1550}
1551
1552void QgsLayoutTable::drawVerticalGridLines( QgsLayoutItemRenderContext &context, const QMap<int, double> &maxWidthMap, int firstRow, int lastRow, bool hasHeader, bool mergeCells ) const
1553{
1554 //vertical lines
1555 if ( lastRow - firstRow < 1 && !hasHeader )
1556 {
1557 return;
1558 }
1559
1560 QHash< QPair< int, int >, bool > skippedCellRightBorders;
1561 for ( int row = 0; row < lastRow; ++row )
1562 {
1563 for ( int col = 0; col < mColumns.size(); ++col )
1564 {
1565 if ( skippedCellRightBorders.constFind( qMakePair( row, col ) ) != skippedCellRightBorders.constEnd() )
1566 continue;
1567
1568 const int rowsSpan = rowSpan( row, col );
1569 const int colsSpan = columnSpan( row, col );
1570 skippedCellRightBorders.insert( qMakePair( row, col ), colsSpan > 1 );
1571
1572 for ( int colDelta = 0; colDelta < colsSpan - 1; ++colDelta )
1573 {
1574 for ( int rowDelta = 0; rowDelta < rowsSpan; ++rowDelta )
1575 {
1576 if ( rowDelta != 0 || colDelta != 0 )
1577 skippedCellRightBorders.insert( qMakePair( row + rowDelta, col + colDelta ), true );
1578 }
1579 }
1580 }
1581 }
1582
1583 QPainter *painter = context.renderContext().painter();
1584
1585 //calculate height of table within frame
1586 double tableHeight = 0;
1587 QList< double > rowHeights;
1588 if ( hasHeader )
1589 {
1590 rowHeights << mCellMargin * 2 + mMaxRowHeightMap[0];
1591 tableHeight += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + mCellMargin * 2 + mMaxRowHeightMap[0];
1592 }
1593 else
1594 {
1595 rowHeights << 0;
1596 }
1597 tableHeight += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
1598 double headerHeight = tableHeight;
1599
1600 double cellBodyHeightForEmptyRows = QgsTextRenderer::fontMetrics( context.renderContext(), mContentTextFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE ).ascent()
1603 for ( int row = firstRow; row < lastRow; ++row )
1604 {
1605 double rowHeight = row < mTableContents.count() ? mMaxRowHeightMap[row + 1] : cellBodyHeightForEmptyRows;
1606 rowHeights << rowHeight + mCellMargin * 2;
1607 tableHeight += rowHeight + ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + mCellMargin * 2;
1608 }
1609
1610 double currentX = ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 ) / 2.0;
1611 ;
1612 // left border of table
1613 painter->drawLine( QPointF( currentX, 0 ), QPointF( currentX, tableHeight ) );
1614 currentX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1615 QMap<int, double>::const_iterator maxColWidthIt = maxWidthMap.constBegin();
1616 int col = 1;
1617 for ( ; maxColWidthIt != maxWidthMap.constEnd(); ++maxColWidthIt )
1618 {
1619 currentX += ( maxColWidthIt.value() + 2 * mCellMargin );
1620 if ( col == maxWidthMap.size() )
1621 {
1622 // right border of table, always drawn
1623 painter->drawLine( QPointF( currentX, 0 ), QPointF( currentX, tableHeight ) );
1624 }
1625 else
1626 {
1627 if ( !mergeCells )
1628 {
1629 double startY = 0;
1630 double endY = startY + ( hasHeader ? ( rowHeights.value( 0 ) + ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) ) : 0 );
1631 for ( int row = firstRow; row < lastRow; ++row )
1632 {
1633 const double rowHeight = rowHeights.value( row - firstRow + 1 );
1634 if ( skippedCellRightBorders.value( qMakePair( row, col - 1 ) ) )
1635 {
1636 // flush existing line
1637 if ( !qgsDoubleNear( startY, endY ) )
1638 {
1639 painter->drawLine( QPointF( currentX, startY ), QPointF( currentX, endY ) );
1640 }
1641 endY += rowHeight;
1642 endY += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
1643 startY = endY;
1644 }
1645 else
1646 {
1647 endY += rowHeight;
1648 endY += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
1649 }
1650 }
1651
1652 // flush last line
1653 if ( !qgsDoubleNear( startY, endY ) )
1654 {
1655 painter->drawLine( QPointF( currentX, startY ), QPointF( currentX, endY ) );
1656 }
1657 }
1658 else if ( hasHeader )
1659 {
1660 painter->drawLine( QPointF( currentX, 0 ), QPointF( currentX, headerHeight ) );
1661 }
1662 }
1663
1664 currentX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1665 col++;
1666 }
1667}
1668
1670{
1672
1673 //force recalculation of frame rects, so that they are set to the correct
1674 //fixed and minimum frame sizes
1676}
1677
1679{
1680 return ( contents.indexOf( row ) >= 0 );
1681}
1682
1684{
1685 return mContentTextFormat;
1686}
1687
1692
1693Qt::Alignment QgsLayoutTable::horizontalAlignmentForCell( int, int column ) const
1694{
1695 return mColumns.value( column ).hAlignment();
1696}
1697
1698Qt::Alignment QgsLayoutTable::verticalAlignmentForCell( int, int column ) const
1699{
1700 return mColumns.value( column ).vAlignment();
1701}
@ Rectangle
Text within rectangle layout mode.
Definition qgis.h:3003
QFlags< TextRendererFlag > TextRendererFlags
Definition qgis.h:3525
@ Millimeters
Millimeters.
Definition qgis.h:5341
@ Points
Points (e.g., for font sizes).
Definition qgis.h:5345
@ Pixels
Pixels.
Definition qgis.h:5343
@ ApplyScalingWorkaroundForTextRendering
Whether a scaling workaround designed to stablise the rendering of small font sizes (or for painters ...
Definition qgis.h:2859
@ VerticalCenter
Center align.
Definition qgis.h:3064
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:3043
@ Center
Center align.
Definition qgis.h:3045
@ WrapLines
Automatically wrap long lines of text.
Definition qgis.h:3522
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:7504
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:7503
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:6975