如何在 DrawTextEx 绘制的文本后面高亮显示
How to highlight behind text drawn by DrawTextEx
对于自定义控件,我主要使用 DrawThemeTextEx (with DrawTextEx via (TextRenderer.DrawText 作为后备)。我正在实施增量搜索,用户在其中输入搜索字符串并突出显示所有匹配项。为了画出文字后面的高光,我需要计算绘制文字的每个匹配子范围的矩形。
我不想在突出显示的边界处理字体字距调整,所以我不想零碎地绘制文本。目的只是获取矩形坐标并绘制样式矩形。
我可以使用什么 GDI 函数来执行此操作?我找遍了。我发现的最接近的 GDI 函数可能是 GetCharacterPlacement,但从文档中我什至无法开始使用它。
我知道这个必须内置到Windows;文本框可以毫无问题地查找和突出显示任意范围的绘制文本。它必须在某处作为 API 公开,对吗?我非常不喜欢 GDI+ 的文本渲染,我不想为了做这样简单的事情而引入 DirectX。
此代码运行良好。我没有添加处理换行的功能,但它可以处理水平和垂直对齐以及色彩鲜艳的椭圆。如果突出显示部分被截断或丢失,则整个省略号都会突出显示。
这个函数在 Unicode 大世界中可能有点天真,所以在某些时候可能值得研究一下 Uniscribe。也许有知识的人可以告诉我 DrawText 在内部使用什么。
public static class TextUtils
{
const TextFormatFlags AlignFlags = TextFormatFlags.HorizontalCenter | TextFormatFlags.Right | TextFormatFlags.VerticalCenter | TextFormatFlags.Bottom;
const TextFormatFlags PaddingFlags = TextFormatFlags.NoPadding | TextFormatFlags.LeftAndRightPadding;
const TextFormatFlags EllipsisFlags = TextFormatFlags.EndEllipsis | TextFormatFlags.WordEllipsis | TextFormatFlags.PathEllipsis;
const TextFormatFlags CalcRectFlag = (TextFormatFlags)0x400;
public static Rectangle GetHighlightRectangle(IDeviceContext dc, string text, int highlightStart, int highlightLength, Font font, Rectangle bounds, TextFormatFlags flags = TextFormatFlags.SingleLine)
{
if ((flags & TextFormatFlags.SingleLine) == 0) throw new InvalidOperationException("This method only handles single line highlights. Multiline text highlights may be composed of multiple rectangles.");
if (string.IsNullOrEmpty(text)) throw new ArgumentException("Text must not be null or empty.", "text");
if (highlightStart < 0) throw new ArgumentOutOfRangeException("highlightStart", highlightLength, "Highlight length must be greater than or equal to zero.");
if (highlightLength <= 0) throw new ArgumentOutOfRangeException("highlightLength", highlightLength, "Highlight length must be greater than zero.");
if (highlightLength > text.Length - highlightStart) throw new ArgumentOutOfRangeException("highlightLength", highlightLength, "Highlight length must be less than or equal to the length of the text minus the highlight start.");
var drawTextParams = GetTextMargins(font, flags);
Size totalSize;
// Determine total size and adjust for cut-off highlight.
// We need access to the modified string in case it was trimmed.
// Unfortunately, TextFormatFlags.ModifyString can't be used with TextRenderer.MeasureText
// so we'll have to roll our own.
var hdc = dc.GetHdc();
try
{
var modifiedText = new StringBuilder(text);
var rectBounds = new RECT(0, 0, bounds.Width, bounds.Height);
var hFont = font.ToHfont();
try
{
var oldFont = Gdi32.SelectObject(hdc, hFont);
try
{
User32.DrawTextEx(hdc, modifiedText, modifiedText.Length, ref rectBounds, (flags & ~AlignFlags) | CalcRectFlag | TextFormatFlags.ModifyString, ref drawTextParams);
}
finally
{
Gdi32.SelectObject(hdc, oldFont);
}
}
finally
{
Gdi32.DeleteObject(hFont);
}
totalSize = new Size(rectBounds.Right - rectBounds.Left, rectBounds.Bottom - rectBounds.Top);
// drawTextParams.uiLengthDrawn may be equal to text.Length even if an ellipsis was applied
for (var i = 0; i < drawTextParams.uiLengthDrawn; i++)
if (modifiedText[i] != text[i])
{
var unchangedLength = i;
if (highlightStart > unchangedLength) highlightStart = unchangedLength;
if (highlightStart + highlightLength > unchangedLength) highlightLength = modifiedText.Length - highlightStart;
text = modifiedText.ToString();
break;
}
}
finally
{
dc.ReleaseHdc();
}
// Find the end of the highlight first rather than the beginning. This avoids having to explicitly deal with kerning.
// Kerning may move the first highlighted character closer to the previous character. As long as the two characters
// are measured together, kerning is taken into consideration.
// The rectangle extends to the full beginning of the highlighted text, even when kerning moves it.
var highlightXEnd = drawTextParams.iLeftMargin + TextRenderer.MeasureText(dc, text.Substring(0, highlightStart + highlightLength), font, bounds.Size, (flags & ~(PaddingFlags | AlignFlags | EllipsisFlags)) | TextFormatFlags.NoPadding).Width;
// We don't care about the kerning of the first character *following* the highlight. This way the highlight is inclusive on both ends.
// The rectangle extends to the full end of the highlighted text, regardless of how close the following character comes.
var highlightSize = TextRenderer.MeasureText(dc, text.Substring(highlightStart, highlightLength), font, bounds.Size, (flags & ~(PaddingFlags | AlignFlags | EllipsisFlags)) | TextFormatFlags.NoPadding);
var unalignedHighlightBounds = new Rectangle(bounds.X + highlightXEnd - highlightSize.Width, bounds.Y, highlightSize.Width, highlightSize.Height);
return ApplyAlignment(unalignedHighlightBounds, bounds.Width - totalSize.Width, bounds.Height - totalSize.Height, flags);
}
// Mimic TextRenderer's margins (see WindowsGraphics.GetTextMargins)
private static User32.DRAWTEXTPARAMS GetTextMargins(Font font, TextFormatFlags flags)
{
var overhangPadding = font.Height / 6f;
const float italicPaddingFactor = 0.5f;
switch (flags & PaddingFlags)
{
case TextFormatFlags.GlyphOverhangPadding:
return new User32.DRAWTEXTPARAMS(Marshal.SizeOf(typeof(User32.DRAWTEXTPARAMS)))
{
iLeftMargin = (int)Math.Ceiling(overhangPadding),
iRightMargin = (int)Math.Ceiling(overhangPadding * (1 + italicPaddingFactor))
};
case TextFormatFlags.LeftAndRightPadding:
return new User32.DRAWTEXTPARAMS(Marshal.SizeOf(typeof(User32.DRAWTEXTPARAMS)))
{
iLeftMargin = (int)Math.Ceiling(overhangPadding * 2),
iRightMargin = (int)Math.Ceiling(overhangPadding * (2 + italicPaddingFactor))
};
case TextFormatFlags.NoPadding:
return new User32.DRAWTEXTPARAMS(Marshal.SizeOf(typeof(User32.DRAWTEXTPARAMS)))
{
iLeftMargin = 0,
iRightMargin = 0
};
default:
throw new ArgumentException("Invalid combination of padding flags.", "flags");
}
}
private static Rectangle ApplyAlignment(Rectangle rect, int widthDifference, int heightDifference, TextFormatFlags flags)
{
switch (flags & AlignFlags)
{
case TextFormatFlags.Top | TextFormatFlags.Left:
break;
case TextFormatFlags.Top | TextFormatFlags.HorizontalCenter:
rect.Offset(widthDifference / 2, 0);
break;
case TextFormatFlags.Top | TextFormatFlags.Right:
rect.Offset(widthDifference, 0);
break;
case TextFormatFlags.VerticalCenter | TextFormatFlags.Left:
rect.Offset(0, heightDifference / 2);
break;
case TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter:
rect.Offset(widthDifference / 2, heightDifference / 2);
break;
case TextFormatFlags.VerticalCenter | TextFormatFlags.Right:
rect.Offset(widthDifference, heightDifference / 2);
break;
case TextFormatFlags.Bottom | TextFormatFlags.Left:
rect.Offset(0, heightDifference);
break;
case TextFormatFlags.Bottom | TextFormatFlags.HorizontalCenter:
rect.Offset(widthDifference / 2, heightDifference);
break;
case TextFormatFlags.Bottom | TextFormatFlags.Right:
rect.Offset(widthDifference, heightDifference);
break;
default:
throw new ArgumentException("Invalid combination of alignment flags.", "flags");
}
return rect;
}
}
对于自定义控件,我主要使用 DrawThemeTextEx (with DrawTextEx via (TextRenderer.DrawText 作为后备)。我正在实施增量搜索,用户在其中输入搜索字符串并突出显示所有匹配项。为了画出文字后面的高光,我需要计算绘制文字的每个匹配子范围的矩形。
我不想在突出显示的边界处理字体字距调整,所以我不想零碎地绘制文本。目的只是获取矩形坐标并绘制样式矩形。
我可以使用什么 GDI 函数来执行此操作?我找遍了。我发现的最接近的 GDI 函数可能是 GetCharacterPlacement,但从文档中我什至无法开始使用它。
我知道这个必须内置到Windows;文本框可以毫无问题地查找和突出显示任意范围的绘制文本。它必须在某处作为 API 公开,对吗?我非常不喜欢 GDI+ 的文本渲染,我不想为了做这样简单的事情而引入 DirectX。
此代码运行良好。我没有添加处理换行的功能,但它可以处理水平和垂直对齐以及色彩鲜艳的椭圆。如果突出显示部分被截断或丢失,则整个省略号都会突出显示。
这个函数在 Unicode 大世界中可能有点天真,所以在某些时候可能值得研究一下 Uniscribe。也许有知识的人可以告诉我 DrawText 在内部使用什么。
public static class TextUtils
{
const TextFormatFlags AlignFlags = TextFormatFlags.HorizontalCenter | TextFormatFlags.Right | TextFormatFlags.VerticalCenter | TextFormatFlags.Bottom;
const TextFormatFlags PaddingFlags = TextFormatFlags.NoPadding | TextFormatFlags.LeftAndRightPadding;
const TextFormatFlags EllipsisFlags = TextFormatFlags.EndEllipsis | TextFormatFlags.WordEllipsis | TextFormatFlags.PathEllipsis;
const TextFormatFlags CalcRectFlag = (TextFormatFlags)0x400;
public static Rectangle GetHighlightRectangle(IDeviceContext dc, string text, int highlightStart, int highlightLength, Font font, Rectangle bounds, TextFormatFlags flags = TextFormatFlags.SingleLine)
{
if ((flags & TextFormatFlags.SingleLine) == 0) throw new InvalidOperationException("This method only handles single line highlights. Multiline text highlights may be composed of multiple rectangles.");
if (string.IsNullOrEmpty(text)) throw new ArgumentException("Text must not be null or empty.", "text");
if (highlightStart < 0) throw new ArgumentOutOfRangeException("highlightStart", highlightLength, "Highlight length must be greater than or equal to zero.");
if (highlightLength <= 0) throw new ArgumentOutOfRangeException("highlightLength", highlightLength, "Highlight length must be greater than zero.");
if (highlightLength > text.Length - highlightStart) throw new ArgumentOutOfRangeException("highlightLength", highlightLength, "Highlight length must be less than or equal to the length of the text minus the highlight start.");
var drawTextParams = GetTextMargins(font, flags);
Size totalSize;
// Determine total size and adjust for cut-off highlight.
// We need access to the modified string in case it was trimmed.
// Unfortunately, TextFormatFlags.ModifyString can't be used with TextRenderer.MeasureText
// so we'll have to roll our own.
var hdc = dc.GetHdc();
try
{
var modifiedText = new StringBuilder(text);
var rectBounds = new RECT(0, 0, bounds.Width, bounds.Height);
var hFont = font.ToHfont();
try
{
var oldFont = Gdi32.SelectObject(hdc, hFont);
try
{
User32.DrawTextEx(hdc, modifiedText, modifiedText.Length, ref rectBounds, (flags & ~AlignFlags) | CalcRectFlag | TextFormatFlags.ModifyString, ref drawTextParams);
}
finally
{
Gdi32.SelectObject(hdc, oldFont);
}
}
finally
{
Gdi32.DeleteObject(hFont);
}
totalSize = new Size(rectBounds.Right - rectBounds.Left, rectBounds.Bottom - rectBounds.Top);
// drawTextParams.uiLengthDrawn may be equal to text.Length even if an ellipsis was applied
for (var i = 0; i < drawTextParams.uiLengthDrawn; i++)
if (modifiedText[i] != text[i])
{
var unchangedLength = i;
if (highlightStart > unchangedLength) highlightStart = unchangedLength;
if (highlightStart + highlightLength > unchangedLength) highlightLength = modifiedText.Length - highlightStart;
text = modifiedText.ToString();
break;
}
}
finally
{
dc.ReleaseHdc();
}
// Find the end of the highlight first rather than the beginning. This avoids having to explicitly deal with kerning.
// Kerning may move the first highlighted character closer to the previous character. As long as the two characters
// are measured together, kerning is taken into consideration.
// The rectangle extends to the full beginning of the highlighted text, even when kerning moves it.
var highlightXEnd = drawTextParams.iLeftMargin + TextRenderer.MeasureText(dc, text.Substring(0, highlightStart + highlightLength), font, bounds.Size, (flags & ~(PaddingFlags | AlignFlags | EllipsisFlags)) | TextFormatFlags.NoPadding).Width;
// We don't care about the kerning of the first character *following* the highlight. This way the highlight is inclusive on both ends.
// The rectangle extends to the full end of the highlighted text, regardless of how close the following character comes.
var highlightSize = TextRenderer.MeasureText(dc, text.Substring(highlightStart, highlightLength), font, bounds.Size, (flags & ~(PaddingFlags | AlignFlags | EllipsisFlags)) | TextFormatFlags.NoPadding);
var unalignedHighlightBounds = new Rectangle(bounds.X + highlightXEnd - highlightSize.Width, bounds.Y, highlightSize.Width, highlightSize.Height);
return ApplyAlignment(unalignedHighlightBounds, bounds.Width - totalSize.Width, bounds.Height - totalSize.Height, flags);
}
// Mimic TextRenderer's margins (see WindowsGraphics.GetTextMargins)
private static User32.DRAWTEXTPARAMS GetTextMargins(Font font, TextFormatFlags flags)
{
var overhangPadding = font.Height / 6f;
const float italicPaddingFactor = 0.5f;
switch (flags & PaddingFlags)
{
case TextFormatFlags.GlyphOverhangPadding:
return new User32.DRAWTEXTPARAMS(Marshal.SizeOf(typeof(User32.DRAWTEXTPARAMS)))
{
iLeftMargin = (int)Math.Ceiling(overhangPadding),
iRightMargin = (int)Math.Ceiling(overhangPadding * (1 + italicPaddingFactor))
};
case TextFormatFlags.LeftAndRightPadding:
return new User32.DRAWTEXTPARAMS(Marshal.SizeOf(typeof(User32.DRAWTEXTPARAMS)))
{
iLeftMargin = (int)Math.Ceiling(overhangPadding * 2),
iRightMargin = (int)Math.Ceiling(overhangPadding * (2 + italicPaddingFactor))
};
case TextFormatFlags.NoPadding:
return new User32.DRAWTEXTPARAMS(Marshal.SizeOf(typeof(User32.DRAWTEXTPARAMS)))
{
iLeftMargin = 0,
iRightMargin = 0
};
default:
throw new ArgumentException("Invalid combination of padding flags.", "flags");
}
}
private static Rectangle ApplyAlignment(Rectangle rect, int widthDifference, int heightDifference, TextFormatFlags flags)
{
switch (flags & AlignFlags)
{
case TextFormatFlags.Top | TextFormatFlags.Left:
break;
case TextFormatFlags.Top | TextFormatFlags.HorizontalCenter:
rect.Offset(widthDifference / 2, 0);
break;
case TextFormatFlags.Top | TextFormatFlags.Right:
rect.Offset(widthDifference, 0);
break;
case TextFormatFlags.VerticalCenter | TextFormatFlags.Left:
rect.Offset(0, heightDifference / 2);
break;
case TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter:
rect.Offset(widthDifference / 2, heightDifference / 2);
break;
case TextFormatFlags.VerticalCenter | TextFormatFlags.Right:
rect.Offset(widthDifference, heightDifference / 2);
break;
case TextFormatFlags.Bottom | TextFormatFlags.Left:
rect.Offset(0, heightDifference);
break;
case TextFormatFlags.Bottom | TextFormatFlags.HorizontalCenter:
rect.Offset(widthDifference / 2, heightDifference);
break;
case TextFormatFlags.Bottom | TextFormatFlags.Right:
rect.Offset(widthDifference, heightDifference);
break;
default:
throw new ArgumentException("Invalid combination of alignment flags.", "flags");
}
return rect;
}
}