Search code examples
c#visual-studiowinformslistbox

Winforms - Listbox item hover and select color


I'm trying to recreate a listbox from a WPF app for my Winforms app. But I really have no idea where to even start.

The selected item's border is blue (1) and when it's unfocused it turns white (2). The hover color is supposed to be a lighter blue (3)

Thanks for your help!

(By the way, I'm using .NET Framework 4.8)


Solution

  • You can create your own listbox deriving from the original one. Once the new list is compiled for the first time, it will appear in the toolbox, so that you can drag and drop it onto your forms. Or you can change the type of existing listboxes to ListBoxEx in the Form.designer.cs manually.

    public class ListBoxEx : ListBox
    {
        public ListBoxEx()
        {
            DrawMode = DrawMode.OwnerDrawFixed;
            DoubleBuffered = true; // Eliminates flicker (optional).
        }
    
        private int _hotTrackedIndex = -1;
        private int HotTrackedIndex
        {
            get => _hotTrackedIndex;
            set {
                if (value != _hotTrackedIndex) {
                    if (_hotTrackedIndex >= 0 && _hotTrackedIndex < Items.Count) {
                        Invalidate(GetItemRectangle(_hotTrackedIndex));
                    }
                    _hotTrackedIndex = value;
                    if (_hotTrackedIndex >= 0) {
                        Invalidate(GetItemRectangle(_hotTrackedIndex));
                    }
                }
            }
        }
    
        protected override void OnDrawItem(DrawItemEventArgs e)
        {
            var borderRect = e.Bounds;
            borderRect.Width--;
            borderRect.Height--;
            if ((e.State & DrawItemState.Selected) == DrawItemState.Selected) {
                if (Focused) {
                    e.Graphics.FillRectangle(Brushes.Teal, e.Bounds);
                    e.Graphics.DrawRectangle(Pens.LightSkyBlue, borderRect);
                } else {
                    e.Graphics.FillRectangle(Brushes.DimGray, e.Bounds);
                    e.Graphics.DrawRectangle(Pens.White, borderRect);
                }
            } else if (e.Index == HotTrackedIndex) {
                e.Graphics.FillRectangle(Brushes.DarkSlateGray, e.Bounds);
                e.Graphics.DrawRectangle(Pens.DarkCyan, borderRect);
            } else {
                e.DrawBackground();
            }
            if (Items[e.Index] != null) {
                e.Graphics.DrawString(Items[e.Index].ToString(), e.Font, Brushes.White, 6, e.Bounds.Top, StringFormat.GenericTypographic);
            }
        }
    
        protected override void OnMouseLeave(EventArgs e)
        {
            HotTrackedIndex = -1;
            base.OnMouseLeave(e);
        }
    
        protected override void OnMouseMove(MouseEventArgs e)
        {
            HotTrackedIndex = IndexFromPoint(e.Location);
            base.OnMouseMove(e);
        }
    
        protected override void OnGotFocus(EventArgs e)
        {
            if (SelectedIndex >= 0) {
                RefreshItem(SelectedIndex);
            }
            base.OnGotFocus(e);
        }
    
        protected override void OnLostFocus(EventArgs e)
        {
            if (SelectedIndex >= 0) {
                RefreshItem(SelectedIndex);
            }
            base.OnLostFocus(e);
        }
    }
    

    We change the appearance of the listbox by overriding OnDrawItem. in the constructor, we set DrawMode = DrawMode.OwnerDrawFixed; to enable owner drawing.

    We must consider selected items and hot tracked items, i.e., items the mouse moves over. If the item to be drawn is the selected one, we further differentiate between the cases where the listbox has the focus or not.

    FillRectangle draws the background. DrawRectangle draws the border. Note that the border rectangle must be smaller by one pixel than the e.Bounds rectangle, otherwise the right and bottom borders will not be drawn.

    If the current item is not selected, we test to see whether it is hot tracked. If it is, we draw in different colors. Otherwise we draw the default background with e.DrawBackground();.

    Then we draw the text over the background with DrawString.


    For all this to work, we also must invalidate regions of the listbox where the colors are changing. We detect changes in hot tracking in OnMouseMove and OnMouseLeave. There we set the HotTrackedIndex. This is a property which triggers drawing when necessary.

    In OnGotFocus and OnLostFocus we refresh the selected item to change its color depending on the focused state.


    My colors do not match your images, but you can easily tweak them. If you need to create Brushes and Pens in non-standard colors, then either create them as static and read-only or then don't forget to dispose them.

    private static readonly Brush HotTrackBrush = new SolidBrush(new Color(123, 45, 67));
    private static readonly Pen HotTrackPen = new Pen(new Color(234, 56, 78));
    

    An improved version of this listbox could expose the different selection and hot tracking colors as properties, so that you could easily change them in the properties window. (Properties automatically appear there.)