Fix centered vertical and bottom aligned themed text drawing

When using non-TOP vertical alignment with wxRendererXP::DrawItemText()
either DT_VCENTER or DT_BOTTOM gets passed to DrawThemeTextEx() but
without the DT_SINGLELINE flag which is required for those alignment
flags to take effect, resulting in text always being top aligned.

Fix by passing DT_SINGLELINE when using either alignment, but only for
single lines as multi-lines are rendered as a single line with invisible
newline character. Draw multi-line text using top alignment and deal
with vertical alignment ourselves, using GetThemeTextExtent() to get
an accurate extent of the text.
This commit is contained in:
Dimitri Schoolwerth
2021-02-13 00:51:33 +01:00
parent 1eb7150309
commit b8af267bf9

View File

@@ -1043,7 +1043,10 @@ void wxRendererXP::DrawItemText(wxWindow* win,
const int itemState = GetListItemState(flags);
typedef HRESULT(__stdcall *DrawThemeTextEx_t)(HTHEME, HDC, int, int, const wchar_t *, int, DWORD, RECT *, const WXDTTOPTS *);
typedef HRESULT(__stdcall *GetThemeTextExtent_t)(HTHEME, HDC, int, int, const wchar_t *, int, DWORD, RECT *, RECT *);
static DrawThemeTextEx_t s_DrawThemeTextEx = NULL;
static GetThemeTextExtent_t s_GetThemeTextExtent = NULL;
static bool s_initDone = false;
if ( !s_initDone )
@@ -1052,6 +1055,7 @@ void wxRendererXP::DrawItemText(wxWindow* win,
{
wxLoadedDLL dllUxTheme(wxS("uxtheme.dll"));
wxDL_INIT_FUNC(s_, DrawThemeTextEx, dllUxTheme);
wxDL_INIT_FUNC(s_, GetThemeTextExtent, dllUxTheme);
}
s_initDone = true;
@@ -1082,7 +1086,10 @@ void wxRendererXP::DrawItemText(wxWindow* win,
textOpts.crText = textColour.GetPixel();
}
DWORD textFlags = DT_NOPREFIX;
const DWORD defTextFlags = DT_NOPREFIX;
DWORD textFlags = defTextFlags;
// Always use DT_* flags for horizontal alignment.
if ( align & wxALIGN_CENTER_HORIZONTAL )
textFlags |= DT_CENTER;
else if ( align & wxALIGN_RIGHT )
@@ -1093,12 +1100,101 @@ void wxRendererXP::DrawItemText(wxWindow* win,
else
textFlags |= DT_LEFT;
/*
Bottom (DT_BOTTOM) and centered vertical (DT_VCENTER) alignment
are documented to be only used with the DT_SINGLELINE flag which
doesn't handle multi-lines. In case of drawing multi-lines with
such alignment use DT_TOP (0), which does work for multi-lines,
and deal with the actual desired vertical alignment ourselves with
the help of GetThemeTextExtent().
*/
bool useTopDrawing =
s_GetThemeTextExtent
&& ( align & (wxALIGN_BOTTOM | wxALIGN_CENTRE_VERTICAL) ) != 0
&& text.Contains(wxS('\n'));
if ( useTopDrawing )
{
/*
Get the actual text extent using GetThemeTextExtent() and adjust
drawing rect if needed.
Note that DrawThemeTextEx() in combination with DT_CALCRECT
and DTT_CALCRECT can also be used to get the text extent.
This seems to always result in the exact same extent (checked
with an assert) as using GetThemeTextExtent(), despite having
an additional WXDTTOPTS argument for various effects.
Some effects have been tried (DTT_BORDERSIZE, DTT_SHADOWTYPE
and DTT_SHADOWOFFSET) and while rendered correctly with effects
the returned extent remains the same as without effects.
Official docs don't seem to prefer one method over the other
though a possibly outdated note for DrawThemeText() recommends
using GetThemeTextExtent(). Because Wine as of writing doesn't
support DT_CALCRECT with DrawThemeTextEx() while it does support
GetThemeTextExtent(), opt to use the latter.
*/
/*
It's important for the dwTextFlags parameter passed to
GetThemeTextExtent() not to have some DT_* flags because they
influence the extent size in unwanted ways: Using
DT_SINGLELINE combined with either DT_VCENTER or DT_BOTTOM
results in a height that can't be used (either halved or 0),
and having DT_END_ELLIPSIS ends up always ellipsizing.
Passing a non-NULL rect solves these problems but is not
really a good option as it doesn't make the rectangle extent
a tight fit and calculations would have to be done with large
numbers needlessly (provided the passed rect is set to
something like {0, 0, LONG_MAX, LONG_MAX} ).
*/
RECT rcExtent;
HRESULT hr = s_GetThemeTextExtent(hTheme, dc.GetHDC(),
LVP_LISTITEM, itemState, text.wchar_str(), -1,
defTextFlags, NULL, &rcExtent);
if ( SUCCEEDED(hr) )
{
/*
For height compare with the height of the passed rect and use
the difference for handling vertical alignment. This has
consequences for particularly multi-line text: it will now
always try to fit vertically while a rect received from wxDVC
may have its extent based on calculations for a single line
only and therefore couldn't show more than one line.
As a result of the expanded height clipping may be needed
which at least already happens with wxDVC which (out of
necessity) confines rendering to a cell's bounds.
*/
const int heightDiff = rect.GetHeight() - rcExtent.bottom;
if ( heightDiff )
{
if ( align & wxALIGN_CENTRE_VERTICAL )
{
const int heightOffset = heightDiff / 2;
rc.top += heightOffset;
rc.bottom -= heightOffset;
}
else if ( align & wxALIGN_BOTTOM )
rc.top += heightDiff;
else // top aligned
rc.bottom -= heightDiff;
}
}
else
{
useTopDrawing = false;
}
}
if ( !useTopDrawing )
{
if ( align & wxALIGN_BOTTOM )
textFlags |= DT_BOTTOM;
textFlags |= DT_BOTTOM | DT_SINGLELINE;
else if ( align & wxALIGN_CENTER_VERTICAL )
textFlags |= DT_VCENTER;
textFlags |= DT_VCENTER | DT_SINGLELINE;
else
textFlags |= DT_TOP;
}
const wxString* drawText = &text;
wxString ellipsizedText;