Re-measuring existing items in ListBox

2019-08-08 11:28发布

问题:

I have a ListBox in WinForms that displays some data, and when you click on an item, it expands and displays more. This is the code:

  private void historyList_SelectedIndexChanged(object sender, EventArgs e)
  {
     historyList.Refresh();
  }

  private void historyList_DrawItem(object sender, DrawItemEventArgs e)
  {
     e.DrawBackground();

     FeedHistoryItem item = (sender as ListBox).Items[e.Index] as FeedHistoryItem;
     if (e.Index == historyList.SelectedIndex)
     {
        e.Graphics.DrawString(item.ExpandedText,
           e.Font, Brushes.Black, e.Bounds);
     }
     else
     {
        e.Graphics.DrawString(item.CollapsedText,
           e.Font, Brushes.Black, e.Bounds);
     }

     e.DrawFocusRectangle();
  }      

  private void historyList_MeasureItem(object sender, MeasureItemEventArgs e)
  {
     FeedHistoryItem item = (sender as ListBox).Items[e.Index] as FeedHistoryItem;
     string itemText = e.Index == historyList.SelectedIndex ? item.ExpandedText : item.CollapsedText;
     int size = 15 * (itemText.Count(s => s == '\r') + 1);
     e.ItemHeight = size;
  }

The events are called as expected. When you click on an item, it calls Refresh(), then measure, then draw. The text expands. However, the size doesn't change.

I've verified that the first time an item is drawn, it respects when I put in ItemHeight, but when resizing, it does not - if I set a breakpoint in the DrawItem method, even though MeasureItem was just called, the height in e.Bounds does not update. Yes - I do have historyList.DrawMode = DrawMode.OwnerDrawVariable; set.

Any ideas?

回答1:

Try inheriting the ListBox control and call the RecreateHandle on the OnSelectedIndexChanged method.

public class ListEx : ListBox {
  protected override void OnSelectedIndexChanged(EventArgs e) {
    base.OnSelectedIndexChanged(e);
    this.RecreateHandle();
  }
}

Unfortunately, it will probably flicker, but I think it's the only way to get the ListBox to call the MeasureItem event again. The other way is to suspend the drawing and then remove the item from the list and put it back in again. No clean way to do this in WinForms.



回答2:

I required something similar: when the ListBox is resized I need the items to resize too to fit the new width.

So every time my ListBox is resized I call the following method, which will resize every item! (be carefully when you have too much items)

private static void ForceMeasureItems(ListBox listBox, Action<object, MeasureItemEventArgs> onMeasureEvent)
{
    for (int i = 0; i < listBox.Items.Count; i++)
    {
        MeasureItemEventArgs eArgs = new MeasureItemEventArgs(listBox.CreateGraphics(), i);
        onMeasureEvent(listBox, eArgs);
        SendMessage(listBox.Handle, LB_SETITEMHEIGHT, i, eArgs.ItemHeight);
    }
    listBox.Refresh();
}

Usage:

private void OnMyMeasureItem(object sender, MeasureItemEventArgs e)
{
    // measure logic goes here
    int myCustomHeightValue = 22;
    e.ItemHeight = myCustomHeightValue;
}

ListBox listBox = new ListBox();
listBox.MeasureItem += OnMyMeasureItem;
listBox.Resize += (sender, args) => ForceMeasureItems(listBox, OnMyMeasureItem);

Reference: MSDN Community