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
andRemoveItem
methods directly on my derivedListBox
class. I could create a corresponding button each time a new item is added, and remove the item's button inRemoveItem
. However, to me, this is an ugly hack, because it forces me to call these special add/remove methods instead of just usingListBox.Items
. - I could draw a new button manually in
OnDrawItem
usingSystem.Windows.Forms.ButtonRenderer
, but then I have to do a lot of extra work to make it act like a real button, sinceButtonRenderer
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 aDictionary<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 theListBox
anymore, but then I'm iterating over two lists every time an item in theListBox
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.