You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
    }
}

使用方法

  1. 将原有窗体中的ListBox控件替换为CustomListBox(可在设计器中添加,或通过代码实例化)。
  2. 无需再调用原有的ModListbox方法,因为自定义类已内置设置DrawMode和事件绑定。

关键修复点

  • 维护_itemHeights列表记录每个项的实际高度,确保滚动范围计算准确。
  • 在项集合变化、控件 resize 时重新计算高度并更新滚动条。
  • 通过Win32 APISetScrollInfo强制覆盖Mono默认的滚动条计算逻辑,确保滚动范围与实际内容总高度匹配。

内容的提问来源于stack exchange,提问作者Vlad Stefan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 01:18:14