自定义ToolStrip渲染器中如何测量ToolStripItem尺寸适配GDI+绘制
问题描述
基于.NET Framework开发WinForms应用时,为统一视觉风格,给ContextMenuStrip组件编写了自定义渲染器,使用GDI+的Graphics.DrawString()方法绘制菜单项文本,自定义渲染器核心代码如下:
private class CustomContextMenuRenderer : ToolStripProfessionalRenderer { private static StringFormat fStringFormatLeft = new StringFormat() { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center}; private static StringFormat fStringFormatRight = new StringFormat() { Alignment = StringAlignment.Far, LineAlignment = StringAlignment.Center }; protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) { StringFormat sf = (e.TextFormat & TextFormatFlags.Right) == 0 ? fStringFormatLeft : fStringFormatRight; using (SolidBrush brush = new SolidBrush(e.Item.ForeColor)) { e.Graphics.DrawString(e.Text, e.TextFont, brush, e.TextRectangle, sf); } } }
使用部分字体时会出现菜单项高度不足、文本显示不全的问题,原因是默认渲染器使用GDI的TextRenderer类计算文本尺寸,和GDI+DrawString的实际渲染尺寸存在偏差。需要找到适配GDI+渲染的菜单项尺寸测量、上报方案。
解决方案
问题根源是默认ToolStripProfessionalRenderer的尺寸计算逻辑和自定义GDI+绘制逻辑不匹配,只需要在自定义渲染器中重写OnMeasureItem方法,用GDI+的文本测量逻辑替换默认的GDI测量逻辑,保证测量规则和绘制规则完全一致即可。
实现要点
- 测量文本时使用和绘制时完全相同的
StringFormat配置,避免格式参数差异导致测量结果不准 - 计算尺寸时要把图标区域、快捷键文本、内外边距的占用空间纳入计算,避免内容重叠
- 仅当GDI+计算出的所需尺寸大于默认尺寸时覆盖原有值,保留系统默认的最小尺寸规则,避免菜单项过小
修正后的完整渲染器代码
private class CustomContextMenuRenderer : ToolStripProfessionalRenderer { private static StringFormat fStringFormatLeft = new StringFormat() { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center, FormatFlags = StringFormatFlags.NoWrap | StringFormatFlags.MeasureTrailingSpaces }; private static StringFormat fStringFormatRight = new StringFormat() { Alignment = StringAlignment.Far, LineAlignment = StringAlignment.Center, FormatFlags = StringFormatFlags.NoWrap | StringFormatFlags.MeasureTrailingSpaces }; // 布局常量,和系统默认ContextMenuStrip参数保持一致 private const int ItemVerticalPadding = 4; private const int ItemHorizontalPadding = 4; private const int DefaultIconAreaWidth = 24; protected override void OnMeasureItem(ToolStripItemMeasureEventArgs e) { base.OnMeasureItem(e); // 仅处理下拉菜单中的普通菜单项,跳过分隔符、顶级菜单项 if (e.Item is not ToolStripMenuItem menuItem || !menuItem.IsOnDropDown) return; // 选择和绘制逻辑一致的文本格式 StringFormat sf = (e.TextFormat & TextFormatFlags.Right) == 0 ? fStringFormatLeft : fStringFormatRight; // 用GDI+接口测量主文本尺寸 SizeF mainTextSize = e.Graphics.MeasureString(menuItem.Text, menuItem.Font, int.MaxValue, sf); // 测量快捷键文本尺寸 SizeF shortcutTextSize = SizeF.Empty; if (!string.IsNullOrEmpty(menuItem.ShortcutKeyDisplayString)) { shortcutTextSize = e.Graphics.MeasureString(menuItem.ShortcutKeyDisplayString, menuItem.Font, int.MaxValue, fStringFormatRight); } // 计算所需总高度:文本高度 + 上下边距 int requiredHeight = (int)Math.Ceiling(mainTextSize.Height) + ItemVerticalPadding * 2; // 计算所需总宽度:图标区 + 主文本 + 快捷键区间距 + 快捷键文本 + 左右边距 int requiredWidth = DefaultIconAreaWidth + (int)Math.Ceiling(mainTextSize.Width) + ItemHorizontalPadding * 2; if (!shortcutTextSize.IsEmpty) { requiredWidth += (int)Math.Ceiling(shortcutTextSize.Width) + ItemHorizontalPadding * 3; } // 覆盖默认尺寸,保证文本完整显示 if (requiredHeight > e.ItemHeight) e.ItemHeight = requiredHeight; if (requiredWidth > e.ItemWidth) e.ItemWidth = requiredWidth; } protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) { if (e.Item is not ToolStripMenuItem) { base.OnRenderItemText(e); return; } StringFormat sf = (e.TextFormat & TextFormatFlags.Right) == 0 ? fStringFormatLeft : fStringFormatRight; // 调整文本绘制区域高度和菜单项高度一致,保证垂直居中效果正确 Rectangle drawRect = new Rectangle( e.TextRectangle.X, e.TextRectangle.Y, e.TextRectangle.Width, e.Item.Height ); using (SolidBrush textBrush = new SolidBrush(e.Item.ForeColor)) { e.Graphics.DrawString(e.Text, e.TextFont, textBrush, drawRect, sf); } } }
适配说明
- 如果修改了菜单项的图标大小、边距、快捷键显示规则,同步调整代码中对应的布局常量即可
- 高DPI场景下,可以根据当前设备DPI缩放比例动态计算布局常量值,避免缩放后布局错位
- 选中、禁用、悬停等不同状态的菜单项只要未单独设置特殊字体,不需要额外调整尺寸逻辑
内容的提问来源于stack exchange,提问作者Tecman
相关产品推荐
相关产品推荐

