Merge branch 'grid-autosize'

Optimize wxGrid::AutoSizeColumns() for big grids.

This includes an optimization of wxDC::GetTextExtent() at the price of
slightly reduced precision in wxMSW.

See https://github.com/wxWidgets/wxWidgets/pull/1893
This commit is contained in:
Vadim Zeitlin
2020-06-16 20:37:57 +02:00
9 changed files with 305 additions and 114 deletions

View File

@@ -212,6 +212,20 @@ public:
return GetBestSize(grid, attr, dc, row, col).GetWidth();
}
// Unlike GetBestSize(), this functions is optional: it is used when
// auto-sizing columns to determine the best width without iterating over
// all cells in this column, if possible.
//
// If it isn't, return wxDefaultSize as the base class version does by
// default.
virtual wxSize GetMaxBestSize(wxGrid& WXUNUSED(grid),
wxGridCellAttr& WXUNUSED(attr),
wxDC& WXUNUSED(dc))
{
return wxDefaultSize;
}
// create a new object which is the copy of this one
virtual wxGridCellRenderer *Clone() const = 0;
};
@@ -1065,6 +1079,16 @@ public:
return wxGridCellAttrPtr(GetAttr(row, col, kind));
}
// This is an optimization for a common case when the entire column uses
// roughly the same attribute, which can thus be reused for measuring all
// the cells in this column. Override this to return true (possibly for
// some columns only) to speed up AutoSizeColumns() for the grids using
// this table.
virtual bool CanMeasureColUsingSameAttr(int WXUNUSED(col)) const
{
return false;
}
// these functions take ownership of the pointer
virtual void SetAttr(wxGridCellAttr* attr, int row, int col);
virtual void SetRowAttr(wxGridCellAttr *attr, int row);
@@ -1869,11 +1893,8 @@ public:
{ AutoSizeColOrRow(row, setAsMin, wxGRID_ROW); }
// auto size all columns (very ineffective for big grids!)
void AutoSizeColumns( bool setAsMin = true )
{ (void)SetOrCalcColumnSizes(false, setAsMin); }
void AutoSizeRows( bool setAsMin = true )
{ (void)SetOrCalcRowSizes(false, setAsMin); }
void AutoSizeColumns( bool setAsMin = true );
void AutoSizeRows( bool setAsMin = true );
// auto size the grid, that is make the columns/rows of the "right" size
// and also set the grid size to just fit its contents
@@ -2414,10 +2435,6 @@ protected:
wxColour m_gridFrozenBorderColour;
int m_gridFrozenBorderPenWidth;
// common part of AutoSizeColumn/Row() and GetBestSize()
int SetOrCalcColumnSizes(bool calcOnly, bool setAsMin = true);
int SetOrCalcRowSizes(bool calcOnly, bool setAsMin = true);
// common part of AutoSizeColumn/Row()
void AutoSizeColOrRow(int n, bool setAsMin, wxGridDirection direction);

View File

