QGIS API Documentation 3.32.0-Lima (311a8cb8a6)
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
19#include "qgsexpressionutils.h"
20#include "qgslayouttable.h"
21#include "qgslayoututils.h"
23#include "qgssymbollayerutils.h"
24#include "qgslayoutframe.h"
25#include "qgsfontutils.h"
27#include "qgstextrenderer.h"
29
30//
31// QgsLayoutTableStyle
32//
33
34bool QgsLayoutTableStyle::writeXml( QDomElement &styleElem, QDomDocument &doc ) const
35{
36 Q_UNUSED( doc )
37 styleElem.setAttribute( QStringLiteral( "cellBackgroundColor" ), QgsSymbolLayerUtils::encodeColor( cellBackgroundColor ) );
38 styleElem.setAttribute( QStringLiteral( "enabled" ), enabled );
39 return true;
40}
41
42bool QgsLayoutTableStyle::readXml( const QDomElement &styleElem )
43{
44 cellBackgroundColor = QgsSymbolLayerUtils::decodeColor( styleElem.attribute( QStringLiteral( "cellBackgroundColor" ), QStringLiteral( "255,255,255,255" ) ) );
45 enabled = ( styleElem.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ) != QLatin1String( "0" ) );
46 return true;
47}
48
49
50//
51// QgsLayoutTable
52//
53
55 : QgsLayoutMultiFrame( layout )
56{
57 initStyles();
58}
59
61{
62 mColumns.clear();
63 mSortColumns.clear();
64
65 qDeleteAll( mCellStyles );
66 mCellStyles.clear();
67}
68
69bool QgsLayoutTable::writePropertiesToElement( QDomElement &elem, QDomDocument &doc, const QgsReadWriteContext &context ) const
70{
71 elem.setAttribute( QStringLiteral( "cellMargin" ), QString::number( mCellMargin ) );
72 elem.setAttribute( QStringLiteral( "emptyTableMode" ), QString::number( static_cast< int >( mEmptyTableMode ) ) );
73 elem.setAttribute( QStringLiteral( "emptyTableMessage" ), mEmptyTableMessage );
74 elem.setAttribute( QStringLiteral( "showEmptyRows" ), mShowEmptyRows );
75
76 QDomElement headerElem = doc.createElement( QStringLiteral( "headerTextFormat" ) );
77 const QDomElement headerTextElem = mHeaderTextFormat.writeXml( doc, context );
78 headerElem.appendChild( headerTextElem );
79 elem.appendChild( headerElem );
80 elem.setAttribute( QStringLiteral( "headerHAlignment" ), QString::number( static_cast< int >( mHeaderHAlignment ) ) );
81 elem.setAttribute( QStringLiteral( "headerMode" ), QString::number( static_cast< int >( mHeaderMode ) ) );
82
83 QDomElement contentElem = doc.createElement( QStringLiteral( "contentTextFormat" ) );
84 const QDomElement contentTextElem = mContentTextFormat.writeXml( doc, context );
85 contentElem.appendChild( contentTextElem );
86 elem.appendChild( contentElem );
87 elem.setAttribute( QStringLiteral( "gridStrokeWidth" ), QString::number( mGridStrokeWidth ) );
88 elem.setAttribute( QStringLiteral( "gridColor" ), QgsSymbolLayerUtils::encodeColor( mGridColor ) );
89 elem.setAttribute( QStringLiteral( "horizontalGrid" ), mHorizontalGrid );
90 elem.setAttribute( QStringLiteral( "verticalGrid" ), mVerticalGrid );
91 elem.setAttribute( QStringLiteral( "showGrid" ), mShowGrid );
92 elem.setAttribute( QStringLiteral( "backgroundColor" ), QgsSymbolLayerUtils::encodeColor( mBackgroundColor ) );
93 elem.setAttribute( QStringLiteral( "wrapBehavior" ), QString::number( static_cast< int >( mWrapBehavior ) ) );
94
95 // display columns
96 QDomElement displayColumnsElem = doc.createElement( QStringLiteral( "displayColumns" ) );
97 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
98 {
99 QDomElement columnElem = doc.createElement( QStringLiteral( "column" ) );
100 column.writeXml( columnElem, doc );
101 displayColumnsElem.appendChild( columnElem );
102 }
103 elem.appendChild( displayColumnsElem );
104 // sort columns
105 QDomElement sortColumnsElem = doc.createElement( QStringLiteral( "sortColumns" ) );
106 for ( const QgsLayoutTableColumn &column : std::as_const( mSortColumns ) )
107 {
108 QDomElement columnElem = doc.createElement( QStringLiteral( "column" ) );
109 column.writeXml( columnElem, doc );
110 sortColumnsElem.appendChild( columnElem );
111 }
112 elem.appendChild( sortColumnsElem );
113
114
115 //cell styles
116 QDomElement stylesElem = doc.createElement( QStringLiteral( "cellStyles" ) );
117 QMap< CellStyleGroup, QString >::const_iterator it = mCellStyleNames.constBegin();
118 for ( ; it != mCellStyleNames.constEnd(); ++it )
119 {
120 QString styleName = it.value();
121 QDomElement styleElem = doc.createElement( styleName );
122 QgsLayoutTableStyle *style = mCellStyles.value( it.key() );
123 if ( style )
124 {
125 style->writeXml( styleElem, doc );
126 stylesElem.appendChild( styleElem );
127 }
128 }
129 elem.appendChild( stylesElem );
130 return true;
131}
132
133bool QgsLayoutTable::readPropertiesFromElement( const QDomElement &itemElem, const QDomDocument &, const QgsReadWriteContext &context )
134{
135 mEmptyTableMode = QgsLayoutTable::EmptyTableMode( itemElem.attribute( QStringLiteral( "emptyTableMode" ), QStringLiteral( "0" ) ).toInt() );
136 mEmptyTableMessage = itemElem.attribute( QStringLiteral( "emptyTableMessage" ), tr( "No matching records" ) );
137 mShowEmptyRows = itemElem.attribute( QStringLiteral( "showEmptyRows" ), QStringLiteral( "0" ) ).toInt();
138
139 const QDomElement headerTextFormat = itemElem.firstChildElement( QStringLiteral( "headerTextFormat" ) );
140 if ( !headerTextFormat.isNull() )
141 {
142 QDomNodeList textFormatNodeList = headerTextFormat.elementsByTagName( QStringLiteral( "text-style" ) );
143 QDomElement textFormatElem = textFormatNodeList.at( 0 ).toElement();
144 mHeaderTextFormat.readXml( textFormatElem, context );
145 }
146 else
147 {
148 QFont headerFont;
149 if ( !QgsFontUtils::setFromXmlChildNode( headerFont, itemElem, QStringLiteral( "headerFontProperties" ) ) )
150 {
151 headerFont.fromString( itemElem.attribute( QStringLiteral( "headerFont" ), QString() ) );
152 }
153 QColor headerFontColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "headerFontColor" ), QStringLiteral( "0,0,0,255" ) ) );
155 if ( headerFont.pointSizeF() > 0 )
156 {
157 mHeaderTextFormat.setSize( headerFont.pointSizeF() );
158 mHeaderTextFormat.setSizeUnit( Qgis::RenderUnit::Points );
159 }
160 else if ( headerFont.pixelSize() > 0 )
161 {
162 mHeaderTextFormat.setSize( headerFont.pixelSize() );
163 mHeaderTextFormat.setSizeUnit( Qgis::RenderUnit::Pixels );
164 }
166 }
167
168 mHeaderHAlignment = QgsLayoutTable::HeaderHAlignment( itemElem.attribute( QStringLiteral( "headerHAlignment" ), QStringLiteral( "0" ) ).toInt() );
169 mHeaderMode = QgsLayoutTable::HeaderMode( itemElem.attribute( QStringLiteral( "headerMode" ), QStringLiteral( "0" ) ).toInt() );
170
171 const QDomElement contentTextFormat = itemElem.firstChildElement( QStringLiteral( "contentTextFormat" ) );
172 if ( !contentTextFormat.isNull() )
173 {
174 QDomNodeList textFormatNodeList = contentTextFormat.elementsByTagName( QStringLiteral( "text-style" ) );
175 QDomElement textFormatElem = textFormatNodeList.at( 0 ).toElement();
176 mContentTextFormat.readXml( textFormatElem, context );
177 }
178 else
179 {
180 QFont contentFont;
181 if ( !QgsFontUtils::setFromXmlChildNode( contentFont, itemElem, QStringLiteral( "contentFontProperties" ) ) )
182 {
183 contentFont.fromString( itemElem.attribute( QStringLiteral( "contentFont" ), QString() ) );
184 }
185 QColor contentFontColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "contentFontColor" ), QStringLiteral( "0,0,0,255" ) ) );
187 if ( contentFont.pointSizeF() > 0 )
188 {
189 mContentTextFormat.setSize( contentFont.pointSizeF() );
190 mContentTextFormat.setSizeUnit( Qgis::RenderUnit::Points );
191 }
192 else if ( contentFont.pixelSize() > 0 )
193 {
195 mContentTextFormat.setSizeUnit( Qgis::RenderUnit::Pixels );
196 }
198 }
199
200 mCellMargin = itemElem.attribute( QStringLiteral( "cellMargin" ), QStringLiteral( "1.0" ) ).toDouble();
201 mGridStrokeWidth = itemElem.attribute( QStringLiteral( "gridStrokeWidth" ), QStringLiteral( "0.5" ) ).toDouble();
202 mHorizontalGrid = itemElem.attribute( QStringLiteral( "horizontalGrid" ), QStringLiteral( "1" ) ).toInt();
203 mVerticalGrid = itemElem.attribute( QStringLiteral( "verticalGrid" ), QStringLiteral( "1" ) ).toInt();
204 mShowGrid = itemElem.attribute( QStringLiteral( "showGrid" ), QStringLiteral( "1" ) ).toInt();
205 mGridColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "gridColor" ), QStringLiteral( "0,0,0,255" ) ) );
206 mBackgroundColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "backgroundColor" ), QStringLiteral( "255,255,255,0" ) ) );
207 mWrapBehavior = QgsLayoutTable::WrapBehavior( itemElem.attribute( QStringLiteral( "wrapBehavior" ), QStringLiteral( "0" ) ).toInt() );
208
209 //restore display column specifications
210 mColumns.clear();
211 QDomNodeList columnsList = itemElem.elementsByTagName( QStringLiteral( "displayColumns" ) );
212 if ( !columnsList.isEmpty() )
213 {
214 QDomElement columnsElem = columnsList.at( 0 ).toElement();
215 QDomNodeList columnEntryList = columnsElem.elementsByTagName( QStringLiteral( "column" ) );
216 for ( int i = 0; i < columnEntryList.size(); ++i )
217 {
218 QDomElement columnElem = columnEntryList.at( i ).toElement();
220 column.readXml( columnElem );
221 mColumns.append( column );
222 }
223 }
224 // sort columns
225 mSortColumns.clear();
226 QDomNodeList sortColumnsList = itemElem.elementsByTagName( QStringLiteral( "sortColumns" ) );
227 if ( !sortColumnsList.isEmpty() )
228 {
229 QDomElement columnsElem = sortColumnsList.at( 0 ).toElement();
230 QDomNodeList columnEntryList = columnsElem.elementsByTagName( QStringLiteral( "column" ) );
231 for ( int i = 0; i < columnEntryList.size(); ++i )
232 {
233 QDomElement columnElem = columnEntryList.at( i ).toElement();
235 column.readXml( columnElem );
236 mSortColumns.append( column );
237 }
238 }
239 else
240 {
241 // backward compatibility for QGIS < 3.14
242 // copy the display columns if sortByRank > 0 and then, sort them by rank
244 std::copy_if( mColumns.begin(), mColumns.end(), std::back_inserter( mSortColumns ), []( const QgsLayoutTableColumn & col ) {return col.sortByRank() > 0;} );
245 std::sort( mSortColumns.begin(), mSortColumns.end(), []( const QgsLayoutTableColumn & a, const QgsLayoutTableColumn & b ) {return a.sortByRank() < b.sortByRank();} );
247 }
248
249 //restore cell styles
250 QDomNodeList stylesList = itemElem.elementsByTagName( QStringLiteral( "cellStyles" ) );
251 if ( !stylesList.isEmpty() )
252 {
253 QDomElement stylesElem = stylesList.at( 0 ).toElement();
254
255 QMap< CellStyleGroup, QString >::const_iterator it = mCellStyleNames.constBegin();
256 for ( ; it != mCellStyleNames.constEnd(); ++it )
257 {
258 QString styleName = it.value();
259 QDomNodeList styleList = stylesElem.elementsByTagName( styleName );
260 if ( !styleList.isEmpty() )
261 {
262 QDomElement styleElem = styleList.at( 0 ).toElement();
263 QgsLayoutTableStyle *style = mCellStyles.value( it.key() );
264 if ( style )
265 style->readXml( styleElem );
266 }
267 }
268 }
269
270 emit changed();
271 return true;
272}
273
275{
276 return mTableSize;
277}
278
280{
283}
284
285int QgsLayoutTable::rowsVisible( QgsRenderContext &context, double frameHeight, int firstRow, bool includeHeader, bool includeEmptyRows ) const
286{
287 //calculate header height
288 double headerHeight = 0;
289 if ( includeHeader )
290 {
291 headerHeight = mMaxRowHeightMap.value( 0 ) + 2 * ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + 2 * mCellMargin;
292 }
293 else
294 {
295 //frame has no header text, just the stroke
296 headerHeight = ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
297 }
298
299 //remaining height available for content rows
300 double contentHeight = frameHeight - headerHeight;
301
302 double gridHeight = ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
303
304 int currentRow = firstRow;
305 while ( contentHeight > 0 && currentRow <= mTableContents.count() )
306 {
307 double currentRowHeight = mMaxRowHeightMap.value( currentRow + 1 ) + gridHeight + 2 * mCellMargin;
308 contentHeight -= currentRowHeight;
309 currentRow++;
310 }
311
312 if ( includeEmptyRows && contentHeight > 0 )
313 {
314 const QFontMetricsF emptyRowContentFontMetrics = QgsTextRenderer::fontMetrics( context, mContentTextFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE );
315 double rowHeight = ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + 2 * mCellMargin + emptyRowContentFontMetrics.ascent() / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ) / QgsTextRenderer::FONT_WORKAROUND_SCALE;
316 currentRow += std::max( std::floor( contentHeight / rowHeight ), 0.0 );
317 }
318
319 return currentRow - firstRow - 1;
320}
321
322int QgsLayoutTable::rowsVisible( QgsRenderContext &context, int frameIndex, int firstRow, bool includeEmptyRows ) const
323{
324 //get frame extent
325 if ( frameIndex >= frameCount() )
326 {
327 return 0;
328 }
329 QRectF frameExtent = frame( frameIndex )->extent();
330
331 bool includeHeader = false;
334 {
335 includeHeader = true;
336 }
337 return rowsVisible( context, frameExtent.height(), firstRow, includeHeader, includeEmptyRows );
338}
339
340QPair<int, int> QgsLayoutTable::rowRange( QgsRenderContext &context, const int frameIndex ) const
341{
342 //calculate row height
343 if ( frameIndex >= frameCount() )
344 {
345 //bad frame index
346 return qMakePair( 0, 0 );
347 }
348
349 //loop through all previous frames to calculate how many rows are visible in each
350 //as the entire height of a frame may not be utilized for content rows
351 int rowsAlreadyShown = 0;
352 for ( int idx = 0; idx < frameIndex; ++idx )
353 {
354 rowsAlreadyShown += rowsVisible( context, idx, rowsAlreadyShown, false );
355 }
356
357 //using zero based indexes
358 int firstVisible = std::min( rowsAlreadyShown, static_cast<int>( mTableContents.length() ) );
359 int possibleRowsVisible = rowsVisible( context, frameIndex, rowsAlreadyShown, false );
360 int lastVisible = std::min( firstVisible + possibleRowsVisible, static_cast<int>( mTableContents.length() ) );
361
362 return qMakePair( firstVisible, lastVisible );
363}
364
365void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &, const int frameIndex )
366{
367 bool emptyTable = mTableContents.length() == 0;
368 if ( emptyTable && mEmptyTableMode == QgsLayoutTable::HideTable )
369 {
370 //empty table set to hide table mode, so don't draw anything
371 return;
372 }
373
374 if ( !mLayout->renderContext().isPreviewRender() )
375 {
376 //exporting composition, so force an attribute refresh
377 //we do this in case vector layer has changed via an external source (e.g., another database user)
379 }
380
381 const bool prevTextFormatScaleFlag = context.renderContext().testFlag( Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering );
383
384 //calculate which rows to show in this frame
385 QPair< int, int > rowsToShow = rowRange( context.renderContext(), frameIndex );
386
387 double gridSizeX = mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0;
388 double gridSizeY = mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0;
389 double cellHeaderHeight = mMaxRowHeightMap[0] + 2 * mCellMargin;
390 double cellBodyHeightForEmptyRows = QgsTextRenderer::fontMetrics( context.renderContext(), mContentTextFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE ).ascent() / context.renderContext().convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ) / QgsTextRenderer::FONT_WORKAROUND_SCALE + 2 * mCellMargin;
391 QRectF cell;
392
393 //calculate whether a header is required
394 bool drawHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && frameIndex < 1 )
396 //calculate whether drawing table contents is required
397 bool drawContents = !( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage );
398
399 int numberRowsToDraw = rowsToShow.second - rowsToShow.first;
400 int numberEmptyRows = 0;
401 if ( drawContents && mShowEmptyRows )
402 {
403 numberRowsToDraw = rowsVisible( context.renderContext(), frameIndex, rowsToShow.first, true );
404 numberEmptyRows = numberRowsToDraw - rowsToShow.second + rowsToShow.first;
405 }
406 bool mergeCells = false;
407 if ( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage )
408 {
409 //draw a merged row for the empty table message
410 numberRowsToDraw++;
411 rowsToShow.second++;
412 mergeCells = true;
413 }
414
415 QPainter *p = context.renderContext().painter();
416 QgsScopedQPainterState painterState( p );
417 // painter is scaled to dots, so scale back to layout units
418 p->scale( context.renderContext().scaleFactor(), context.renderContext().scaleFactor() );
419
420 //draw the text
421 p->setPen( Qt::SolidLine );
422
423 double currentX = gridSizeX;
424 double currentY = gridSizeY;
425 if ( drawHeader )
426 {
427 //draw the headers
428 int col = 0;
429 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
430 {
431 std::unique_ptr< QgsExpressionContextScope > headerCellScope = std::make_unique< QgsExpressionContextScope >();
432 headerCellScope->setVariable( QStringLiteral( "column_number" ), col + 1, true );
433 QgsExpressionContextScopePopper popper( context.renderContext().expressionContext(), headerCellScope.release() );
434
435 const QgsTextFormat headerFormat = textFormatForHeader( col );
436 //draw background
437 p->save();
438 p->setPen( Qt::NoPen );
439 p->setBrush( backgroundColor( -1, col ) );
440 p->drawRect( QRectF( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, cellHeaderHeight ) );
441 p->restore();
442
443 currentX += mCellMargin;
444
445 cell = QRectF( currentX, currentY, mMaxColumnWidthMap[col], cellHeaderHeight );
446
447 //calculate alignment of header
448 Qgis::TextHorizontalAlignment headerAlign = Qgis::TextHorizontalAlignment::Left;
449 switch ( mHeaderHAlignment )
450 {
451 case FollowColumn:
452 headerAlign = QgsTextRenderer::convertQtHAlignment( column.hAlignment() );
453 break;
454 case HeaderLeft:
455 headerAlign = Qgis::TextHorizontalAlignment::Left;
456 break;
457 case HeaderCenter:
458 headerAlign = Qgis::TextHorizontalAlignment::Center;
459 break;
460 case HeaderRight:
461 headerAlign = Qgis::TextHorizontalAlignment::Right;
462 break;
463 }
464
465 const QRectF textCell = QRectF( currentX, currentY + mCellMargin, mMaxColumnWidthMap[col], cellHeaderHeight - 2 * mCellMargin );
466
467 const QStringList str = column.heading().split( '\n' );
468
469 // scale to dots
470 {
472 QgsTextRenderer::drawText( QRectF( textCell.left() * context.renderContext().scaleFactor(),
473 textCell.top() * context.renderContext().scaleFactor(),
474 textCell.width() * context.renderContext().scaleFactor(),
475 textCell.height() * context.renderContext().scaleFactor() ), 0,
476 headerAlign, str, context.renderContext(), headerFormat, true, Qgis::TextVerticalAlignment::VerticalCenter,
477 mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags()
478 );
479 }
480
481 currentX += mMaxColumnWidthMap[ col ];
482 currentX += mCellMargin;
483 currentX += gridSizeX;
484 col++;
485 }
486
487 currentY += cellHeaderHeight;
488 currentY += gridSizeY;
489 }
490
491 //now draw the body cells
492 int rowsDrawn = 0;
493 if ( drawContents )
494 {
495 //draw the attribute values
496 for ( int row = rowsToShow.first; row < rowsToShow.second; ++row )
497 {
498 rowsDrawn++;
499 currentX = gridSizeX;
500 int col = 0;
501
502 //calculate row height
503 double rowHeight = mMaxRowHeightMap[row + 1] + 2 * mCellMargin;
504
505 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
506 {
507 ( void )column;
508 const QRectF fullCell( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, rowHeight );
509 //draw background
510 p->save();
511 p->setPen( Qt::NoPen );
512 p->setBrush( backgroundColor( row, col ) );
513 p->drawRect( fullCell );
514 p->restore();
515
516 // currentY = gridSize;
517 currentX += mCellMargin;
518
519 QVariant cellContents = mTableContents.at( row ).at( col );
520 const QString localizedString { QgsExpressionUtils::toLocalizedString( cellContents ) };
521 const QStringList str = localizedString.split( '\n' );
522
523 QgsTextFormat cellFormat = textFormatForCell( row, col );
525 cellFormat.updateDataDefinedProperties( context.renderContext() );
526
527 p->save();
528 p->setClipRect( fullCell );
529 const QRectF textCell = QRectF( currentX, currentY + mCellMargin, mMaxColumnWidthMap[col], rowHeight - 2 * mCellMargin );
530
531 const QgsConditionalStyle style = conditionalCellStyle( row, col );
532 QColor foreColor = cellFormat.color();
533 if ( style.textColor().isValid() )
534 foreColor = style.textColor();
535
536 cellFormat.setColor( foreColor );
537
538 // scale to dots
539 {
541 QgsTextRenderer::drawText( QRectF( textCell.left() * context.renderContext().scaleFactor(),
542 textCell.top() * context.renderContext().scaleFactor(),
543 textCell.width() * context.renderContext().scaleFactor(),
544 textCell.height() * context.renderContext().scaleFactor() ), 0,
545 QgsTextRenderer::convertQtHAlignment( horizontalAlignmentForCell( row, col ) ), str, context.renderContext(), cellFormat, true,
547 mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags() );
548 }
549 p->restore();
550
551 currentX += mMaxColumnWidthMap[ col ];
552 currentX += mCellMargin;
553 currentX += gridSizeX;
554 col++;
555 }
556 currentY += rowHeight;
557 currentY += gridSizeY;
558 }
559 }
560
561 if ( numberRowsToDraw > rowsDrawn )
562 {
563 p->save();
564 p->setPen( Qt::NoPen );
565
566 //draw background of empty rows
567 for ( int row = rowsDrawn; row < numberRowsToDraw; ++row )
568 {
569 currentX = gridSizeX;
570 int col = 0;
571
572 if ( mergeCells )
573 {
574 p->setBrush( backgroundColor( row + 10000, 0 ) );
575 p->drawRect( QRectF( gridSizeX, currentY, mTableSize.width() - 2 * gridSizeX, cellBodyHeightForEmptyRows ) );
576 }
577 else
578 {
579 for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
580 {
581 Q_UNUSED( column )
582
583 //draw background
584
585 //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
586 p->setBrush( backgroundColor( row + 10000, col ) );
587 p->drawRect( QRectF( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, cellBodyHeightForEmptyRows ) );
588
589 // currentY = gridSize;
590 currentX += mMaxColumnWidthMap[ col ] + 2 * mCellMargin;
591 currentX += gridSizeX;
592 col++;
593 }
594 }
595 currentY += cellBodyHeightForEmptyRows + gridSizeY;
596 }
597 p->restore();
598 }
599
600 //and the borders
601 if ( mShowGrid )
602 {
603 QPen gridPen;
604 gridPen.setWidthF( mGridStrokeWidth );
605 gridPen.setColor( mGridColor );
606 gridPen.setJoinStyle( Qt::MiterJoin );
607 p->setPen( gridPen );
608 if ( mHorizontalGrid )
609 {
610 drawHorizontalGridLines( context, rowsToShow.first, rowsToShow.second + numberEmptyRows, drawHeader );
611 }
612 if ( mVerticalGrid )
613 {
614 drawVerticalGridLines( context, mMaxColumnWidthMap, rowsToShow.first, rowsToShow.second + numberEmptyRows, drawHeader, mergeCells );
615 }
616 }
617
618 //special case - no records and table is set to ShowMessage mode
619 if ( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage )
620 {
621 double messageX = gridSizeX + mCellMargin;
622 double messageY = gridSizeY + ( drawHeader ? cellHeaderHeight + gridSizeY : 0 );
623 cell = QRectF( messageX, messageY, mTableSize.width() - messageX, cellBodyHeightForEmptyRows );
624
625 // scale to dots
626 {
628 QgsTextRenderer::drawText( QRectF( cell.left() * context.renderContext().scaleFactor(),
629 cell.top() * context.renderContext().scaleFactor(),
630 cell.width() * context.renderContext().scaleFactor(),
631 cell.height() * context.renderContext().scaleFactor() ), 0,
632 Qgis::TextHorizontalAlignment::Center, QStringList() << mEmptyTableMessage, context.renderContext(), mContentTextFormat, true, Qgis::TextVerticalAlignment::VerticalCenter );
633 }
634 }
635
637}
638
639void QgsLayoutTable::setCellMargin( const double margin )
640{
641 if ( qgsDoubleNear( margin, mCellMargin ) )
642 {
643 return;
644 }
645
646 mCellMargin = margin;
647
648 //since spacing has changed, we need to recalculate the table size
650
651 emit changed();
652}
653
655{
656 if ( mode == mEmptyTableMode )
657 {
658 return;
659 }
660
661 mEmptyTableMode = mode;
662
663 //since appearance has changed, we need to recalculate the table size
665
666 emit changed();
667}
668
669void QgsLayoutTable::setEmptyTableMessage( const QString &message )
670{
671 if ( message == mEmptyTableMessage )
672 {
673 return;
674 }
675
676 mEmptyTableMessage = message;
677
678 //since message has changed, we need to recalculate the table size
680
681 emit changed();
682}
683
684void QgsLayoutTable::setShowEmptyRows( const bool showEmpty )
685{
686 if ( showEmpty == mShowEmptyRows )
687 {
688 return;
689 }
690
691 mShowEmptyRows = showEmpty;
692 update();
693 emit changed();
694}
695
696void QgsLayoutTable::setHeaderFont( const QFont &font )
697{
699 if ( font.pointSizeF() > 0 )
700 {
701 mHeaderTextFormat.setSize( font.pointSizeF() );
702 mHeaderTextFormat.setSizeUnit( Qgis::RenderUnit::Points );
703 }
704 else if ( font.pixelSize() > 0 )
705 {
706 mHeaderTextFormat.setSize( font.pixelSize() );
707 mHeaderTextFormat.setSizeUnit( Qgis::RenderUnit::Pixels );
708 }
709
710 //since font attributes have changed, we need to recalculate the table size
712
713 emit changed();
714}
715
717{
718 return mHeaderTextFormat.toQFont();
719}
720
721void QgsLayoutTable::setHeaderFontColor( const QColor &color )
722{
723 if ( color == mHeaderTextFormat.color() )
724 {
725 return;
726 }
727
729 update();
730
731 emit changed();
732}
733
735{
736 return mHeaderTextFormat.color();
737}
738
740{
741 mHeaderTextFormat = format;
742
743 //since font attributes have changed, we need to recalculate the table size
745
746 emit changed();
747}
748
750{
751 return mHeaderTextFormat;
752}
753
755{
756 if ( alignment == mHeaderHAlignment )
757 {
758 return;
759 }
760
761 mHeaderHAlignment = alignment;
762 update();
763
764 emit changed();
765}
766
768{
769 if ( mode == mHeaderMode )
770 {
771 return;
772 }
773
774 mHeaderMode = mode;
776
777 emit changed();
778}
779
780void QgsLayoutTable::setContentFont( const QFont &font )
781{
783 if ( font.pointSizeF() > 0 )
784 {
785 mContentTextFormat.setSize( font.pointSizeF() );
786 mContentTextFormat.setSizeUnit( Qgis::RenderUnit::Points );
787 }
788 else if ( font.pixelSize() > 0 )
789 {
790 mContentTextFormat.setSize( font.pixelSize() );
791 mContentTextFormat.setSizeUnit( Qgis::RenderUnit::Pixels );
792 }
793
794 //since font attributes have changed, we need to recalculate the table size
796
797 emit changed();
798}
799
801{
803}
804
805void QgsLayoutTable::setContentFontColor( const QColor &color )
806{
807 if ( color == mContentTextFormat.color() )
808 {
809 return;
810 }
811
813 update();
814
815 emit changed();
816}
817
819{
820 return mContentTextFormat.color();
821}
822
824{
825 mContentTextFormat = format;
826
827 //since spacing has changed, we need to recalculate the table size
829
830 emit changed();
831}
832
834{
835 return mContentTextFormat;
836}
837
838void QgsLayoutTable::setShowGrid( const bool showGrid )
839{
840 if ( showGrid == mShowGrid )
841 {
842 return;
843 }
844
846 //since grid spacing has changed, we need to recalculate the table size
848
849 emit changed();
850}
851
852void QgsLayoutTable::setGridStrokeWidth( const double width )
853{
854 if ( qgsDoubleNear( width, mGridStrokeWidth ) )
855 {
856 return;
857 }
858
859 mGridStrokeWidth = width;
860 //since grid spacing has changed, we need to recalculate the table size
862
863 emit changed();
864}
865
866void QgsLayoutTable::setGridColor( const QColor &color )
867{
868 if ( color == mGridColor )
869 {
870 return;
871 }
872
873 mGridColor = color;
874 update();
875
876 emit changed();
877}
878
879void QgsLayoutTable::setHorizontalGrid( const bool horizontalGrid )
880{
882 {
883 return;
884 }
885
887 //since grid spacing has changed, we need to recalculate the table size
889
890 emit changed();
891}
892
893void QgsLayoutTable::setVerticalGrid( const bool verticalGrid )
894{
896 {
897 return;
898 }
899
901 //since grid spacing has changed, we need to recalculate the table size
903
904 emit changed();
905}
906
907void QgsLayoutTable::setBackgroundColor( const QColor &color )
908{
909 if ( color == mBackgroundColor )
910 {
911 return;
912 }
913
914 mBackgroundColor = color;
915 update();
916
917 emit changed();
918}
919
921{
922 if ( behavior == mWrapBehavior )
923 {
924 return;
925 }
926
927 mWrapBehavior = behavior;
929
930 emit changed();
931}
932
934{
935 //remove existing columns
937
938 // backward compatibility
939 // test if sorting is provided with the columns and call setSortColumns in such case
940 QgsLayoutTableSortColumns newSortColumns;
942 std::copy_if( mColumns.begin(), mColumns.end(), std::back_inserter( newSortColumns ), []( const QgsLayoutTableColumn & col ) {return col.sortByRank() > 0;} );
943 if ( !newSortColumns.isEmpty() )
944 {
945 std::sort( newSortColumns.begin(), newSortColumns.end(), []( const QgsLayoutTableColumn & a, const QgsLayoutTableColumn & b ) {return a.sortByRank() < b.sortByRank();} );
946 setSortColumns( newSortColumns );
947 }
949}
950
952{
954}
955
957{
958 if ( mCellStyles.contains( group ) )
959 delete mCellStyles.take( group );
960
961 mCellStyles.insert( group, new QgsLayoutTableStyle( style ) );
962}
963
965{
966 if ( !mCellStyles.contains( group ) )
967 return nullptr;
968
969 return mCellStyles.value( group );
970}
971
972QMap<int, QString> QgsLayoutTable::headerLabels() const
973{
974 QMap<int, QString> headers;
975
976 int i = 0;
977 for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
978 {
979 headers.insert( i, col.heading() );
980 i++;
981 }
982 return headers;
983}
984
986{
987 std::unique_ptr< QgsExpressionContextScope > cellScope = std::make_unique< QgsExpressionContextScope >();
988 cellScope->setVariable( QStringLiteral( "row_number" ), row + 1, true );
989 cellScope->setVariable( QStringLiteral( "column_number" ), column + 1, true );
990 return cellScope.release();
991}
992
994{
995 return QgsConditionalStyle();
996}
997
998QSizeF QgsLayoutTable::fixedFrameSize( const int frameIndex ) const
999{
1000 Q_UNUSED( frameIndex )
1001 return QSizeF( mTableSize.width(), 0 );
1002}
1003
1004QSizeF QgsLayoutTable::minFrameSize( const int frameIndex ) const
1005{
1008
1009 double height = 0;
1012 {
1013 //header required, force frame to be high enough for header
1014 for ( int col = 0; col < mColumns.size(); ++ col )
1015 {
1016 height = std::max( height, 2 * ( mShowGrid ? mGridStrokeWidth : 0 ) + 2 * mCellMargin + QgsTextRenderer::fontMetrics( context, textFormatForHeader( col ), QgsTextRenderer::FONT_WORKAROUND_SCALE ).ascent() / QgsTextRenderer::FONT_WORKAROUND_SCALE / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ) );
1017 }
1018 }
1019 return QSizeF( 0, height );
1020}
1021
1023{
1024 mMaxColumnWidthMap.clear();
1025 mMaxRowHeightMap.clear();
1026 mTableContents.clear();
1027
1028 //get new contents
1030 {
1031 return;
1032 }
1033}
1034
1036{
1037 mTableSize = QSizeF( totalWidth(), totalHeight() );
1039}
1040
1041void QgsLayoutTable::initStyles()
1042{
1045 mCellStyles.insert( OddRows, new QgsLayoutTableStyle() );
1046 mCellStyles.insert( EvenRows, new QgsLayoutTableStyle() );
1049 mCellStyles.insert( HeaderRow, new QgsLayoutTableStyle() );
1050 mCellStyles.insert( FirstRow, new QgsLayoutTableStyle() );
1051 mCellStyles.insert( LastRow, new QgsLayoutTableStyle() );
1052
1053 mCellStyleNames.insert( OddColumns, QStringLiteral( "oddColumns" ) );
1054 mCellStyleNames.insert( EvenColumns, QStringLiteral( "evenColumns" ) );
1055 mCellStyleNames.insert( OddRows, QStringLiteral( "oddRows" ) );
1056 mCellStyleNames.insert( EvenRows, QStringLiteral( "evenRows" ) );
1057 mCellStyleNames.insert( FirstColumn, QStringLiteral( "firstColumn" ) );
1058 mCellStyleNames.insert( LastColumn, QStringLiteral( "lastColumn" ) );
1059 mCellStyleNames.insert( HeaderRow, QStringLiteral( "headerRow" ) );
1060 mCellStyleNames.insert( FirstRow, QStringLiteral( "firstRow" ) );
1061 mCellStyleNames.insert( LastRow, QStringLiteral( "lastRow" ) );
1062}
1063
1065{
1066 mMaxColumnWidthMap.clear();
1067
1068 //total number of cells (rows + 1 for header)
1069 int cols = mColumns.count();
1070 int cells = cols * ( mTableContents.count() + 1 );
1071 QVector< double > widths( cells );
1072
1073 double currentCellTextWidth;
1074
1077
1078 //first, go through all the column headers and calculate the sizes
1079 int i = 0;
1080 for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1081 {
1082 if ( col.width() > 0 )
1083 {
1084 //column has manually specified width
1085 widths[i] = col.width();
1086 }
1088 {
1089 std::unique_ptr< QgsExpressionContextScope > headerCellScope = std::make_unique< QgsExpressionContextScope >();
1090 headerCellScope->setVariable( QStringLiteral( "column_number" ), i + 1, true );
1091 QgsExpressionContextScopePopper popper( context.expressionContext(), headerCellScope.release() );
1092
1093 //column width set to automatic, so check content size
1094 const QStringList multiLineSplit = col.heading().split( '\n' );
1095 currentCellTextWidth = QgsTextRenderer::textWidth( context, textFormatForHeader( i ), multiLineSplit ) / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
1096 widths[i] = currentCellTextWidth;
1097 }
1098 else
1099 {
1100 widths[i] = 0.0;
1101 }
1102 i++;
1103 }
1104
1105 //next, go through all the table contents and calculate the sizes
1106 QgsLayoutTableContents::const_iterator rowIt = mTableContents.constBegin();
1107 int row = 1;
1108 for ( ; rowIt != mTableContents.constEnd(); ++rowIt )
1109 {
1110 QgsLayoutTableRow::const_iterator colIt = rowIt->constBegin();
1111 int col = 0;
1112 for ( ; colIt != rowIt->constEnd(); ++colIt )
1113 {
1114 if ( mColumns.at( col ).width() <= 0 )
1115 {
1116 //column width set to automatic, so check content size
1117 const QStringList multiLineSplit = QgsExpressionUtils::toLocalizedString( *colIt ).split( '\n' );
1118
1119 QgsTextFormat cellFormat = textFormatForCell( row - 1, col );
1120 QgsExpressionContextScopePopper popper( context.expressionContext(), scopeForCell( row - 1, col ) );
1121 cellFormat.updateDataDefinedProperties( context );
1122
1123 currentCellTextWidth = QgsTextRenderer::textWidth( context, cellFormat, multiLineSplit ) / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
1124 widths[ row * cols + col ] = currentCellTextWidth;
1125 }
1126 else
1127 {
1128 widths[ row * cols + col ] = 0;
1129 }
1130
1131 col++;
1132 }
1133 row++;
1134 }
1135
1136 //calculate maximum
1137 for ( int col = 0; col < cols; ++col )
1138 {
1139 double maxColWidth = 0;
1140 for ( int row = 0; row < mTableContents.count() + 1; ++row )
1141 {
1142 maxColWidth = std::max( widths[ row * cols + col ], maxColWidth );
1143 }
1144 mMaxColumnWidthMap.insert( col, maxColWidth );
1145 }
1146
1147 return true;
1148}
1149
1151{
1152 mMaxRowHeightMap.clear();
1153
1154 //total number of cells (rows + 1 for header)
1155 int cols = mColumns.count();
1156 int cells = cols * ( mTableContents.count() + 1 );
1157 QVector< double > heights( cells );
1158
1161
1162 //first, go through all the column headers and calculate the sizes
1163 int i = 0;
1164 for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1165 {
1166 std::unique_ptr< QgsExpressionContextScope > headerCellScope = std::make_unique< QgsExpressionContextScope >();
1167 headerCellScope->setVariable( QStringLiteral( "column_number" ), i + 1, true );
1168 QgsExpressionContextScopePopper popper( context.expressionContext(), headerCellScope.release() );
1169
1170 const QgsTextFormat cellFormat = textFormatForHeader( i );
1171 const double headerDescentMm = QgsTextRenderer::fontMetrics( context, cellFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE ).descent() / QgsTextRenderer::FONT_WORKAROUND_SCALE / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
1172 //height
1174 {
1175 heights[i] = 0;
1176 }
1177 else
1178 {
1179 heights[i] = QgsTextRenderer::textHeight( context,
1180 cellFormat,
1181 QStringList() << col.heading(), Qgis::TextLayoutMode::Rectangle,
1182 nullptr,
1183 mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags(),
1184 context.convertToPainterUnits( mColumns.at( i ).width(), Qgis::RenderUnit::Millimeters )
1185 )
1186 / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters )
1187 - headerDescentMm;
1188 }
1189 i++;
1190 }
1191
1192 //next, go through all the table contents and calculate the sizes
1193 QgsLayoutTableContents::const_iterator rowIt = mTableContents.constBegin();
1194 int row = 1;
1195 for ( ; rowIt != mTableContents.constEnd(); ++rowIt )
1196 {
1197 QgsLayoutTableRow::const_iterator colIt = rowIt->constBegin();
1198 int i = 0;
1199 for ( ; colIt != rowIt->constEnd(); ++colIt )
1200 {
1201 QgsTextFormat cellFormat = textFormatForCell( row - 1, i );
1202 QgsExpressionContextScopePopper popper( context.expressionContext(), scopeForCell( row - 1, i ) );
1203 cellFormat.updateDataDefinedProperties( context );
1204 const double contentDescentMm = QgsTextRenderer::fontMetrics( context, cellFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE ).descent() / QgsTextRenderer::FONT_WORKAROUND_SCALE / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
1205 const QString localizedString { QgsExpressionUtils::toLocalizedString( *colIt ) };
1206
1207 heights[ row * cols + i ] = QgsTextRenderer::textHeight( context,
1208 cellFormat,
1209 QStringList() << localizedString.split( '\n' ),
1210 Qgis::TextLayoutMode::Rectangle,
1211 nullptr,
1212 mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags(),
1213 context.convertToPainterUnits( mColumns.at( i ).width(), Qgis::RenderUnit::Millimeters )
1214 ) / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ) - contentDescentMm;
1215
1216 i++;
1217 }
1218 row++;
1219 }
1220
1221 //calculate maximum
1222 for ( int row = 0; row < mTableContents.count() + 1; ++row )
1223 {
1224 double maxRowHeight = 0;
1225 for ( int col = 0; col < cols; ++col )
1226 {
1227 maxRowHeight = std::max( heights[ row * cols + col ], maxRowHeight );
1228 }
1229 mMaxRowHeightMap.insert( row, maxRowHeight );
1230 }
1231
1232 return true;
1233}
1234
1236{
1237 //check how much space each column needs
1238 if ( !calculateMaxColumnWidths() )
1239 {
1240 return 0;
1241 }
1242
1243 //adapt frame to total width
1244 double totalWidth = 0;
1245 QMap<int, double>::const_iterator maxColWidthIt = mMaxColumnWidthMap.constBegin();
1246 for ( ; maxColWidthIt != mMaxColumnWidthMap.constEnd(); ++maxColWidthIt )
1247 {
1248 totalWidth += maxColWidthIt.value();
1249 }
1250 totalWidth += ( 2 * mMaxColumnWidthMap.size() * mCellMargin );
1251 totalWidth += ( mMaxColumnWidthMap.size() + 1 ) * ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1252
1253 return totalWidth;
1254}
1255
1257{
1258 //check how much space each row needs
1259 if ( !calculateMaxRowHeights() )
1260 {
1261 return 0;
1262 }
1263
1264 double height = 0;
1265
1268
1269 //loop through all existing frames to calculate how many rows are visible in each
1270 //as the entire height of a frame may not be utilized for content rows
1271 int rowsAlreadyShown = 0;
1272 int numberExistingFrames = frameCount();
1273 int rowsVisibleInLastFrame = 0;
1274 double heightOfLastFrame = 0;
1275 for ( int idx = 0; idx < numberExistingFrames; ++idx )
1276 {
1277 bool hasHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && idx == 0 )
1279 heightOfLastFrame = frame( idx )->rect().height();
1280 rowsVisibleInLastFrame = rowsVisible( context, heightOfLastFrame, rowsAlreadyShown, hasHeader, false );
1281 rowsAlreadyShown += rowsVisibleInLastFrame;
1282 height += heightOfLastFrame;
1283 if ( rowsAlreadyShown >= mTableContents.length() )
1284 {
1285 //shown entire contents of table, nothing remaining
1286 return height;
1287 }
1288 }
1289
1290 //calculate how many rows left to show
1291 int remainingRows = mTableContents.length() - rowsAlreadyShown;
1292
1293 if ( remainingRows <= 0 )
1294 {
1295 //no remaining rows
1296 return height;
1297 }
1298
1300 {
1301 QgsLayoutItemPage *page = mLayout->pageCollection()->page( mLayout->pageCollection()->pageCount() - 1 );
1302 if ( page )
1303 heightOfLastFrame = page->sizeWithUnits().height();
1304 }
1305
1306 bool hasHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && numberExistingFrames < 1 )
1308
1309 int numberFramesMissing = 0;
1310 while ( remainingRows > 0 )
1311 {
1312 numberFramesMissing++;
1313
1314 rowsVisibleInLastFrame = rowsVisible( context, heightOfLastFrame, rowsAlreadyShown, hasHeader, false );
1315 if ( rowsVisibleInLastFrame < 1 )
1316 {
1317 //if no rows are visible in the last frame, calculation of missing frames
1318 //is impossible. So just return total height of existing frames
1319 return height;
1320 }
1321
1322 rowsAlreadyShown += rowsVisibleInLastFrame;
1323 remainingRows = mTableContents.length() - rowsAlreadyShown;
1324 }
1325
1326 //rows remain unshown -- how many extra frames would we need to complete the table?
1327 //assume all added frames are same size as final frame
1328 height += heightOfLastFrame * numberFramesMissing;
1329 return height;
1330}
1331
1332void QgsLayoutTable::drawHorizontalGridLines( QgsLayoutItemRenderContext &context, int firstRow, int lastRow, bool drawHeaderLines ) const
1333{
1334 //horizontal lines
1335 if ( lastRow - firstRow < 1 && !drawHeaderLines )
1336 {
1337 return;
1338 }
1339
1340 QPainter *painter = context.renderContext().painter();
1341
1342 double cellBodyHeightForEmptyRows = QgsTextRenderer::fontMetrics( context.renderContext(), mContentTextFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE ).ascent() / QgsTextRenderer::FONT_WORKAROUND_SCALE / context.renderContext().convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
1343 double halfGridStrokeWidth = ( mShowGrid ? mGridStrokeWidth : 0 ) / 2.0;
1344 double currentY = 0;
1345 currentY = halfGridStrokeWidth;
1346 if ( drawHeaderLines )
1347 {
1348 painter->drawLine( QPointF( halfGridStrokeWidth, currentY ), QPointF( mTableSize.width() - halfGridStrokeWidth, currentY ) );
1349 currentY += ( mShowGrid ? mGridStrokeWidth : 0 );
1350 currentY += mMaxRowHeightMap[0] + 2 * mCellMargin;
1351 }
1352 for ( int row = firstRow; row < lastRow; ++row )
1353 {
1354 painter->drawLine( QPointF( halfGridStrokeWidth, currentY ), QPointF( mTableSize.width() - halfGridStrokeWidth, currentY ) );
1355 currentY += ( mShowGrid ? mGridStrokeWidth : 0 );
1356 double rowHeight = row < mTableContents.count() ? mMaxRowHeightMap[row + 1] : cellBodyHeightForEmptyRows;
1357 currentY += ( rowHeight + 2 * mCellMargin );
1358 }
1359 painter->drawLine( QPointF( halfGridStrokeWidth, currentY ), QPointF( mTableSize.width() - halfGridStrokeWidth, currentY ) );
1360}
1361
1362QColor QgsLayoutTable::backgroundColor( int row, int column ) const
1363{
1364 QColor color = mBackgroundColor;
1365 if ( QgsLayoutTableStyle *style = mCellStyles.value( OddColumns ) )
1366 if ( style->enabled && column % 2 == 0 )
1367 color = style->cellBackgroundColor;
1368 if ( QgsLayoutTableStyle *style = mCellStyles.value( EvenColumns ) )
1369 if ( style->enabled && column % 2 == 1 )
1370 color = style->cellBackgroundColor;
1371 if ( QgsLayoutTableStyle *style = mCellStyles.value( OddRows ) )
1372 if ( style->enabled && row % 2 == 0 )
1373 color = style->cellBackgroundColor;
1374 if ( QgsLayoutTableStyle *style = mCellStyles.value( EvenRows ) )
1375 if ( style->enabled && row % 2 == 1 )
1376 color = style->cellBackgroundColor;
1377 if ( QgsLayoutTableStyle *style = mCellStyles.value( FirstColumn ) )
1378 if ( style->enabled && column == 0 )
1379 color = style->cellBackgroundColor;
1380 if ( QgsLayoutTableStyle *style = mCellStyles.value( LastColumn ) )
1381 if ( style->enabled && column == mColumns.count() - 1 )
1382 color = style->cellBackgroundColor;
1383 if ( QgsLayoutTableStyle *style = mCellStyles.value( HeaderRow ) )
1384 if ( style->enabled && row == -1 )
1385 color = style->cellBackgroundColor;
1386 if ( QgsLayoutTableStyle *style = mCellStyles.value( FirstRow ) )
1387 if ( style->enabled && row == 0 )
1388 color = style->cellBackgroundColor;
1389 if ( QgsLayoutTableStyle *style = mCellStyles.value( LastRow ) )
1390 if ( style->enabled && row == mTableContents.count() - 1 )
1391 color = style->cellBackgroundColor;
1392
1393 if ( row >= 0 )
1394 {
1395 QgsConditionalStyle conditionalStyle = conditionalCellStyle( row, column );
1396 if ( conditionalStyle.backgroundColor().isValid() )
1397 color = conditionalStyle.backgroundColor();
1398 }
1399
1400 return color;
1401}
1402
1403void QgsLayoutTable::drawVerticalGridLines( QgsLayoutItemRenderContext &context, const QMap<int, double> &maxWidthMap, int firstRow, int lastRow, bool hasHeader, bool mergeCells ) const
1404{
1405 //vertical lines
1406 if ( lastRow - firstRow < 1 && !hasHeader )
1407 {
1408 return;
1409 }
1410
1411 QPainter *painter = context.renderContext().painter();
1412
1413 //calculate height of table within frame
1414 double tableHeight = 0;
1415 if ( hasHeader )
1416 {
1417 tableHeight += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + mCellMargin * 2 + mMaxRowHeightMap[0];
1418 }
1419 tableHeight += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
1420 double headerHeight = tableHeight;
1421
1422 double cellBodyHeightForEmptyRows = QgsTextRenderer::fontMetrics( context.renderContext(), mContentTextFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE ).ascent() / QgsTextRenderer::FONT_WORKAROUND_SCALE / context.renderContext().convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
1423 for ( int row = firstRow; row < lastRow; ++row )
1424 {
1425 double rowHeight = row < mTableContents.count() ? mMaxRowHeightMap[row + 1] : cellBodyHeightForEmptyRows;
1426 tableHeight += rowHeight + ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + mCellMargin * 2;
1427 }
1428
1429 double halfGridStrokeWidth = ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 ) / 2.0;
1430 double currentX = halfGridStrokeWidth;
1431 painter->drawLine( QPointF( currentX, halfGridStrokeWidth ), QPointF( currentX, tableHeight - halfGridStrokeWidth ) );
1432 currentX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1433 QMap<int, double>::const_iterator maxColWidthIt = maxWidthMap.constBegin();
1434 int col = 1;
1435 for ( ; maxColWidthIt != maxWidthMap.constEnd(); ++maxColWidthIt )
1436 {
1437 currentX += ( maxColWidthIt.value() + 2 * mCellMargin );
1438 if ( col == maxWidthMap.size() || !mergeCells )
1439 {
1440 painter->drawLine( QPointF( currentX, halfGridStrokeWidth ), QPointF( currentX, tableHeight - halfGridStrokeWidth ) );
1441 }
1442 else if ( hasHeader )
1443 {
1444 painter->drawLine( QPointF( currentX, halfGridStrokeWidth ), QPointF( currentX, headerHeight - halfGridStrokeWidth ) );
1445 }
1446
1447 currentX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1448 col++;
1449 }
1450}
1451
1453{
1455
1456 //force recalculation of frame rects, so that they are set to the correct
1457 //fixed and minimum frame sizes
1459}
1460
1462{
1463 return ( contents.indexOf( row ) >= 0 );
1464}
1465
1467{
1468 return mContentTextFormat;
1469}
1470
1472{
1473 return mHeaderTextFormat;
1474}
1475
1476Qt::Alignment QgsLayoutTable::horizontalAlignmentForCell( int, int column ) const
1477{
1478 return mColumns.value( column ).hAlignment();
1479}
1480
1481Qt::Alignment QgsLayoutTable::verticalAlignmentForCell( int, int column ) const
1482{
1483 return mColumns.value( column ).vAlignment();
1484}
1485
@ ApplyScalingWorkaroundForTextRendering
Whether a scaling workaround designed to stablise the rendering of small font sizes (or for painters ...
TextHorizontalAlignment
Text horizontal alignment.
Definition: qgis.h:1992
@ WrapLines
Automatically wrap long lines of text.
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.
Definition: qgslayoutitem.h:44
QgsRenderContext & renderContext()
Returns a reference to the context's render context.
Definition: qgslayoutitem.h:71
QgsLayoutSize sizeWithUnits() const
Returns the item's current size, including units.
Abstract base class for layout items with the ability to distribute the content to several frames (Qg...
int frameCount() const
Returns the number of frames associated with this multiframe.
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.
void changed()
Emitted when the object's properties change.
QPointer< QgsLayout > mLayout
double height() const
Returns the height of the size.
Definition: qgslayoutsize.h:90
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.
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.
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.
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Definition: qgslayout.h:50
The class is used as a container of context for various read/write operations on other 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.
static QColor decodeColor(const QString &str)
static QString encodeColor(const QColor &color)
Container for all settings relating to text rendering.
Definition: qgstextformat.h:42
void setColor(const QColor &color)
Sets the color that text will be rendered in.
void setSize(double size)
Sets the size for rendered text.
void setFont(const QFont &font)
Sets the font used for rendering text.
void setSizeUnit(Qgis::RenderUnit unit)
Sets the units for the size of rendered text.
void updateDataDefinedProperties(QgsRenderContext &context)
Updates the format by evaluating current values of data defined properties.
void readXml(const QDomElement &elem, const QgsReadWriteContext &context)
Read settings from a DOM element.
QFont toQFont() const
Returns a QFont matching the relevant settings from this text format.
QDomElement writeXml(QDomDocument &doc, const QgsReadWriteContext &context) const
Write settings into a DOM element.
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 str(x)
Definition: qgis.cpp:38
#define Q_NOWARN_DEPRECATED_POP
Definition: qgis.h:4572
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:4571
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:3988