views:

311

answers:

2

I'm writing a simple custom owner-drawn ListBox control and banging my head against the wall trying to figure out how to implement what seems like a straight-foward feature. My custom ListBox is supposed to work similarly to the "Add/Remove Programs" list in Windows XP. That is, it displays a list of items as usual, but when the user selects an item in the list, a clickable button should appear next to the item text. In my case, I'm trying to display an "Import" button next to each item in my ListBox.

In order to keep the custom ListBox somewhat encapsulated, I'm trying to display the button by overriding the ListBox.OnDrawItem method (i.e. the button is conceptually part of the list box item), but I can't get it to work quite right.

I should note that I'm trying to use a single button for the entire ListBox. When the user selects a item in the list, the OnDrawItem method just re-positions this single button so that it appears next to the selected item. I have it 99% working: the problem now is that when the ListBox is scrolled and the selected item goes off-screen, the button is still drawn to its previous position, so it draws on top of the wrong item. I'm guessing this is because Windows won't try to redraw the selected item if it is off-screen, and thus the repositioning code doesn't get called.

Here is trimmed-down version of what I have right now:

public partial class EventListBox : ListBox
{
    private Button _importButton;

    public EventListBox()
    {
        InitializeComponent();
        this.DrawMode = DrawMode.OwnerDrawVariable;

    // set up the button that will appear next to the currently-selected item
        _importButton = new Button();
        _importButton.Text = "Import...";
        _importButton.AutoSize = true;
        _importButton.Visible = false;

    // Add the button as a child control of this ListBox
        Controls.Add(_importButton);
    }


    protected override void OnDrawItem(DrawItemEventArgs e)
    {
        if (this.Items.Count > 0)
        {
            e.DrawBackground();

 // draw item here (omitted)

 // if drawing the selected item, re-position the "Import" button so that appears
 // inside the current item, and make it visible if it is hidden.
 // These checks prevent the resulting repaint that will occur from causing an infinite loop

 // The problem seems to be that if the ListBox is scrolled such that the selected item
 // moves off-screen , this code won't run, because it won't repaint the selected item anymore...
 // This means the button will be painted in its previous position.

 // The real question is: Is there a better way to approach the whole notion of
 // rendering buttons within ListBox items?

            if (e.State & DrawItemState.Selected == DrawItemState.Selected)
            {
                _importButton.Top = e.Bounds.Bottom - _importButton.Height - 20;
                _importButton.Left = e.Bounds.Left;
                if(!_importButton.Visible) _importButton.Visible = true;
            }
        }

        base.OnDrawItem(e);
    }

    protected override void OnMeasureItem(MeasureItemEventArgs e)
    {
        base.OnMeasureItem(e);
        e.ItemHeight = 100; //hard-coded for now...
    }

}

Rationale for using a single button

I would rather create a separate button for each item in the ListBox, but I can't find any way to track when items are added/removed from the listbox. I couldn't find any relevant ListBox methods that I could override, and I can't re-assign the ListBox.Items property to a custom collection object, since ListBox.Items is read-only. Because of this, I went with the above approach of using a single button and re-positioning it as needed, but as I mentioned, this isn't very robust and breaks easily.

My current thinking is it would make the most sense to create a new button at the point when new items are added to the ListBox, and remove buttons when items were removed.

Here are some possible solutions I came up with, but is there a better way to implement this?

  • I could just create my own AddItem and RemoveItem methods directly on my derived ListBox class. I could create a corresponding button each time a new item is added, and remove the item's button in RemoveItem. However, to me, this is an ugly hack, because it forces me to call these special add/remove methods instead of just using ListBox.Items.
  • I could draw a new button manually in OnDrawItem using System.Windows.Forms.ButtonRenderer, but then I have to do a lot of extra work to make it act like a real button, since ButtonRenderer is doing nothing more than drawing the button. Figuring out when the user hovers over this "button", and when it is clicked, seems like it would be difficult to get right.
  • When OnDrawItem is called, I could create a new button if the item being drawn doesn't already have a button associated with it (I could keep track of this with a Dictionary<Item, Button>), but I still need a way to remove unused buttons when their corresponding items are removed from the list. I guess I could iterate over my dictionary of item-button mappings and remove items that don't exist in the ListBox anymore, but then I'm iterating over two lists every time an item in the ListBox is redrawn (ack!).

So, is there a better way to include clickable buttons inside a ListBox'? It's obviously been done before, but I can't find anything useful on Google. The only examples I've seen that have buttons in a ListBox` were WPF examples, but I'm looking for how to do this with WinForms.

A: 

One Possible Solution

I found a workable solution, and ended up keeping the single button approach for now. I'll post my workaround here, but if anyone has a more elegant answer to my original question, don't hestitate to post it.

My small epiphany

After experimenting some more, it seems the issue of the button not getting repositioned properly on scrolling only happens when using the mouse wheel to scroll through the ListBox. Scrolling the "normal" way doesn't seem to reproduce the behavior, but as I couldn't be 100% sure, I included a fix for normal scrolling in my workaround as well.

My ugly hack of a workaround

Since I knew the mouse wheel (and scrolling in general) seemed to be at the heart of the issue, I decided to just invalidate my ListBox whenever the ListBox is scrolled or whenever the mouse wheel is moved. This does produce some unsightly flicker, which I would like to get rid of, but I can live with the results for now.

I added the following methods to my derived EventListBox class:

    protected override void WndProc(ref Message m)
    {
        const int WM_VSCROLL = 277;

        if (m.Msg == WM_VSCROLL)
        {
            this.Invalidate();
        }

        base.WndProc(ref m);
    }

    protected override void OnMouseWheel(MouseEventArgs e)
    {
        this.Invalidate();
        base.OnMouseWheel(e);
    }

I was a little surprised that ListBox doesn't inherit from ScrollableControl and there wasn't anything like an OnScroll method that I could override, so I did the scroll checking by overriding the WndProc method. The mouse wheel detection was simpler, since there was already an method available to override.

Like I said, this is less than ideal as invalidating the listbox each time the list is scrolled causes flicker, and I suspect performance would degrade significantly when the listbox contains a lot of items.

I won't accept this answer yet, as I'm curious to see if someone has a better solution.

Mike Spross
A: 

There is a 3-rd party control IntegralUI ListBox from Lidor Systems, which allows to include ANY control in every item. There is no limit on how many controls you can add. Also you can create your own templates and order the item content in custom layouts.

Lokey