@@ -57,6 +57,13 @@ protected:
class WXDLLIMPEXP_ADV wxGridCellNumberRenderer : public wxGridCellStringRenderer
{
public:
explicit wxGridCellNumberRenderer(long minValue = LONG_MIN,
long maxValue = LONG_MAX)
: m_minValue(minValue),
m_maxValue(maxValue)
{
}
// draw the string right aligned
virtual void Draw(wxGrid& grid,
wxGridCellAttr& attr,
@@ -70,11 +77,21 @@ public:
wxDC& dc,
int row, int col) wxOVERRIDE;
virtual wxSize GetMaxBestSize(wxGrid& grid,
wxGridCellAttr& attr,
wxDC& dc) wxOVERRIDE;
// Optional parameters for this renderer are "<min>,<max>".
virtual void SetParameters(const wxString& params) wxOVERRIDE;
virtual wxGridCellRenderer *Clone() const wxOVERRIDE
{ return new wxGridCellNumberRenderer; }
{ return new wxGridCellNumberRenderer(m_minValue, m_maxValue); }
protected:
wxString GetString(const wxGrid& grid, int row, int col);
long m_minValue,
m_maxValue;
};
class WXDLLIMPEXP_ADV wxGridCellFloatRenderer : public wxGridCellStringRenderer
@@ -141,6 +158,10 @@ public:
wxDC& dc,
int row, int col) wxOVERRIDE;
virtual wxSize GetMaxBestSize(wxGrid& grid,
wxGridCellAttr& attr,
wxDC& dc) wxOVERRIDE;
virtual wxGridCellRenderer *Clone() const wxOVERRIDE
{ return new wxGridCellBoolRenderer; }
};
@@ -175,6 +196,10 @@ public:
wxDC& dc,
int row, int col) wxOVERRIDE;
virtual wxSize GetMaxBestSize(wxGrid& grid,
wxGridCellAttr& attr,
wxDC& dc) wxOVERRIDE;
virtual wxGridCellRenderer *Clone() const wxOVERRIDE;
// output strptime()-like format string
@@ -213,8 +238,37 @@ protected:
#endif // wxUSE_DATETIME
// Renderer for fields taking one of a limited set of values: this is the same
// as the renderer for strings, except that it can implement GetMaxBestSize().
class WXDLLIMPEXP_ADV wxGridCellChoiceRenderer : public wxGridCellStringRenderer
{
public:
wxGridCellChoiceRenderer() { }
virtual wxSize GetMaxBestSize(wxGrid& grid,
wxGridCellAttr& attr,
wxDC& dc) wxOVERRIDE;
// Parameters string is a comma-separated list of values.
virtual void SetParameters(const wxString& params) wxOVERRIDE;
virtual wxGridCellRenderer *Clone() const wxOVERRIDE
{
return new wxGridCellChoiceRenderer(*this);
}
protected:
wxGridCellChoiceRenderer(const wxGridCellChoiceRenderer& other)
: m_choices(other.m_choices)
{
}
wxArrayString m_choices;
};
// renders a number using the corresponding text string
class WXDLLIMPEXP_ADV wxGridCellEnumRenderer : public wxGridCellStringRenderer
class WXDLLIMPEXP_ADV wxGridCellEnumRenderer : public wxGridCellChoiceRenderer
{
public:
wxGridCellEnumRenderer( const wxString& choices = wxEmptyString );
@@ -234,14 +288,8 @@ public:
virtual wxGridCellRenderer *Clone() const wxOVERRIDE;
// parameters string format is "item1[,item2[...,itemN]]" where itemN will
// be used if the cell value is N-1
virtual void SetParameters(const wxString& params) wxOVERRIDE;
protected:
wxString GetString(const wxGrid& grid, int row, int col);
wxArrayString m_choices;
};

View File

@@ -88,6 +88,26 @@ public:
virtual int GetBestWidth(wxGrid& grid, wxGridCellAttr& attr, wxDC& dc,
int row, int col, int height);
/**
Get the maximum possible size for a cell using this renderer, if
possible.
This function may be overridden in the derived class if it can return
the maximum size needed for displaying the cells rendered it without
iterating over all cells. The base class version simply returns
::wxDefaultSize, indicating that this is infeasible and that
GetBestSize() should be called for each cell individually.
Note that this method will only be used if
wxGridTableBase::CanMeasureColUsingSameAttr() is overriden to return
@true.
@since 3.1.4
*/
virtual wxSize GetMaxBestSize(wxGrid& grid,
wxGridCellAttr& attr,
wxDC& dc);
protected:
/**
The destructor is private because only DecRef() can delete us.
@@ -2517,6 +2537,25 @@ public:
returns @true.
*/
virtual bool CanHaveAttributes();
/**
Override to return true if the same attribute can be used for measuring
all cells in the given column.
This function is provided for optimization purposes: it returns @false
by default, but can be overridden to return @true when all the cells in
the same grid column use sensibly the same attribute, i.e. they use the
same renderer (either explicitly, or implicitly, due to their type as
returned by GetTypeName()) and the font of the same size.
Returning @true from this function allows AutoSizeColumns() to skip
looking up the attribute and the renderer for each individual cell,
which results in very noticeable performance improvements for the grids
with many rows.
@since 3.1.4
*/
virtual bool CanMeasureColUsingSameAttr(int col) const;
};

