QGIS API Documentation 3.99.0-Master (d270888f95f)
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 ) + 2 * mCellMargin + emptyRowContentFontMetrics.ascent() / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ) / QgsTextRenderer::FONT_WORKAROUND_SCALE;
325 currentRow += std::max( std::floor( contentHeight / rowHeight ), 0.0 );
326 }
327
328 return currentRow - firstRow - 1;
329}
330
331int QgsLayoutTable::rowsVisible( QgsRenderContext &context, int frameIndex, int firstRow, bool includeEmptyRows ) const
332{
333 //get frame extent
334 if ( frameIndex >= frameCount() )
335 {
336 return 0;
337 }
338 QRectF frameExtent = frame( frameIndex )->extent();
339
340 bool includeHeader = false;
343 {
344 includeHeader = true;
345 }
346 return rowsVisible( context, frameExtent.height(), firstRow, includeHeader, includeEmptyRows );
347}
348
349QPair<int, int> QgsLayoutTable::rowRange( QgsRenderContext &context, const int frameIndex ) const
350{
351 //calculate row height
352 if ( frameIndex >= frameCount() )
353 {
354 //bad frame index
355 return qMakePair( 0, 0 );
356 }
357
358 //loop through all previous frames to calculate how many rows are visible in each
359 //as the entire height of a frame may not be utilized for content rows
360 int rowsAlreadyShown = 0;
361 for ( int idx = 0; idx < frameIndex; ++idx )
362 {
363 rowsAlreadyShown += rowsVisible( context, idx, rowsAlreadyShown, false );
364 }
365
366 //using zero based indexes
367 int firstVisible = std::min( rowsAlreadyShown, static_cast<int>( mTableContents.length() ) );
368 int possibleRowsVisible = rowsVisible( context, frameIndex, rowsAlreadyShown, false );
369 int lastVisible = std::min( firstVisible + possibleRowsVisible, static_cast<int>( mTableContents.length() ) );
370
371 return qMakePair( firstVisible, lastVisible );
372}
373
374void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &, const int frameIndex )
375{
376 bool emptyTable = mTableContents.length() == 0;
377 if ( emptyTable && mEmptyTableMode == QgsLayoutTable::HideTable )
378 {
379 //empty table set to hide table mode, so don't draw anything
380 return;
381 }
382
383 if ( !mLayout->renderContext().isPreviewRender() )
384 {
385 //exporting composition, so force an attribute refresh
386 //we do this in case vector layer has changed via an external source (e.g., another database user)
388 }
389
390 const bool prevTextFormatScaleFlag = context.renderContext().testFlag( Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering );
392
393 //calculate which rows to show in this frame
394 QPair< int, int > rowsToShow = rowRange( context.renderContext(), frameIndex );
395
396 double gridSizeX = mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0;
397 double gridSizeY = mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0;
398 double cellHeaderHeight = mMaxRowHeightMap[0] + 2 * mCellMargin;
400 QRectF cell;
401
402 //calculate whether a header is required
403 bool drawHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && frameIndex < 1 )
405 //calculate whether drawing table contents is required
406 bool drawContents = !( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage );
407
408 int numberRowsToDraw = rowsToShow.second - rowsToShow.first;
409 int numberEmptyRows = 0;
410 if ( drawContents && mShowEmptyRows )
411 {
412 numberRowsToDraw = rowsVisible( context.renderContext(), frameIndex, rowsToShow.first, true );
413 numberEmptyRows = numberRowsToDraw - rowsToShow.second + rowsToShow.first;
414 }
415 bool mergeCells = false;
416 if ( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage )
417 {
418 //draw a merged row for the empty table message
419 numberRowsToDraw++;
420 rowsToShow.second++;
421 mergeCells = true;
422 }
423
424 QPainter *p = context.renderContext().painter();
425 QgsScopedQPainterState painterState( p );
426 // painter is scaled to dots, so scale back to layout units
427 p->scale( context.renderContext().scaleFactor(), context.renderContext().scaleFactor() );
428
429 //draw the text
430 p->setPen( Qt::SolidLine );
431
432 double currentX = gridSizeX;
433 double currentY = gridSizeY;
434 if ( drawHeader )
435 {
436 //draw the headers
437 int col = 0;
438 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
439 {
440 auto headerCellScope = std::make_unique< QgsExpressionContextScope >();
441 headerCellScope->setVariable( u"column_number"_s, col + 1, true );
442 QgsExpressionContextScopePopper popper( context.renderContext().expressionContext(), headerCellScope.release() );
443
444 const QgsTextFormat headerFormat = textFormatForHeader( col );
445 //draw background
446 p->save();
447 p->setPen( Qt::NoPen );
448 p->setBrush( backgroundColor( -1, col ) );
449 p->drawRect( QRectF( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, cellHeaderHeight ) );
450 p->restore();
451
452 currentX += mCellMargin;
453
454 cell = QRectF( currentX, currentY, mMaxColumnWidthMap[col], cellHeaderHeight );
455
456 //calculate alignment of header
458 switch ( mHeaderHAlignment )
459 {
460 case FollowColumn:
461 headerAlign = QgsTextRenderer::convertQtHAlignment( column.hAlignment() );
462 break;
463 case HeaderLeft:
465 break;
466 case HeaderCenter:
468 break;
469 case HeaderRight:
471 break;
472 }
473
474 const QRectF textCell = QRectF( currentX, currentY + mCellMargin, mMaxColumnWidthMap[col], cellHeaderHeight - 2 * mCellMargin );
475
476 const QStringList str = column.heading().split( '\n' );
477
478 // scale to dots
479 {
481 QgsTextRenderer::drawText( QRectF( textCell.left() * context.renderContext().scaleFactor(),
482 textCell.top() * context.renderContext().scaleFactor(),
483 textCell.width() * context.renderContext().scaleFactor(),
484 textCell.height() * context.renderContext().scaleFactor() ), 0,
485 headerAlign, str, context.renderContext(), headerFormat, true, Qgis::TextVerticalAlignment::VerticalCenter,
487 );
488 }
489
490 currentX += mMaxColumnWidthMap[ col ];
491 currentX += mCellMargin;
492 currentX += gridSizeX;
493 col++;
494 }
495
496 currentY += cellHeaderHeight;
497 currentY += gridSizeY;
498 }
499
500 //now draw the body cells
501 int rowsDrawn = 0;
502 std::set< std::pair< int, int > > spannedCells;
503 if ( drawContents )
504 {
505 //draw the attribute values
506 for ( int row = rowsToShow.first; row < rowsToShow.second; ++row )
507 {
508 rowsDrawn++;
509 currentX = gridSizeX;
510 int col = 0;
511
512 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
513 {
514 ( void )column;
515
516 bool isSpanned = false;
517 QRectF fullCell;
518
519 double cellHeight = 0;
520 double cellWidth = 0;
521 const int rowsSpan = rowSpan( row, col );
522 const int colsSpan = columnSpan( row, col );
523 if ( spannedCells.find( std::make_pair( row, col ) ) != spannedCells.end() )
524 {
525 isSpanned = true;
526 }
527 else
528 {
529 for ( int spannedRow = row; spannedRow < row + rowsSpan; ++spannedRow )
530 {
531 cellHeight += mMaxRowHeightMap[spannedRow + 1] + 2 * mCellMargin
532 + ( spannedRow > row ? gridSizeY : 0 );
533 for ( int spannedCol = col; spannedCol < col + colsSpan; ++spannedCol )
534 {
535 spannedCells.insert( std::make_pair( spannedRow, spannedCol ) );
536 }
537 }
538 for ( int spannedCol = col; spannedCol < col + colsSpan; ++spannedCol )
539 {
540 cellWidth += mMaxColumnWidthMap[spannedCol] + 2 * mCellMargin
541 + ( spannedCol > col ? gridSizeX : 0 );
542 }
543 }
544
545 fullCell = QRectF( currentX, currentY, cellWidth, cellHeight );
546
547 if ( !isSpanned )
548 {
549 //draw background
550 p->save();
551 p->setPen( Qt::NoPen );
552 p->setBrush( backgroundColor( row, col, rowsSpan, colsSpan ) );
553 p->drawRect( fullCell );
554 p->restore();
555 }
556
557 // currentY = gridSize;
558 currentX += mCellMargin;
559
560 if ( !isSpanned )
561 {
562 QVariant cellContents = mTableContents.at( row ).at( col );
563 const QString localizedString { QgsExpressionUtils::toLocalizedString( cellContents ) };
564 const QStringList str = localizedString.split( '\n' );
565
566 QgsTextFormat cellFormat = textFormatForCell( row, col );
568 cellFormat.updateDataDefinedProperties( context.renderContext() );
569
570 p->save();
571 p->setClipRect( fullCell );
572 const QRectF textCell = QRectF( currentX, currentY + mCellMargin, cellWidth - 2 * mCellMargin, cellHeight - 2 * mCellMargin );
573
574 const QgsConditionalStyle style = conditionalCellStyle( row, col );
575 QColor foreColor = cellFormat.color();
576 if ( style.textColor().isValid() )
577 foreColor = style.textColor();
578
579 cellFormat.setColor( foreColor );
580
581 // scale to dots
582 {
584 QgsTextRenderer::drawText( QRectF( textCell.left() * context.renderContext().scaleFactor(),
585 textCell.top() * context.renderContext().scaleFactor(),
586 textCell.width() * context.renderContext().scaleFactor(),
587 textCell.height() * context.renderContext().scaleFactor() ), 0,
588 QgsTextRenderer::convertQtHAlignment( horizontalAlignmentForCell( row, col ) ), str, context.renderContext(), cellFormat, true,
591 }
592 p->restore();
593 }
594
595 currentX += mMaxColumnWidthMap[ col ];
596 currentX += mCellMargin;
597 currentX += gridSizeX;
598 col++;
599 }
600 currentY += mMaxRowHeightMap[row + 1] + 2 * mCellMargin;
601 currentY += gridSizeY;
602 }
603 }
604
605 if ( numberRowsToDraw > rowsDrawn )
606 {
607 p->save();
608 p->setPen( Qt::NoPen );
609
610 //draw background of empty rows
611 for ( int row = rowsDrawn; row < numberRowsToDraw; ++row )
612 {
613 currentX = gridSizeX;
614 int col = 0;
615
616 if ( mergeCells )
617 {
618 p->setBrush( backgroundColor( row + 10000, 0 ) );
619 p->drawRect( QRectF( gridSizeX, currentY, mTableSize.width() - 2 * gridSizeX, cellBodyHeightForEmptyRows ) );
620 }
621 else
622 {
623 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
624 {
625 Q_UNUSED( column )
626
627 //draw background
628
629 //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
630 p->setBrush( backgroundColor( row + 10000, col ) );
631 p->drawRect( QRectF( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, cellBodyHeightForEmptyRows ) );
632
633 // currentY = gridSize;
634 currentX += mMaxColumnWidthMap[ col ] + 2 * mCellMargin;
635 currentX += gridSizeX;
636 col++;
637 }
638 }
639 currentY += cellBodyHeightForEmptyRows + gridSizeY;
640 }
641 p->restore();
642 }
643
644 //and the borders
645 if ( mShowGrid )
646 {
647 QPen gridPen;
648 gridPen.setWidthF( mGridStrokeWidth );
649 gridPen.setColor( mGridColor );
650 gridPen.setJoinStyle( Qt::MiterJoin );
651 gridPen.setCapStyle( Qt::FlatCap );
652 p->setPen( gridPen );
653 if ( mHorizontalGrid )
654 {
655 drawHorizontalGridLines( context, rowsToShow.first, rowsToShow.second + numberEmptyRows, drawHeader );
656 }
657 if ( mVerticalGrid )
658 {
659 drawVerticalGridLines( context, mMaxColumnWidthMap, rowsToShow.first, rowsToShow.second + numberEmptyRows, drawHeader, mergeCells );
660 }
661 }
662
663 //special case - no records and table is set to ShowMessage mode
664 if ( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage )
665 {
666 double messageX = gridSizeX + mCellMargin;
667 double messageY = gridSizeY + ( drawHeader ? cellHeaderHeight + gridSizeY : 0 );
668 cell = QRectF( messageX, messageY, mTableSize.width() - messageX, cellBodyHeightForEmptyRows );
669
670 // scale to dots
671 {
673 QgsTextRenderer::drawText( QRectF( cell.left() * context.renderContext().scaleFactor(),
674 cell.top() * context.renderContext().scaleFactor(),
675 cell.width() * context.renderContext().scaleFactor(),
676 cell.height() * context.renderContext().scaleFactor() ), 0,
678 }
679 }
680
682}
683
684void QgsLayoutTable::setCellMargin( const double margin )
685{
686 if ( qgsDoubleNear( margin, mCellMargin ) )
687 {
688 return;
689 }
690
691 mCellMargin = margin;
692
693 //since spacing has changed, we need to recalculate the table size
695
696 emit changed();
697}
698
700{
701 if ( mode == mEmptyTableMode )
702 {
703 return;
704 }
705
706 mEmptyTableMode = mode;
707
708 //since appearance has changed, we need to recalculate the table size
710
711 emit changed();
712}
713
714void QgsLayoutTable::setEmptyTableMessage( const QString &message )
715{
716 if ( message == mEmptyTableMessage )
717 {
718 return;
719 }
720
721 mEmptyTableMessage = message;
722
723 //since message has changed, we need to recalculate the table size
725
726 emit changed();
727}
728
729void QgsLayoutTable::setShowEmptyRows( const bool showEmpty )
730{
731 if ( showEmpty == mShowEmptyRows )
732 {
733 return;
734 }
735
736 mShowEmptyRows = showEmpty;
737 update();
738 emit changed();
739}
740
741void QgsLayoutTable::setHeaderFont( const QFont &font )
742{
743 mHeaderTextFormat.setFont( font );
744 if ( font.pointSizeF() > 0 )
745 {
746 mHeaderTextFormat.setSize( font.pointSizeF() );
748 }
749 else if ( font.pixelSize() > 0 )
750 {
751 mHeaderTextFormat.setSize( font.pixelSize() );
753 }
754
755 //since font attributes have changed, we need to recalculate the table size
757
758 emit changed();
759}
760
762{
763 return mHeaderTextFormat.toQFont();
764}
765
766void QgsLayoutTable::setHeaderFontColor( const QColor &color )
767{
768 if ( color == mHeaderTextFormat.color() )
769 {
770 return;
771 }
772
773 mHeaderTextFormat.setColor( color );
774 update();
775
776 emit changed();
777}
778
780{
781 return mHeaderTextFormat.color();
782}
783
785{
786 mHeaderTextFormat = format;
787
788 //since font attributes have changed, we need to recalculate the table size
790
791 emit changed();
792}
793
798
800{
801 if ( alignment == mHeaderHAlignment )
802 {
803 return;
804 }
805
806 mHeaderHAlignment = alignment;
807 update();
808
809 emit changed();
810}
811
813{
814 if ( mode == mHeaderMode )
815 {
816 return;
817 }
818
819 mHeaderMode = mode;
821
822 emit changed();
823}
824
825void QgsLayoutTable::setContentFont( const QFont &font )
826{
827 mContentTextFormat.setFont( font );
828 if ( font.pointSizeF() > 0 )
829 {
830 mContentTextFormat.setSize( font.pointSizeF() );
832 }
833 else if ( font.pixelSize() > 0 )
834 {
835 mContentTextFormat.setSize( font.pixelSize() );
837 }
838
839 //since font attributes have changed, we need to recalculate the table size
841
842 emit changed();
843}
844
846{
847 return mContentTextFormat.toQFont();
848}
849
850void QgsLayoutTable::setContentFontColor( const QColor &color )
851{
852 if ( color == mContentTextFormat.color() )
853 {
854 return;
855 }
856
857 mContentTextFormat.setColor( color );
858 update();
859
860 emit changed();
861}
862
864{
865 return mContentTextFormat.color();
866}
867
869{
870 mContentTextFormat = format;
871
872 //since spacing has changed, we need to recalculate the table size
874
875 emit changed();
876}
877
882
884{
885 if ( showGrid == mShowGrid )
886 {
887 return;
888 }
889
891 //since grid spacing has changed, we need to recalculate the table size
893
894 emit changed();
895}
896
897void QgsLayoutTable::setGridStrokeWidth( const double width )
898{
899 if ( qgsDoubleNear( width, mGridStrokeWidth ) )
900 {
901 return;
902 }
903
904 mGridStrokeWidth = width;
905 //since grid spacing has changed, we need to recalculate the table size
907
908 emit changed();
909}
910
911void QgsLayoutTable::setGridColor( const QColor &color )
912{
913 if ( color == mGridColor )
914 {
915 return;
916 }
917
918 mGridColor = color;
919 update();
920
921 emit changed();
922}
923
925{
927 {
928 return;
929 }
930
932 //since grid spacing has changed, we need to recalculate the table size
934
935 emit changed();
936}
937
939{
941 {
942 return;
943 }
944
946 //since grid spacing has changed, we need to recalculate the table size
948
949 emit changed();
950}
951
952void QgsLayoutTable::setBackgroundColor( const QColor &color )
953{
954 if ( color == mBackgroundColor )
955 {
956 return;
957 }
958
959 mBackgroundColor = color;
960 update();
961
962 emit changed();
963}
964
966{
967 if ( behavior == mWrapBehavior )
968 {
969 return;
970 }
971
972 mWrapBehavior = behavior;
974
975 emit changed();
976}
977
979{
980 //remove existing columns
982
983 // backward compatibility
984 // test if sorting is provided with the columns and call setSortColumns in such case
985 QgsLayoutTableSortColumns newSortColumns;
987 std::copy_if( mColumns.begin(), mColumns.end(), std::back_inserter( newSortColumns ), []( const QgsLayoutTableColumn & col ) {return col.sortByRank() > 0;} );
988 if ( !newSortColumns.isEmpty() )
989 {
990 std::sort( newSortColumns.begin(), newSortColumns.end(), []( const QgsLayoutTableColumn & a, const QgsLayoutTableColumn & b ) {return a.sortByRank() < b.sortByRank();} );
991 setSortColumns( newSortColumns );
992 }
994}
995
1000
1002{
1003 if ( mCellStyles.contains( group ) )
1004 delete mCellStyles.take( group );
1005
1006 mCellStyles.insert( group, new QgsLayoutTableStyle( style ) );
1007}
1008
1010{
1011 if ( !mCellStyles.contains( group ) )
1012 return nullptr;
1013
1014 return mCellStyles.value( group );
1015}
1016
1017QMap<int, QString> QgsLayoutTable::headerLabels() const
1018{
1019 QMap<int, QString> headers;
1020
1021 int i = 0;
1022 for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1023 {
1024 headers.insert( i, col.heading() );
1025 i++;
1026 }
1027 return headers;
1028}
1029
1031{
1032 auto cellScope = std::make_unique< QgsExpressionContextScope >();
1033 cellScope->setVariable( u"row_number"_s, row + 1, true );
1034 cellScope->setVariable( u"column_number"_s, column + 1, true );
1035 return cellScope.release();
1036}
1037
1038int QgsLayoutTable::rowSpan( int, int ) const
1039{
1040 return 1;
1041}
1042
1043int QgsLayoutTable::columnSpan( int, int ) const
1044{
1045 return 1;
1046}
1047
1052
1054{
1055 Q_UNUSED( frameIndex )
1056 return QSizeF( mTableSize.width(), 0 );
1057}
1058
1060{
1063
1064 double height = 0;
1067 {
1068 //header required, force frame to be high enough for header
1069 for ( int col = 0; col < mColumns.size(); ++ col )
1070 {
1072 }
1073 }
1074 return QSizeF( 0, height );
1075}
1076
1078{
1079 mMaxColumnWidthMap.clear();
1080 mMaxRowHeightMap.clear();
1081 mTableContents.clear();
1082
1083 //get new contents
1085 {
1086 return;
1087 }
1088}
1089
1095
1096void QgsLayoutTable::initStyles()
1097{
1100 mCellStyles.insert( OddRows, new QgsLayoutTableStyle() );
1101 mCellStyles.insert( EvenRows, new QgsLayoutTableStyle() );
1104 mCellStyles.insert( HeaderRow, new QgsLayoutTableStyle() );
1105 mCellStyles.insert( FirstRow, new QgsLayoutTableStyle() );
1106 mCellStyles.insert( LastRow, new QgsLayoutTableStyle() );
1107
1108 mCellStyleNames.insert( OddColumns, u"oddColumns"_s );
1109 mCellStyleNames.insert( EvenColumns, u"evenColumns"_s );
1110 mCellStyleNames.insert( OddRows, u"oddRows"_s );
1111 mCellStyleNames.insert( EvenRows, u"evenRows"_s );
1112 mCellStyleNames.insert( FirstColumn, u"firstColumn"_s );
1113 mCellStyleNames.insert( LastColumn, u"lastColumn"_s );
1114 mCellStyleNames.insert( HeaderRow, u"headerRow"_s );
1115 mCellStyleNames.insert( FirstRow, u"firstRow"_s );
1116 mCellStyleNames.insert( LastRow, u"lastRow"_s );
1117}
1118
1120{
1121 mMaxColumnWidthMap.clear();
1122
1123 //total number of cells (rows + 1 for header)
1124 int cols = mColumns.count();
1125 int cells = cols * ( mTableContents.count() + 1 );
1126 QVector< double > widths( cells );
1127
1128 double currentCellTextWidth;
1129
1132
1133 //first, go through all the column headers and calculate the sizes
1134 int i = 0;
1135 for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1136 {
1137 if ( col.width() > 0 )
1138 {
1139 //column has manually specified width
1140 widths[i] = col.width();
1141 }
1143 {
1144 auto headerCellScope = std::make_unique< QgsExpressionContextScope >();
1145 headerCellScope->setVariable( u"column_number"_s, i + 1, true );
1146 QgsExpressionContextScopePopper popper( context.expressionContext(), headerCellScope.release() );
1147
1148 //column width set to automatic, so check content size
1149 const QStringList multiLineSplit = col.heading().split( '\n' );
1150 currentCellTextWidth = QgsTextRenderer::textWidth( context, textFormatForHeader( i ), multiLineSplit ) / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
1151 widths[i] = currentCellTextWidth;
1152 }
1153 else
1154 {
1155 widths[i] = 0.0;
1156 }
1157 i++;
1158 }
1159
1160 //next, go through all the table contents and calculate the sizes
1161 QgsLayoutTableContents::const_iterator rowIt = mTableContents.constBegin();
1162 int row = 1;
1163 for ( ; rowIt != mTableContents.constEnd(); ++rowIt )
1164 {
1165 QgsLayoutTableRow::const_iterator colIt = rowIt->constBegin();
1166 int col = 0;
1167 for ( ; colIt != rowIt->constEnd(); ++colIt )
1168 {
1169 if ( mColumns.at( col ).width() <= 0 )
1170 {
1171 //column width set to automatic, so check content size
1172 const QStringList multiLineSplit = QgsExpressionUtils::toLocalizedString( *colIt ).split( '\n' );
1173
1174 QgsTextFormat cellFormat = textFormatForCell( row - 1, col );
1175 QgsExpressionContextScopePopper popper( context.expressionContext(), scopeForCell( row - 1, col ) );
1176 cellFormat.updateDataDefinedProperties( context );
1177
1178 currentCellTextWidth = QgsTextRenderer::textWidth( context, cellFormat, multiLineSplit ) / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
1179 widths[ row * cols + col ] = currentCellTextWidth;
1180 }
1181 else
1182 {
1183 widths[ row * cols + col ] = 0;
1184 }
1185
1186 col++;
1187 }
1188 row++;
1189 }
1190
1191 //calculate maximum
1192 for ( int col = 0; col < cols; ++col )
1193 {
1194 double maxColWidth = 0;
1195 for ( int row = 0; row < mTableContents.count() + 1; ++row )
1196 {
1197 maxColWidth = std::max( widths[ row * cols + col ], maxColWidth );
1198 }
1199 mMaxColumnWidthMap.insert( col, maxColWidth );
1200 }
1201
1202 return true;
1203}
1204
1206{
1207 mMaxRowHeightMap.clear();
1208
1209 //total number of cells (rows + 1 for header)
1210 int cols = mColumns.count();
1211 int cells = cols * ( mTableContents.count() + 1 );
1212 QVector< double > heights( cells );
1213
1216
1217 //first, go through all the column headers and calculate the sizes
1218 int i = 0;
1219 for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1220 {
1221 auto headerCellScope = std::make_unique< QgsExpressionContextScope >();
1222 headerCellScope->setVariable( u"column_number"_s, i + 1, true );
1223 QgsExpressionContextScopePopper popper( context.expressionContext(), headerCellScope.release() );
1224
1225 const QgsTextFormat cellFormat = textFormatForHeader( i );
1227 //height
1229 {
1230 heights[i] = 0;
1231 }
1232 else
1233 {
1234 heights[i] = QgsTextRenderer::textHeight( context,
1235 cellFormat,
1236 QStringList() << col.heading(), Qgis::TextLayoutMode::Rectangle,
1237 nullptr,
1240 )
1242 - headerDescentMm;
1243 }
1244 i++;
1245 }
1246
1247 //next, go through all the table contents and calculate the sizes
1248 QgsLayoutTableContents::const_iterator rowIt = mTableContents.constBegin();
1249 int row = 1;
1250 for ( ; rowIt != mTableContents.constEnd(); ++rowIt )
1251 {
1252 QgsLayoutTableRow::const_iterator colIt = rowIt->constBegin();
1253 int i = 0;
1254 for ( ; colIt != rowIt->constEnd(); ++colIt )
1255 {
1256 QgsTextFormat cellFormat = textFormatForCell( row - 1, i );
1257 QgsExpressionContextScopePopper popper( context.expressionContext(), scopeForCell( row - 1, i ) );
1258 cellFormat.updateDataDefinedProperties( context );
1260 const QString localizedString { QgsExpressionUtils::toLocalizedString( *colIt ) };
1261
1262 heights[ row * cols + i ] = QgsTextRenderer::textHeight( context,
1263 cellFormat,
1264 QStringList() << localizedString.split( '\n' ),
1266 nullptr,
1269 ) / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ) - contentDescentMm;
1270
1271 i++;
1272 }
1273 row++;
1274 }
1275
1276 //calculate maximum
1277 for ( int row = 0; row < mTableContents.count() + 1; ++row )
1278 {
1279 double maxRowHeight = 0;
1280 for ( int col = 0; col < cols; ++col )
1281 {
1282 maxRowHeight = std::max( heights[ row * cols + col ], maxRowHeight );
1283 }
1284 mMaxRowHeightMap.insert( row, maxRowHeight );
1285 }
1286
1287 return true;
1288}
1289
1291{
1292 //check how much space each column needs
1293 if ( !calculateMaxColumnWidths() )
1294 {
1295 return 0;
1296 }
1297
1298 //adapt frame to total width
1299 double totalWidth = 0;
1300 QMap<int, double>::const_iterator maxColWidthIt = mMaxColumnWidthMap.constBegin();
1301 for ( ; maxColWidthIt != mMaxColumnWidthMap.constEnd(); ++maxColWidthIt )
1302 {
1303 totalWidth += maxColWidthIt.value();
1304 }
1305 totalWidth += ( 2 * mMaxColumnWidthMap.size() * mCellMargin );
1306 totalWidth += ( mMaxColumnWidthMap.size() + 1 ) * ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1307
1308 return totalWidth;
1309}
1310
1312{
1313 //check how much space each row needs
1314 if ( !calculateMaxRowHeights() )
1315 {
1316 return 0;
1317 }
1318
1319 double height = 0;
1320
1323
1324 //loop through all existing frames to calculate how many rows are visible in each
1325 //as the entire height of a frame may not be utilized for content rows
1326 int rowsAlreadyShown = 0;
1327 int numberExistingFrames = frameCount();
1328 int rowsVisibleInLastFrame = 0;
1329 double heightOfLastFrame = 0;
1330 for ( int idx = 0; idx < numberExistingFrames; ++idx )
1331 {
1332 bool hasHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && idx == 0 )
1334 heightOfLastFrame = frame( idx )->rect().height();
1335 rowsVisibleInLastFrame = rowsVisible( context, heightOfLastFrame, rowsAlreadyShown, hasHeader, false );
1336 rowsAlreadyShown += rowsVisibleInLastFrame;
1337 height += heightOfLastFrame;
1338 if ( rowsAlreadyShown >= mTableContents.length() )
1339 {
1340 //shown entire contents of table, nothing remaining
1341 return height;
1342 }
1343 }
1344
1345 //calculate how many rows left to show
1346 int remainingRows = mTableContents.length() - rowsAlreadyShown;
1347
1348 if ( remainingRows <= 0 )
1349 {
1350 //no remaining rows
1351 return height;
1352 }
1353
1355 {
1356 QgsLayoutItemPage *page = mLayout->pageCollection()->page( mLayout->pageCollection()->pageCount() - 1 );
1357 if ( page )
1358 heightOfLastFrame = page->sizeWithUnits().height();
1359 }
1360
1361 bool hasHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && numberExistingFrames < 1 )
1363
1364 int numberFramesMissing = 0;
1365 while ( remainingRows > 0 )
1366 {
1367 numberFramesMissing++;
1368
1369 rowsVisibleInLastFrame = rowsVisible( context, heightOfLastFrame, rowsAlreadyShown, hasHeader, false );
1370 if ( rowsVisibleInLastFrame < 1 )
1371 {
1372 //if no rows are visible in the last frame, calculation of missing frames
1373 //is impossible. So just return total height of existing frames
1374 return height;
1375 }
1376
1377 rowsAlreadyShown += rowsVisibleInLastFrame;
1378 remainingRows = mTableContents.length() - rowsAlreadyShown;
1379 }
1380
1381 //rows remain unshown -- how many extra frames would we need to complete the table?
1382 //assume all added frames are same size as final frame
1383 height += heightOfLastFrame * numberFramesMissing;
1384 return height;
1385}
1386
1387void QgsLayoutTable::drawHorizontalGridLines( QgsLayoutItemRenderContext &context, int firstRow, int lastRow, bool drawHeaderLines ) const
1388{
1389 //horizontal lines
1390 if ( lastRow - firstRow < 1 && !drawHeaderLines )
1391 {
1392 return;
1393 }
1394
1395 QPainter *painter = context.renderContext().painter();
1396
1398 double halfGridStrokeWidth = ( mShowGrid ? mGridStrokeWidth : 0 ) / 2.0;
1399 double currentY = halfGridStrokeWidth;
1400 if ( drawHeaderLines )
1401 {
1402 painter->drawLine( QPointF( 0, currentY ), QPointF( mTableSize.width(), currentY ) );
1403 currentY += ( mShowGrid ? mGridStrokeWidth : 0 );
1404 currentY += mMaxRowHeightMap[0] + 2 * mCellMargin;
1405 }
1406
1407 QHash< QPair< int, int >, bool > skippedCellBottomBorders;
1408 for ( int row = 0; row < lastRow; ++row )
1409 {
1410 for ( int col = 0; col < mColumns.size(); ++col )
1411 {
1412 if ( skippedCellBottomBorders.constFind( qMakePair( row, col ) ) != skippedCellBottomBorders.constEnd() )
1413 continue;
1414
1415 const int rowsSpan = rowSpan( row, col );
1416 const int colsSpan = columnSpan( row, col );
1417 skippedCellBottomBorders.insert( qMakePair( row, col ), rowsSpan > 1 );
1418 for ( int rowDelta = 0; rowDelta < rowsSpan - 1; ++rowDelta )
1419 {
1420 for ( int colDelta = 0; colDelta < colsSpan; ++colDelta )
1421 {
1422 if ( rowDelta != 0 || colDelta != 0 )
1423 skippedCellBottomBorders.insert( qMakePair( row + rowDelta, col + colDelta ), true );
1424 }
1425 }
1426 }
1427 }
1428
1429 for ( int row = firstRow; row < lastRow; ++row )
1430 {
1431 double startX = 0;
1432 double endX = startX;
1433
1434 for ( int col = 0; col < mColumns.size(); ++col )
1435 {
1436 const double colWidth = mMaxColumnWidthMap.value( col ) + 2 * mCellMargin;
1437
1438 if ( skippedCellBottomBorders.value( qMakePair( row - 1, col ) ) )
1439 {
1440 // flush existing line
1441 if ( !qgsDoubleNear( startX, endX ) )
1442 {
1443 painter->drawLine( QPointF( startX, currentY ), QPointF( endX, currentY ) );
1444 }
1445 endX += colWidth;
1446 endX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1447 startX = endX;
1448 }
1449 else
1450 {
1451 endX += colWidth;
1452 endX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1453 }
1454 }
1455
1456 // flush last line
1457 if ( !qgsDoubleNear( startX, endX ) )
1458 {
1459 painter->drawLine( QPointF( startX, currentY ), QPointF( endX, currentY ) );
1460 }
1461
1462 currentY += ( mShowGrid ? mGridStrokeWidth : 0 );
1463 double rowHeight = row < mTableContents.count() ? mMaxRowHeightMap[row + 1] : cellBodyHeightForEmptyRows;
1464 currentY += ( rowHeight + 2 * mCellMargin );
1465 }
1466 painter->drawLine( QPointF( 0, currentY ), QPointF( mTableSize.width(), currentY ) );
1467}
1468
1469QColor QgsLayoutTable::backgroundColor( int row, int column, int rowSpan, int columnSpan ) const
1470{
1471 QColor color = mBackgroundColor;
1472 if ( QgsLayoutTableStyle *style = mCellStyles.value( OddColumns ) )
1473 if ( style->enabled && column % 2 == 0 )
1474 color = style->cellBackgroundColor;
1475 if ( QgsLayoutTableStyle *style = mCellStyles.value( EvenColumns ) )
1476 if ( style->enabled && column % 2 == 1 )
1477 color = style->cellBackgroundColor;
1478 if ( QgsLayoutTableStyle *style = mCellStyles.value( OddRows ) )
1479 if ( style->enabled && row % 2 == 0 )
1480 color = style->cellBackgroundColor;
1481 if ( QgsLayoutTableStyle *style = mCellStyles.value( EvenRows ) )
1482 if ( style->enabled && row % 2 == 1 )
1483 color = style->cellBackgroundColor;
1484 if ( QgsLayoutTableStyle *style = mCellStyles.value( FirstColumn ) )
1485 if ( style->enabled && column == 0 )
1486 color = style->cellBackgroundColor;
1487 if ( QgsLayoutTableStyle *style = mCellStyles.value( LastColumn ) )
1488 if ( style->enabled && ( column + columnSpan == mColumns.count() ) )
1489 color = style->cellBackgroundColor;
1490 if ( QgsLayoutTableStyle *style = mCellStyles.value( HeaderRow ) )
1491 if ( style->enabled && row == -1 )
1492 color = style->cellBackgroundColor;
1493 if ( QgsLayoutTableStyle *style = mCellStyles.value( FirstRow ) )
1494 if ( style->enabled && row == 0 )
1495 color = style->cellBackgroundColor;
1496 if ( QgsLayoutTableStyle *style = mCellStyles.value( LastRow ) )
1497 if ( style->enabled && ( row + rowSpan == mTableContents.count() ) )
1498 color = style->cellBackgroundColor;
1499
1500 if ( row >= 0 )
1501 {
1502 QgsConditionalStyle conditionalStyle = conditionalCellStyle( row, column );
1503 if ( conditionalStyle.backgroundColor().isValid() )
1504 color = conditionalStyle.backgroundColor();
1505 }
1506
1507 return color;
1508}
1509
1510void QgsLayoutTable::drawVerticalGridLines( QgsLayoutItemRenderContext &context, const QMap<int, double> &maxWidthMap, int firstRow, int lastRow, bool hasHeader, bool mergeCells ) const
1511{
1512 //vertical lines
1513 if ( lastRow - firstRow < 1 && !hasHeader )
1514 {
1515 return;
1516 }
1517
1518 QHash< QPair< int, int >, bool > skippedCellRightBorders;
1519 for ( int row = 0; row < lastRow; ++row )
1520 {
1521 for ( int col = 0; col < mColumns.size(); ++col )
1522 {
1523 if ( skippedCellRightBorders.constFind( qMakePair( row, col ) ) != skippedCellRightBorders.constEnd() )
1524 continue;
1525
1526 const int rowsSpan = rowSpan( row, col );
1527 const int colsSpan = columnSpan( row, col );
1528 skippedCellRightBorders.insert( qMakePair( row, col ), colsSpan > 1 );
1529
1530 for ( int colDelta = 0; colDelta < colsSpan - 1; ++colDelta )
1531 {
1532 for ( int rowDelta = 0; rowDelta < rowsSpan; ++rowDelta )
1533 {
1534 if ( rowDelta != 0 || colDelta != 0 )
1535 skippedCellRightBorders.insert( qMakePair( row + rowDelta, col + colDelta ), true );
1536 }
1537 }
1538 }
1539 }
1540
1541 QPainter *painter = context.renderContext().painter();
1542
1543 //calculate height of table within frame
1544 double tableHeight = 0;
1545 QList< double > rowHeights;
1546 if ( hasHeader )
1547 {
1548 rowHeights << mCellMargin * 2 + mMaxRowHeightMap[0];
1549 tableHeight += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + mCellMargin * 2 + mMaxRowHeightMap[0];
1550 }
1551 else
1552 {
1553 rowHeights << 0;
1554 }
1555 tableHeight += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
1556 double headerHeight = tableHeight;
1557
1559 for ( int row = firstRow; row < lastRow; ++row )
1560 {
1561 double rowHeight = row < mTableContents.count() ? mMaxRowHeightMap[row + 1] : cellBodyHeightForEmptyRows;
1562 rowHeights << rowHeight + mCellMargin * 2;
1563 tableHeight += rowHeight + ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + mCellMargin * 2;
1564 }
1565
1566 double currentX = ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 ) / 2.0;;
1567 // left border of table
1568 painter->drawLine( QPointF( currentX, 0 ), QPointF( currentX, tableHeight ) );
1569 currentX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1570 QMap<int, double>::const_iterator maxColWidthIt = maxWidthMap.constBegin();
1571 int col = 1;
1572 for ( ; maxColWidthIt != maxWidthMap.constEnd(); ++maxColWidthIt )
1573 {
1574 currentX += ( maxColWidthIt.value() + 2 * mCellMargin );
1575 if ( col == maxWidthMap.size() )
1576 {
1577 // right border of table, always drawn
1578 painter->drawLine( QPointF( currentX, 0 ), QPointF( currentX, tableHeight ) );
1579 }
1580 else
1581 {
1582 if ( !mergeCells )
1583 {
1584 double startY = 0;
1585 double endY = startY + ( hasHeader ? ( rowHeights.value( 0 ) + ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) ) : 0 );
1586 for ( int row = firstRow; row < lastRow; ++row )
1587 {
1588 const double rowHeight = rowHeights.value( row - firstRow + 1 );
1589 if ( skippedCellRightBorders.value( qMakePair( row, col - 1 ) ) )
1590 {
1591 // flush existing line
1592 if ( !qgsDoubleNear( startY, endY ) )
1593 {
1594 painter->drawLine( QPointF( currentX, startY ), QPointF( currentX, endY ) );
1595 }
1596 endY += rowHeight;
1597 endY += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
1598 startY = endY;
1599 }
1600 else
1601 {
1602 endY += rowHeight;
1603 endY += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
1604 }
1605 }
1606
1607 // flush last line
1608 if ( !qgsDoubleNear( startY, endY ) )
1609 {
1610 painter->drawLine( QPointF( currentX, startY ), QPointF( currentX, endY ) );
1611 }
1612 }
1613 else if ( hasHeader )
1614 {
1615 painter->drawLine( QPointF( currentX, 0 ), QPointF( currentX, headerHeight ) );
1616 }
1617 }
1618
1619 currentX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1620 col++;
1621 }
1622}
1623
1625{
1627
1628 //force recalculation of frame rects, so that they are set to the correct
1629 //fixed and minimum frame sizes
1631}
1632
1634{
1635 return ( contents.indexOf( row ) >= 0 );
1636}
1637
1639{
1640 return mContentTextFormat;
1641}
1642
1647
1648Qt::Alignment QgsLayoutTable::horizontalAlignmentForCell( int, int column ) const
1649{
1650 return mColumns.value( column ).hAlignment();
1651}
1652
1653Qt::Alignment QgsLayoutTable::verticalAlignmentForCell( int, int column ) const
1654{
1655 return mColumns.value( column ).vAlignment();
1656}
1657
@ Rectangle
Text within rectangle layout mode.
Definition qgis.h:2960
QFlags< TextRendererFlag > TextRendererFlags
Definition qgis.h:3470
@ Millimeters
Millimeters.
Definition qgis.h:5256
@ Points
Points (e.g., for font sizes).
Definition qgis.h:5260
@ Pixels
Pixels.
Definition qgis.h:5258
@ ApplyScalingWorkaroundForTextRendering
Whether a scaling workaround designed to stablise the rendering of small font sizes (or for painters ...
Definition qgis.h:2820
@ VerticalCenter
Center align.
Definition qgis.h:3021
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:3000
@ Center
Center align.
Definition qgis.h:3002
@ WrapLines
Automatically wrap long lines of text.
Definition qgis.h:3467
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:7451
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:7450
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:6900