I'm trying to implement a auto-complete/search box similar to Visual Studio's Go To
member search:
However, my formatting of the bold
text and its spacing isn't calculating right. I'll omit the auto complete functionality of this and only include code that is formatting the result by hard coding a search term.
The spacing determined by e.Graphics.MeasureString
doesn't seem to return correct value. I tried to use StringFormat.GenericTypographic
from this question and I got closer but still not correct.
Here is a display of my dropdown where the matched term (in bold) is easily showing that my format position calculation is off (the f
is clearly encroaching on the i
).
In addition to that, if I hover over an item, it redraws my text without bold. I'd like to stop that as well.
Update: I changed my code to use TextRenderer
but now it appears even worse.
There now seems to be extra space before and after each match I concatenate.
Updated code below:
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;
}
See Question&Answers more detail:
os