View File

@@ -105,17 +105,27 @@ void wxTextMeasureBase::GetMultiLineTextExtent(const wxString& text,
wxCoord *height,
wxCoord *heightOneLine)
{
// It's noticeably faster to handle the case of a string which isn't
// actually multiline specially here, to skip iteration above in this case.
if ( text.find('\n') == wxString::npos )
{
GetTextExtent(text, width, height);
if ( heightOneLine && height )
*heightOneLine = *height;
return;
}
MeasuringGuard guard(*this);
wxCoord widthTextMax = 0, widthLine,
heightTextTotal = 0, heightLineDefault = 0, heightLine = 0;
wxString curLine;
wxString::const_iterator lineStart = text.begin();
for ( wxString::const_iterator pc = text.begin(); ; ++pc )
{
if ( pc == text.end() || *pc == wxS('\n') )
{
if ( curLine.empty() )
if ( pc == lineStart )
{
// we can't use GetTextExtent - it will return 0 for both width
// and height and an empty line should count in height
@@ -137,7 +147,7 @@ void wxTextMeasureBase::GetMultiLineTextExtent(const wxString& text,
}
else
{
CallGetTextExtent(curLine, &widthLine, &heightLine);
CallGetTextExtent(wxString(lineStart, pc), &widthLine, &heightLine);
if ( widthLine > widthTextMax )
widthTextMax = widthLine;
heightTextTotal += heightLine;
@@ -149,13 +159,10 @@ void wxTextMeasureBase::GetMultiLineTextExtent(const wxString& text,
}
else // '\n'
{
curLine.clear();
lineStart = pc;
++lineStart;
}
}
else
{
curLine += *pc;
}
}
if ( width )

View File

@@ -9967,6 +9967,14 @@ wxGrid::AutoSizeColOrRow(int colOrRow, bool setAsMin, wxGridDirection direction)
col = -1;
}
// If possible, reuse the same attribute and renderer for all cells: this
// is an important optimization (resulting in up to 80% speed up of
// AutoSizeColumns()) as finding the attribute and renderer for the cell
// are very slow operations, due to the number of steps involved in them.
const bool canReuseAttr = column && m_table->CanMeasureColUsingSameAttr(col);
wxGridCellAttrPtr attr;
wxGridCellRendererPtr renderer;
wxCoord extent, extentMax = 0;
int max = column ? m_numRows : m_numCols;
for ( int rowOrCol = 0; rowOrCol < max; rowOrCol++ )
@@ -10005,8 +10013,27 @@ wxGrid::AutoSizeColOrRow(int colOrRow, bool setAsMin, wxGridDirection direction)
}
// get cell ( main cell if CellSpan_Inside ) renderer best size
wxGridCellAttrPtr attr = GetCellAttrPtr(row, col);
wxGridCellRendererPtr renderer = attr->GetRendererPtr(this, row, col);
if ( !canReuseAttr || !attr )
{
attr = GetCellAttrPtr(row, col);
renderer = attr->GetRendererPtr(this, row, col);
if ( canReuseAttr )
{
// Try to get the best width for the entire column at once, if
// it's supported by the renderer.
extent = renderer->GetMaxBestSize(*this, *attr, dc).x;
if ( extent != wxDefaultCoord )
{
extentMax = extent;
// No need to check all the values.
break;
}
}
}
if ( renderer )
{
extent = column
@@ -10206,50 +10233,28 @@ wxCoord wxGrid::CalcColOrRowLabelAreaMinSize(wxGridDirection direction)
return extentMax;
}
int wxGrid::SetOrCalcColumnSizes(bool calcOnly, bool setAsMin)
void wxGrid::AutoSizeColumns(bool setAsMin)
{
int width = m_rowLabelWidth;
wxGridUpdateLocker locker;
if(!calcOnly)
locker.Create(this);
wxGridUpdateLocker locker(this);
for ( int col = 0; col < m_numCols; col++ )
{
if ( !calcOnly )
AutoSizeColumn(col, setAsMin);
width += GetColWidth(col);
}
return width;
AutoSizeColumn(col, setAsMin);
}
int wxGrid::SetOrCalcRowSizes(bool calcOnly, bool setAsMin)
void wxGrid::AutoSizeRows(bool setAsMin)
{
int height = m_colLabelHeight;
wxGridUpdateLocker locker;
if(!calcOnly)
locker.Create(this);
wxGridUpdateLocker locker(this);
for ( int row = 0; row < m_numRows; row++ )
{
if ( !calcOnly )
AutoSizeRow(row, setAsMin);
height += GetRowHeight(row);
}
return height;
AutoSizeRow(row, setAsMin);
}
void wxGrid::AutoSize()
{
wxGridUpdateLocker locker(this);
wxSize size(SetOrCalcColumnSizes(false) - m_rowLabelWidth + m_extraWidth,
SetOrCalcRowSizes(false) - m_colLabelHeight + m_extraHeight);
AutoSizeColumns();
AutoSizeRows();
// we know that we're not going to have scrollbars so disable them now to
// avoid trouble in SetClientSize() which can otherwise set the correct
@@ -10257,7 +10262,7 @@ void wxGrid::AutoSize()
SetScrollbars(m_xScrollPixelsPerLine, m_yScrollPixelsPerLine,
0, 0, 0, 0, true);
SetClientSize(size.x + m_rowLabelWidth, size.y + m_colLabelHeight);
SetSize(DoGetBestSize());
}
void wxGrid::AutoSizeRowLabelSize( int row )
@@ -10294,15 +10299,30 @@ void wxGrid::AutoSizeColLabelSize( int col )
wxSize wxGrid::DoGetBestSize() const
{
wxGrid * const self = const_cast<wxGrid *>(this);
wxSize size(m_rowLabelWidth + m_extraWidth,
m_colLabelHeight + m_extraHeight);
// we do the same as in AutoSize() here with the exception that we don't
// change the column/row sizes, only calculate them
wxSize size(self->SetOrCalcColumnSizes(true) - m_rowLabelWidth + m_extraWidth,
self->SetOrCalcRowSizes(true) - m_colLabelHeight + m_extraHeight);
if ( m_colWidths.empty() )
{
size.x += m_defaultColWidth*m_numCols;
}
else
{
for ( int col = 0; col < m_numCols; col++ )
size.x += GetColWidth(col);
}
return wxSize(size.x + m_rowLabelWidth, size.y + m_colLabelHeight)
+ GetWindowBorderSize();
if ( m_rowHeights.empty() )
{
size.y += m_defaultRowHeight*m_numRows;
}
else
{
for ( int row = 0; row < m_numRows; row++ )
size.y += GetRowHeight(row);
}
return size + GetWindowBorderSize();
}
void wxGrid::Fit()
@@ -10997,7 +11017,7 @@ int wxGridTypeRegistry::FindDataType(const wxString& typeName)
if ( typeName == wxGRID_VALUE_CHOICE )
{
RegisterDataType(wxGRID_VALUE_CHOICE,
new wxGridCellStringRenderer,
new wxGridCellChoiceRenderer,
new wxGridCellChoiceEditor);
}
else

View File

@@ -165,6 +165,24 @@ wxSize wxGridCellDateRenderer::GetBestSize(wxGrid& grid,
return DoGetBestSize(attr, dc, GetString(grid, row, col));
}
wxSize wxGridCellDateRenderer::GetMaxBestSize(wxGrid& WXUNUSED(grid),
wxGridCellAttr& attr,
wxDC& dc)
{
wxSize size;
// Try to produce the longest string in the current format: as we don't
// know which month is the longest, we need to try all of them.
for ( int m = wxDateTime::Jan; m <= wxDateTime::Dec; ++m )
{
const wxDateTime d(28, static_cast<wxDateTime::Month>(m), 9999);
size.IncTo(DoGetBestSize(attr, dc, d.Format(m_oformat, m_tz)));
}
return size;
}
void wxGridCellDateRenderer::SetParameters(const wxString& params)
{
if (!params.empty())
@@ -194,6 +212,38 @@ bool wxGridCellDateTimeRenderer::Parse(const wxString& text, wxDateTime& result)
#endif // wxUSE_DATETIME
// ----------------------------------------------------------------------------
// wxGridCellChoiceRenderer
// ----------------------------------------------------------------------------
wxSize wxGridCellChoiceRenderer::GetMaxBestSize(wxGrid& WXUNUSED(grid),
wxGridCellAttr& attr,
wxDC& dc)
{
wxSize size;
for ( size_t n = 0; n < m_choices.size(); ++n )
{
size.IncTo(DoGetBestSize(attr, dc, m_choices[n]));
}
return size;
}
void wxGridCellChoiceRenderer::SetParameters(const wxString& params)
{
m_choices.clear();
if ( params.empty() )
return;
wxStringTokenizer tk(params, wxT(','));
while ( tk.HasMoreTokens() )
{
m_choices.Add(tk.GetNextToken());
}
}
// ----------------------------------------------------------------------------
// wxGridCellEnumRenderer
// ----------------------------------------------------------------------------
@@ -260,24 +310,6 @@ wxSize wxGridCellEnumRenderer::GetBestSize(wxGrid& grid,
return DoGetBestSize(attr, dc, GetString(grid, row, col));
}
void wxGridCellEnumRenderer::SetParameters(const wxString& params)
{
if ( !params )
{
// what can we do?
return;
}
m_choices.Empty();
wxStringTokenizer tk(params, wxT(','));
while ( tk.HasMoreTokens() )
{
m_choices.Add(tk.GetNextToken());
}
}
// ----------------------------------------------------------------------------
// wxGridCellAutoWrapStringRenderer
// ----------------------------------------------------------------------------
@@ -557,18 +589,8 @@ wxSize wxGridCellStringRenderer::DoGetBestSize(const wxGridCellAttr& attr,
wxDC& dc,
const wxString& text)
{
wxCoord x = 0, y = 0, max_x = 0;
dc.SetFont(attr.GetFont());
wxStringTokenizer tk(text, wxT('\n'));
while ( tk.HasMoreTokens() )
{
dc.GetTextExtent(tk.GetNextToken(), &x, &y);
max_x = wxMax(max_x, x);
}
y *= 1 + text.Freq(wxT('\n')); // multiply by the number of lines.
return wxSize(max_x, y);
return dc.GetMultiLineTextExtent(text);
}
wxSize wxGridCellStringRenderer::GetBestSize(wxGrid& grid,
@@ -726,6 +748,34 @@ wxSize wxGridCellNumberRenderer::GetBestSize(wxGrid& grid,
return DoGetBestSize(attr, dc, GetString(grid, row, col));
}
wxSize wxGridCellNumberRenderer::GetMaxBestSize(wxGrid& WXUNUSED(grid),
wxGridCellAttr& attr,
wxDC& dc)
{
// In theory, it's possible that there is a value in min..max range which
// is longer than both min and max, e.g. we could conceivably have "88" be
// wider than both "87" and "91" with some fonts, but it seems something
// too exotic to worry about in practice.
wxSize size = DoGetBestSize(attr, dc, wxString::Format("%ld", m_minValue));
size.IncTo(DoGetBestSize(attr, dc, wxString::Format("%ld", m_maxValue)));
return size;
}
void wxGridCellNumberRenderer::SetParameters(const wxString& params)
{
if ( params.empty() )
return;
wxString maxStr;
const wxString minStr = params.BeforeFirst(',', &maxStr);
if ( !minStr.ToLong(&m_minValue) || !maxStr.ToLong(&m_maxValue) )
{
wxLogDebug("Invalid wxGridCellNumberRenderer parameters \"%s\"", params);
}
}
// ----------------------------------------------------------------------------
// wxGridCellFloatRenderer
// ----------------------------------------------------------------------------
@@ -922,10 +972,17 @@ void wxGridCellFloatRenderer::SetParameters(const wxString& params)
// ----------------------------------------------------------------------------
wxSize wxGridCellBoolRenderer::GetBestSize(wxGrid& grid,
wxGridCellAttr& WXUNUSED(attr),
wxDC& WXUNUSED(dc),
wxGridCellAttr& attr,
wxDC& dc,
int WXUNUSED(row),
int WXUNUSED(col))
{
return GetMaxBestSize(grid, attr, dc);
}
wxSize wxGridCellBoolRenderer::GetMaxBestSize(wxGrid& grid,
wxGridCellAttr& WXUNUSED(attr),
wxDC& WXUNUSED(dc))
{
static wxPrivate::DpiDependentValue<wxSize> s_sizeCheckMark;

View File

@@ -1451,22 +1451,12 @@ void wxGridCellChoiceEditor::SetSize(const wxRect& rect)
wxASSERT_MSG(m_control,
wxT("The wxGridCellChoiceEditor must be created first!"));
// Check that the height is not too small to fit the combobox.
wxRect rectTallEnough = rect;
const wxSize bestSize = m_control->GetBestSize();
const wxCoord diffY = bestSize.GetHeight() - rectTallEnough.GetHeight();
if ( diffY > 0 )
{
// Do make it tall enough.
rectTallEnough.height += diffY;
// Check that the rectangle is big enough to fit the combobox, we can't
// afford truncating it.
wxSize size = rect.GetSize();
size.IncTo(m_control->GetBestSize());
// Also centre the effective rectangle vertically with respect to the
// original one.
rectTallEnough.y -= diffY/2;
}
//else: The rectangle provided is already tall enough.
wxGridCellEditor::SetSize(rectTallEnough);
wxGridCellEditor::SetSize(wxRect(size).CentreIn(rect));
}
void wxGridCellChoiceEditor::PaintBackground(wxDC& dc,

View File

@@ -108,7 +108,13 @@ void wxTextMeasure::DoGetTextExtent(const wxString& string,
// the result computed by GetTextExtentPoint32() may be too small as it
// accounts for under/overhang of the first/last character while we want
// just the bounding rect for this string so adjust the width as needed
if ( len > 0 )
// when using italic fonts as the difference is really noticeable for them
// (it may still exist, but seems to be at most 1px for the other fonts,
// and calling GetCharABCWidths() is pretty slow and much slower than
// calling GetTextExtentPoint32() itself, so avoid its overhead unless it's
// really, really necessary).
const wxFont font = GetFont();
if ( font.IsOk() && font.GetStyle() != wxFONTSTYLE_NORMAL && len > 0 )
{
ABC widthABC;
const wxChar chFirst = *string.begin();

View File

@@ -66,6 +66,7 @@ struct GraphicsBenchmarkOptions
testCircles =
testEllipses =
testTextExtent =
testMultiLineTextExtent =
testPartialTextExtents = false;
usePaint =
@@ -95,6 +96,7 @@ struct GraphicsBenchmarkOptions
testCircles,
testEllipses,
testTextExtent,
testMultiLineTextExtent,
testPartialTextExtents;
bool usePaint,
@@ -634,7 +636,10 @@ private:
wxStopWatch sw;
for ( long n = 0; n < opts.numIters; n++ )
{
size += dc.GetTextExtent(str);
if ( opts.testMultiLineTextExtent )
size += dc.GetMultiLineTextExtent(str);
else
size += dc.GetTextExtent(str);
}
const long t = sw.Time();
@@ -847,6 +852,7 @@ public:
{ wxCMD_LINE_SWITCH, "", "circles" },
{ wxCMD_LINE_SWITCH, "", "ellipses" },
{ wxCMD_LINE_SWITCH, "", "textextent" },
{ wxCMD_LINE_SWITCH, "", "multilinetextextent" },
{ wxCMD_LINE_SWITCH, "", "partialtextextents" },
{ wxCMD_LINE_SWITCH, "", "paint" },
{ wxCMD_LINE_SWITCH, "", "client" },
@@ -922,6 +928,7 @@ public:
opts.testCircles = parser.Found("circles");
opts.testEllipses = parser.Found("ellipses");
opts.testTextExtent = parser.Found("textextent");
opts.testMultiLineTextExtent = parser.Found("multilinetextextent");
opts.testPartialTextExtents = parser.Found("partialtextextents");
if ( !(opts.testBitmaps || opts.testImages || opts.testLines
|| opts.testRawBitmaps || opts.testRectangles