Mono环境下强制WinForms ListBox重新计算所有项高度的问题
解决Mono下WinForms ListBox OwnerDrawVariable滚动范围错误问题
问题分析
在Mono环境中,使用OwnerDrawVariable模式实现ListBox项自动换行和高度调整时,滚动条的总高度计算存在缺陷:它会采用默认项高度 × 项数的固定逻辑,而非累加每个项的实际高度,导致当存在多高度项时,列表总滚动范围不足,最后一项无法显示也无法滚动到视图中。尽管MeasureItem事件能正确触发并计算每个项的高度,但Mono的ListBox并未基于这些值更新滚动区域。
修复方案
通过自定义ListBox类,手动维护每个项的高度,并调用Win32 API强制设置正确的滚动条范围,替代Mono默认的滚动计算逻辑。
自定义ListBox实现代码
using System; using System.Collections.Generic; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; public class CustomListBox : ListBox { private List<int> _itemHeights = new List<int>(); public CustomListBox() { DrawMode = DrawMode.OwnerDrawVariable; } protected override void OnMeasureItem(MeasureItemEventArgs e) { base.OnMeasureItem(e); if (Items.Count <= 0 || e.Index >= Items.Count) return; string itemText = Items[e.Index].ToString(); // 计算项的实际高度(适配ListBox宽度自动换行) int calculatedHeight = (int)e.Graphics.MeasureString(itemText, Font, ClientSize.Width).Height; e.ItemHeight = calculatedHeight; // 维护每个项的高度记录 if (_itemHeights.Count > e.Index) _itemHeights[e.Index] = calculatedHeight; else _itemHeights.Add(calculatedHeight); } protected override void OnDrawItem(DrawItemEventArgs e) { base.OnDrawItem(e); if (Items.Count <= 0 || e.Index >= Items.Count) return; e.DrawBackground(); e.DrawFocusRectangle(); // 绘制项文本(自动换行已由MeasureItem的宽度限制实现) e.Graphics.DrawString(Items[e.Index].ToString(), e.Font, new SolidBrush(e.ForeColor), e.Bounds); } protected override void OnItemsChanged(EventArgs e) { base.OnItemsChanged(e); _itemHeights.Clear(); // 触发重新测量所有项并更新滚动范围 Refresh(); UpdateScrollRange(); } protected override void OnResize(EventArgs e) { base.OnResize(e); // 宽度变化时重新计算所有项高度并更新滚动范围 _itemHeights.Clear(); Refresh(); UpdateScrollRange(); } private void UpdateScrollRange() { if (_itemHeights.Count != Items.Count) return; // 计算所有项的总高度 int totalContentHeight = _itemHeights.Sum(); // 设置滚动条参数 SCROLLINFO scrollInfo = new SCROLLINFO(); scrollInfo.cbSize = (uint)Marshal.SizeOf(scrollInfo); scrollInfo.fMask = (uint)ScrollInfoMask.SIF_ALL; scrollInfo.nMin = 0; scrollInfo.nMax = totalContentHeight; scrollInfo.nPage = ClientSize.Height; // 可见区域高度 scrollInfo.nPos = VerticalScroll.Value; // 调用Win32 API更新滚动条 SetScrollInfo(Handle, (int)ScrollBarDirection.SB_VERT, ref scrollInfo, true); } // Win32 API导入 [DllImport("user32.dll")] private static extern bool SetScrollInfo(IntPtr hwnd, int fnBar, ref SCROLLINFO lpsi, bool fRedraw); private enum ScrollBarDirection { SB_HORZ = 0, SB_VERT = 1 } private enum ScrollInfoMask { SIF_RANGE = 0x1, SIF_PAGE = 0x2, SIF_POS = 0x4, SIF_ALL = SIF_RANGE | SIF_PAGE | SIF_POS } [StructLayout(LayoutKind.Sequential)] private struct SCROLLINFO { public uint cbSize; public uint fMask; public int nMin; public int nMax; public uint nPage; public int nPos; public int nTrackPos; } }
使用方法
- 将原有窗体中的
ListBox控件替换为CustomListBox(可在设计器中添加,或通过代码实例化)。 - 无需再调用原有的
ModListbox方法,因为自定义类已内置设置DrawMode和事件绑定。
关键修复点
- 维护
_itemHeights列表记录每个项的实际高度,确保滚动范围计算准确。 - 在项集合变化、控件 resize 时重新计算高度并更新滚动条。
- 通过Win32 API
SetScrollInfo强制覆盖Mono默认的滚动条计算逻辑,确保滚动范围与实际内容总高度匹配。
内容的提问来源于stack exchange,提问作者Vlad Stefan
相关产品推荐
相关产品推荐

