ComboBox OwnerDrawVariable字体格式大小问题
ComboBox OwnerDrawVariable Font format size problem
我正在尝试实施类似于 Visual Studio 的 Go To
成员搜索的 auto-complete/search 框:
但是,我对 bold
文本及其间距的格式计算不正确。我将省略它的自动完成功能,只包括通过硬编码搜索词来格式化结果的代码。
e.Graphics.MeasureString
确定的间距似乎 return 不正确。我尝试使用 this question 中的 StringFormat.GenericTypographic
并且我接近了但仍然不正确。
这是我的下拉列表的显示,其中匹配的术语(粗体)很容易表明我的格式位置计算已关闭(f
显然侵占了 i
)。
除此之外,如果我将鼠标悬停在某个项目上,它会重新绘制我的文本 并且没有 粗体。我也想阻止它。
更新:我更改了我的代码以使用 TextRenderer
但现在看起来更糟了。
现在似乎在我连接的每场比赛前后都有额外的 space。
更新了以下代码:
private void Form1_Load( object sender, EventArgs e )
{
var docGenFields = new[] {
new DocGenFieldItem { Display = $"Profile.date-birth.value", Value = "5/9/1973", FieldCode = $"Profile.date-birth.value" },
new DocGenFieldItem { Display = $"Profile.date-birth.text", Value = "Birth Date", FieldCode = $"Profile.date-birth.text" },
new DocGenFieldItem { Display = $"Profile.date-birth.raw-value", Value = "1973-05-09", FieldCode = $"Profile.date-birth.raw-value" },
new DocGenFieldItem { Display = $"Profile.name-first.value", Value = "Terry", FieldCode = $"Profile.name-first.value" },
new DocGenFieldItem { Display = $"Profile.name-first.text", Value = "First Name", FieldCode = $"Profile.name-first.text" },
new DocGenFieldItem { Display = $"Profile.name-first.raw-value", Value = "Terry", FieldCode = $"Profile.name-first.raw-value" },
new DocGenFieldItem { Display = $"Profile.name-first.value", Value = "Minnesota", FieldCode = $"Profile.state.value" },
new DocGenFieldItem { Display = $"Profile.name-first.text", Value = "State", FieldCode = $"Profile.state.text" },
new DocGenFieldItem { Display = $"Profile.name-first.raw-value", Value = "MN", FieldCode = $"Profile.state.raw-value" }
};
comboBoxItems.FormattingEnabled = true;
comboBoxItems.DrawMode = DrawMode.OwnerDrawVariable;
comboBoxItems.DropDownHeight = 44 * 5;
// comboBoxItems.Font = new Font( "Microsoft Sans Serif", 12F, FontStyle.Regular, GraphicsUnit.Point, 0 );
comboBoxItems.Font = new Font( "Segoe UI", 12F, FontStyle.Regular, GraphicsUnit.Point, 0 );
comboBoxItems.Items.AddRange( docGenFields );
comboBoxItems.DrawItem += new DrawItemEventHandler( comboBoxItems_DrawItem );
comboBoxItems.MeasureItem += new MeasureItemEventHandler( comboBoxItems_MeasureItem );
}
private void comboBoxItems_DrawItem( object sender, DrawItemEventArgs e )
{
// Draw the background of the item.
e.DrawBackground();
var listItem = comboBoxItems.Items[ e.Index ] as DocGenFieldItem;
var searchTerm = "P";
var matches = Regex.Split( listItem.Display, "(?i)" + searchTerm );
var bold = new Font( e.Font.FontFamily, e.Font.Size, FontStyle.Bold );
// e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
var currentCharacter = 0;
// float currentX = 0;
var currentX = 0;
var currentMatch = 0;
var keyLength = searchTerm.Length;
foreach ( var m in matches )
{
// If search term characters are first (like StartsWith) or last (like EndsWith) characters
// then the match will be empty. So if not empty, then need to render the characters 'between'
// matches of search term in regular font
if ( !string.IsNullOrEmpty( m ) )
{
// var p = new PointF( e.Bounds.X + currentX, e.Bounds.Y );
// var mWidth = e.Graphics.MeasureString( m, e.Font, p, StringFormat.GenericTypographic );
// e.Graphics.DrawString( m, e.Font, Brushes.Black, p );
var p = new Point( currentX, e.Bounds.Y );
var mWidth = TextRenderer.MeasureText( e.Graphics, m, e.Font );
TextRenderer.DrawText( e.Graphics, m, e.Font, p, System.Drawing.Color.Black );
currentX += mWidth.Width;
currentCharacter += m.Length;
}
currentMatch++;
// Render the search term characters (need to use 'substring' of current text to maintain
// original case of text) *bold* in between matches.
// string.IsNullOrEmpty( m ) && currentMatch == 1 - If the search term matches ENTIRE value
// then currentMatch will = matches.Length (1) but the match of 'm' will be empty.
if ( currentMatch < matches.Length || ( string.IsNullOrEmpty( m ) && currentMatch == 1 ) )
{
var mValue = listItem.Display.Substring( currentCharacter, keyLength );
// var p = new PointF( e.Bounds.X + currentX, e.Bounds.Y );
// var mWidth = e.Graphics.MeasureString( mValue, bold, p, StringFormat.GenericTypographic );
// e.Graphics.DrawString( mValue, bold, Brushes.Black, p, StringFormat.GenericTypographic );
var p = new Point( currentX, e.Bounds.Y );
var mWidth = TextRenderer.MeasureText( e.Graphics, mValue, bold );
TextRenderer.DrawText( e.Graphics, mValue, bold, p, System.Drawing.Color.Black );
currentX += mWidth.Width;
currentCharacter += keyLength;
}
}
// Render a secondary 'info' line in the dropdown
var b = new SolidBrush( ColorTranslator.FromHtml( "#636363" ) );
var valueWidth = e.Graphics.MeasureString( "Value: ", bold );
e.Graphics.DrawString( "Value: ", bold, b,
new RectangleF( e.Bounds.X, e.Bounds.Y + 21, e.Bounds.Width, e.Bounds.Height )
);
e.Graphics.DrawString( listItem.Value, e.Font, b,
new RectangleF( e.Bounds.X + valueWidth.Width, e.Bounds.Y + 21, e.Bounds.Width, 21 )
);
// Draw the focus rectangle if the mouse hovers over an item.
e.DrawFocusRectangle();
}
private void comboBoxItems_MeasureItem( object sender, MeasureItemEventArgs e )
{
e.ItemHeight = 44;
}
当TextRenderer is used to render text in a non-generic Graphics context, this context needs to be considered: for this reason, TextRenderer provides overloads of both MeasureText and DrawText that accept a Graphics context (IDeviceContext)参数时。
Graphics 上下文包含 TextRenderer 可以用来更好地适应 DC 细节的信息。
此外,我们需要将 TextFormatFlags 值的组合传递给方法,这些值定义了我们要如何测量 and/or 呈现文本。
- 始终声明对齐类型
- 指定 clipping/wrapping 行为(例如,我们希望文本换行或者我们真的不希望它换行,我们希望它被剪裁)
- 如果不应填充文本,请指定
TextFormatFlags.NoPadding
,否则文本将被 拉伸 以填充绘图边界。
- 如果未手动安排绘图边界(在特定位置绘制文本),请指定
TextFormatFlags.LeftAndRightPadding
以向文本添加预定义的填充。此设置应用的填充(基于字体字距调整)匹配文本与标准控件(例如,ListBox 或 ListView)边框之间的距离
有关 TextFormatFlags
的更多信息(部分 :) 可在文档中找到。
我已将所有绘图部分移动到一个方法中,RenderText()
。
所有的测量和绘图都在这里进行:这样在绘制项目时应该更容易理解是怎么回事。
DrawItem
处理程序中的代码调用此方法,在满足特定条件时传递一些适当的值(如更改 FontStyle
,部分文本的替代 ForeColor
等)
导致:
▶ 这里使用的字体是Microsoft YaHei UI, 12pt
。当然你可以使用任何其他字体,但是具有 UI
appendix 的系统字体系列是为此设计的(很好)。
▶ 请记住处理您创建的图形对象,这非常重要,当这些对象用于向控件提供自定义功能时更重要,因此可能会不断生成。不要指望垃圾收集器为此,它在这种情况下对你无能为力。
string searchTerm = string.Empty;
TextFormatFlags format = TextFormatFlags.Top | TextFormatFlags.Left |
TextFormatFlags.NoClipping | TextFormatFlags.NoPadding;
private Size RenderText(string text, DrawItemEventArgs e, FontStyle style, Color altForeColor, Point offset)
{
var color = altForeColor == Color.Empty ? e.ForeColor : altForeColor;
using (var font = new Font(e.Font, style)) {
var textSize = TextRenderer.MeasureText(e.Graphics, text, font, e.Bounds.Size, format);
var rect = new Rectangle(offset, e.Bounds.Size);
TextRenderer.DrawText(e.Graphics, text, font, rect, color, e.BackColor, format);
return textSize;
}
}
private IEnumerable<(string Text, bool Selected)> BuildDrawingString(string itemContent, string pattern)
{
if (pattern.Length == 0) {
yield return (itemContent, false);
}
else {
var matches = Regex.Split(itemContent, $"(?i){pattern}");
int pos = itemContent.IndexOf(pattern, StringComparison.CurrentCultureIgnoreCase);
for (int i = 0; i < matches.Length; i++) {
if (matches[i].Length == 0 && i < matches.Length - 1) {
yield return (itemContent.Substring(pos, pattern.Length), matches[i].Length > 0 ? false : true);
}
else {
yield return (matches[i], false);
if (i < matches.Length - 1) {
yield return (itemContent.Substring(pos, pattern.Length), true);
}
}
}
}
}
private void comboBoxItems_DrawItem(object sender, DrawItemEventArgs e)
{
var listItem = (sender as ComboBox).Items[e.Index] as DocGenFieldItem;
e.DrawBackground();
int drawingPosition = 0;
foreach (var part in BuildDrawingString(listItem.Display, searchTerm)) {
var style = part.Selected ? FontStyle.Bold : FontStyle.Regular;
drawingPosition += RenderText(part.Text, e, style, Color.Empty, new Point(drawingPosition, e.Bounds.Y)).Width;
}
var offsetBottom = new Point(0, e.Bounds.Bottom - e.Font.Height - 2);
var valueSize = RenderText("Value: ", e, FontStyle.Bold, Color.FromArgb(64, 64, 64), offsetBottom);
offsetBottom.Offset(valueSize.Width, 0);
RenderText(listItem.Value, e, FontStyle.Regular, Color.FromArgb(63, 63, 63), offsetBottom);
e.DrawFocusRectangle();
}
private void comboBoxItems_MeasureItem(object sender, MeasureItemEventArgs e)
=> e.ItemHeight = (sender as Control).Font.Height * 2 + 4;
关于更新前问题中使用的Graphics.MeasureString()
和Graphics.DrawString()
方法:
- 当我们使用特定的 StringFormat 测量文本时,如果我们希望我们的绘图符合测量范围,那么我们使用相同的 StringFormat 绘制文本。
当使用 Graphics.DrawString()
呈现文本时,Graphics.TextRenderingHint = TextRenderingHint.AntiAlias
效果不佳:请改用 TextRenderingHint.ClearTypeGridFit
。
- 可能,避免
Microsoft Sans Serif
作为字体,使用 Segoe UI
或 Microsoft YaHei UI
代替(例如):这些字体的权重要好得多,并且专门为此设计(UI
后缀放弃了它)。
我正在尝试实施类似于 Visual Studio 的 Go To
成员搜索的 auto-complete/search 框:
但是,我对 bold
文本及其间距的格式计算不正确。我将省略它的自动完成功能,只包括通过硬编码搜索词来格式化结果的代码。
e.Graphics.MeasureString
确定的间距似乎 return 不正确。我尝试使用 this question 中的 StringFormat.GenericTypographic
并且我接近了但仍然不正确。
这是我的下拉列表的显示,其中匹配的术语(粗体)很容易表明我的格式位置计算已关闭(f
显然侵占了 i
)。
除此之外,如果我将鼠标悬停在某个项目上,它会重新绘制我的文本 并且没有 粗体。我也想阻止它。
更新:我更改了我的代码以使用 TextRenderer
但现在看起来更糟了。
现在似乎在我连接的每场比赛前后都有额外的 space。
更新了以下代码:
private void Form1_Load( object sender, EventArgs e )
{
var docGenFields = new[] {
new DocGenFieldItem { Display = $"Profile.date-birth.value", Value = "5/9/1973", FieldCode = $"Profile.date-birth.value" },
new DocGenFieldItem { Display = $"Profile.date-birth.text", Value = "Birth Date", FieldCode = $"Profile.date-birth.text" },
new DocGenFieldItem { Display = $"Profile.date-birth.raw-value", Value = "1973-05-09", FieldCode = $"Profile.date-birth.raw-value" },
new DocGenFieldItem { Display = $"Profile.name-first.value", Value = "Terry", FieldCode = $"Profile.name-first.value" },
new DocGenFieldItem { Display = $"Profile.name-first.text", Value = "First Name", FieldCode = $"Profile.name-first.text" },
new DocGenFieldItem { Display = $"Profile.name-first.raw-value", Value = "Terry", FieldCode = $"Profile.name-first.raw-value" },
new DocGenFieldItem { Display = $"Profile.name-first.value", Value = "Minnesota", FieldCode = $"Profile.state.value" },
new DocGenFieldItem { Display = $"Profile.name-first.text", Value = "State", FieldCode = $"Profile.state.text" },
new DocGenFieldItem { Display = $"Profile.name-first.raw-value", Value = "MN", FieldCode = $"Profile.state.raw-value" }
};
comboBoxItems.FormattingEnabled = true;
comboBoxItems.DrawMode = DrawMode.OwnerDrawVariable;
comboBoxItems.DropDownHeight = 44 * 5;
// comboBoxItems.Font = new Font( "Microsoft Sans Serif", 12F, FontStyle.Regular, GraphicsUnit.Point, 0 );
comboBoxItems.Font = new Font( "Segoe UI", 12F, FontStyle.Regular, GraphicsUnit.Point, 0 );
comboBoxItems.Items.AddRange( docGenFields );
comboBoxItems.DrawItem += new DrawItemEventHandler( comboBoxItems_DrawItem );
comboBoxItems.MeasureItem += new MeasureItemEventHandler( comboBoxItems_MeasureItem );
}
private void comboBoxItems_DrawItem( object sender, DrawItemEventArgs e )
{
// Draw the background of the item.
e.DrawBackground();
var listItem = comboBoxItems.Items[ e.Index ] as DocGenFieldItem;
var searchTerm = "P";
var matches = Regex.Split( listItem.Display, "(?i)" + searchTerm );
var bold = new Font( e.Font.FontFamily, e.Font.Size, FontStyle.Bold );
// e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
var currentCharacter = 0;
// float currentX = 0;
var currentX = 0;
var currentMatch = 0;
var keyLength = searchTerm.Length;
foreach ( var m in matches )
{
// If search term characters are first (like StartsWith) or last (like EndsWith) characters
// then the match will be empty. So if not empty, then need to render the characters 'between'
// matches of search term in regular font
if ( !string.IsNullOrEmpty( m ) )
{
// var p = new PointF( e.Bounds.X + currentX, e.Bounds.Y );
// var mWidth = e.Graphics.MeasureString( m, e.Font, p, StringFormat.GenericTypographic );
// e.Graphics.DrawString( m, e.Font, Brushes.Black, p );
var p = new Point( currentX, e.Bounds.Y );
var mWidth = TextRenderer.MeasureText( e.Graphics, m, e.Font );
TextRenderer.DrawText( e.Graphics, m, e.Font, p, System.Drawing.Color.Black );
currentX += mWidth.Width;
currentCharacter += m.Length;
}
currentMatch++;
// Render the search term characters (need to use 'substring' of current text to maintain
// original case of text) *bold* in between matches.
// string.IsNullOrEmpty( m ) && currentMatch == 1 - If the search term matches ENTIRE value
// then currentMatch will = matches.Length (1) but the match of 'm' will be empty.
if ( currentMatch < matches.Length || ( string.IsNullOrEmpty( m ) && currentMatch == 1 ) )
{
var mValue = listItem.Display.Substring( currentCharacter, keyLength );
// var p = new PointF( e.Bounds.X + currentX, e.Bounds.Y );
// var mWidth = e.Graphics.MeasureString( mValue, bold, p, StringFormat.GenericTypographic );
// e.Graphics.DrawString( mValue, bold, Brushes.Black, p, StringFormat.GenericTypographic );
var p = new Point( currentX, e.Bounds.Y );
var mWidth = TextRenderer.MeasureText( e.Graphics, mValue, bold );
TextRenderer.DrawText( e.Graphics, mValue, bold, p, System.Drawing.Color.Black );
currentX += mWidth.Width;
currentCharacter += keyLength;
}
}
// Render a secondary 'info' line in the dropdown
var b = new SolidBrush( ColorTranslator.FromHtml( "#636363" ) );
var valueWidth = e.Graphics.MeasureString( "Value: ", bold );
e.Graphics.DrawString( "Value: ", bold, b,
new RectangleF( e.Bounds.X, e.Bounds.Y + 21, e.Bounds.Width, e.Bounds.Height )
);
e.Graphics.DrawString( listItem.Value, e.Font, b,
new RectangleF( e.Bounds.X + valueWidth.Width, e.Bounds.Y + 21, e.Bounds.Width, 21 )
);
// Draw the focus rectangle if the mouse hovers over an item.
e.DrawFocusRectangle();
}
private void comboBoxItems_MeasureItem( object sender, MeasureItemEventArgs e )
{
e.ItemHeight = 44;
}
当TextRenderer is used to render text in a non-generic Graphics context, this context needs to be considered: for this reason, TextRenderer provides overloads of both MeasureText and DrawText that accept a Graphics context (IDeviceContext)参数时。
Graphics 上下文包含 TextRenderer 可以用来更好地适应 DC 细节的信息。
此外,我们需要将 TextFormatFlags 值的组合传递给方法,这些值定义了我们要如何测量 and/or 呈现文本。
- 始终声明对齐类型
- 指定 clipping/wrapping 行为(例如,我们希望文本换行或者我们真的不希望它换行,我们希望它被剪裁)
- 如果不应填充文本,请指定
TextFormatFlags.NoPadding
,否则文本将被 拉伸 以填充绘图边界。 - 如果未手动安排绘图边界(在特定位置绘制文本),请指定
TextFormatFlags.LeftAndRightPadding
以向文本添加预定义的填充。此设置应用的填充(基于字体字距调整)匹配文本与标准控件(例如,ListBox 或 ListView)边框之间的距离
有关 TextFormatFlags
的更多信息(部分 :) 可在文档中找到。
我已将所有绘图部分移动到一个方法中,RenderText()
。
所有的测量和绘图都在这里进行:这样在绘制项目时应该更容易理解是怎么回事。
DrawItem
处理程序中的代码调用此方法,在满足特定条件时传递一些适当的值(如更改 FontStyle
,部分文本的替代 ForeColor
等)
导致:
▶ 这里使用的字体是Microsoft YaHei UI, 12pt
。当然你可以使用任何其他字体,但是具有 UI
appendix 的系统字体系列是为此设计的(很好)。
▶ 请记住处理您创建的图形对象,这非常重要,当这些对象用于向控件提供自定义功能时更重要,因此可能会不断生成。不要指望垃圾收集器为此,它在这种情况下对你无能为力。
string searchTerm = string.Empty;
TextFormatFlags format = TextFormatFlags.Top | TextFormatFlags.Left |
TextFormatFlags.NoClipping | TextFormatFlags.NoPadding;
private Size RenderText(string text, DrawItemEventArgs e, FontStyle style, Color altForeColor, Point offset)
{
var color = altForeColor == Color.Empty ? e.ForeColor : altForeColor;
using (var font = new Font(e.Font, style)) {
var textSize = TextRenderer.MeasureText(e.Graphics, text, font, e.Bounds.Size, format);
var rect = new Rectangle(offset, e.Bounds.Size);
TextRenderer.DrawText(e.Graphics, text, font, rect, color, e.BackColor, format);
return textSize;
}
}
private IEnumerable<(string Text, bool Selected)> BuildDrawingString(string itemContent, string pattern)
{
if (pattern.Length == 0) {
yield return (itemContent, false);
}
else {
var matches = Regex.Split(itemContent, $"(?i){pattern}");
int pos = itemContent.IndexOf(pattern, StringComparison.CurrentCultureIgnoreCase);
for (int i = 0; i < matches.Length; i++) {
if (matches[i].Length == 0 && i < matches.Length - 1) {
yield return (itemContent.Substring(pos, pattern.Length), matches[i].Length > 0 ? false : true);
}
else {
yield return (matches[i], false);
if (i < matches.Length - 1) {
yield return (itemContent.Substring(pos, pattern.Length), true);
}
}
}
}
}
private void comboBoxItems_DrawItem(object sender, DrawItemEventArgs e)
{
var listItem = (sender as ComboBox).Items[e.Index] as DocGenFieldItem;
e.DrawBackground();
int drawingPosition = 0;
foreach (var part in BuildDrawingString(listItem.Display, searchTerm)) {
var style = part.Selected ? FontStyle.Bold : FontStyle.Regular;
drawingPosition += RenderText(part.Text, e, style, Color.Empty, new Point(drawingPosition, e.Bounds.Y)).Width;
}
var offsetBottom = new Point(0, e.Bounds.Bottom - e.Font.Height - 2);
var valueSize = RenderText("Value: ", e, FontStyle.Bold, Color.FromArgb(64, 64, 64), offsetBottom);
offsetBottom.Offset(valueSize.Width, 0);
RenderText(listItem.Value, e, FontStyle.Regular, Color.FromArgb(63, 63, 63), offsetBottom);
e.DrawFocusRectangle();
}
private void comboBoxItems_MeasureItem(object sender, MeasureItemEventArgs e)
=> e.ItemHeight = (sender as Control).Font.Height * 2 + 4;
关于更新前问题中使用的Graphics.MeasureString()
和Graphics.DrawString()
方法:
- 当我们使用特定的 StringFormat 测量文本时,如果我们希望我们的绘图符合测量范围,那么我们使用相同的 StringFormat 绘制文本。 当使用
Graphics.TextRenderingHint = TextRenderingHint.AntiAlias
效果不佳:请改用TextRenderingHint.ClearTypeGridFit
。- 可能,避免
Microsoft Sans Serif
作为字体,使用Segoe UI
或Microsoft YaHei UI
代替(例如):这些字体的权重要好得多,并且专门为此设计(UI
后缀放弃了它)。
Graphics.DrawString()
呈现文本时,