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 UIMicrosoft YaHei UI 代替(例如):这些字体的权重要好得多,并且专门为此设计(UI 后缀放弃了它)。