Home > other >  ListBox Change a Specific Item ForeColor
ListBox Change a Specific Item ForeColor

Time:05-03

I'm struggling in creating customized functionality for a listbox. I need my ListBox to change the Forecolor of a specific ("marked") item. Not to be confused with Selected Item.

The Functionality Required: Let's say the ListBox Collection Contains Several File Names.
When I double click an Item; that item index and object are stored into two variables (index and object).
These variables would then be used to set the Item ForeColor (when Listbox Item is Unselected).
Remaining Items and Item Rectangle should be drawn with default properties (in this case they have their own color properties to allow further customization).

My problems:

  • Not Painting upon Load
  • String is Drawn with "Weird Characters"
  • When Selecting an Item; I'm drawing the "Marked" Item.

I'm really confused. MSDN documentation is not very clear how to achieve this; nor how & when the DrawItem Event Occurs.

I've been messing with several alternatives; however deleted everything and got back to the current code attempting to understand the logic and behaviour.

1st Attempt code (Original Question Code):

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Custom_Controls.Controls
{
    internal class MyPlaylist : ListBox
    {
        public MyPlaylist()
        {
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

            // Enable ListBox Customized Design
            DrawMode = DrawMode.OwnerDrawVariable;
            //SelectionMode = SelectionMode.MultiExtended;
        }

        #region <Custom Properties>
        private int markedIndex = -1;
        public int MarkedIndex
        {
            get { return markedIndex; }
            set { markedIndex = value; Invalidate(); }
        }

        private object markedItem = string.Empty;
        public object MarkedItem
        {
            get { return markedItem; }
            set
            {
                markedItem = value;
                Invalidate();
            }
        }

        private Color markedItemForeColor = Color.Red;
        public Color MarkedItemForeColor
        {
            get { return markedItemForeColor; }
            set { markedItemForeColor = value; Invalidate(); }
        }


        private Color markedItemBackColor = Color.DimGray;
        public Color MarkedItemBackColor
        {
            get { return markedItemBackColor; }
            set { markedItemBackColor = value; Invalidate(); }
        }


        private Color selectionBackColor = Color.DeepSkyBlue;
        public Color SelectionBackColor
        {
            get { return selectionBackColor; }
            set { selectionBackColor = value; Invalidate(); }
        }


        private Color selectionForeColor = Color.White;
        public Color SelectionForeColor
        {
            get { return selectionForeColor; }
            set { selectionForeColor = value; Invalidate(); }
        }
        #endregion


        
    protected override void OnDrawItem(DrawItemEventArgs e) // When Selected?
    {
        e.DrawBackground();
        e.DrawFocusRectangle();

        //// Improve Graphic Quality and Pixel Precision
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
        e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
        e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;

        
        using (var defaultForeBrush = new SolidBrush(Color.White))
        using (var markForeBrush = new SolidBrush(markedItemForeColor))
        {
            // Iterate over all the items
            for (int i = 0; i < Items.Count; i  )
            {
                var item = Items[i];

                // Draw "Marked" Item
                if (i == markedIndex)
                { 
                    e.Graphics.DrawString(Items[markedIndex].ToString(), e.Font, markForeBrush, e.Bounds, StringFormat.GenericDefault); 
                }

                // Draw Remaining Items
                else
                {
                    e.Graphics.DrawString(item.ToString(), e.Font, markForeBrush, e.Bounds, StringFormat.GenericDefault);
                }

                // Draw Selection Rectangle
                // ...
            }
        }
    }        



        #region <Overriden Events>
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
        }

        protected override void OnDoubleClick(EventArgs e)
        {
            base.OnDoubleClick(e);

            SetMarkedItem();
        }

        protected override void onm ouseDoubleClick(MouseEventArgs e)
        {
            base.OnMouseDoubleClick(e);

            SetMarkedItem();
        }


        #region <Methods>
        private void SetMarkedItem()
        {
            markedIndex = SelectedIndex;
            markedItem = SelectedItem;
        } 
        #endregion
    }
}

My 2nd Attempt using Jimi's Help (Current Code)

Changes:

  • I've Commented the Pinvoke LB_ Enums and WndProc as I was not able to make it work.
  • In order to keep simplicity: I removed the ternary operators (however I loved the way Jimi's code would alternate the colors with them).
  • SetMarker() was reverted to previous version. The Marked Item was never Drawn that way.
  • Custom Properties were not providing their value to the Brushes; therefore they were temporarily removed.

Current Issues: WndProc definitly needs to be reimplemented to clear the Drawing (Marked Item); and perhaps to Redraw the Control so it Updates the Marker ASAP.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Custom_Controls.Controls
{
    internal class MyPlaylist : ListBox
    {
        #region <Constructor>
        public MyPlaylist()
        {
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

            DrawMode = DrawMode.OwnerDrawVariable;

            BackColor = Color.FromArgb(255, 25, 25, 25);
            ForeColor = Color.White;
            BorderStyle = BorderStyle.FixedSingle;
        }
        #endregion


        #region <Fields>
        //private const int LB_RESETCONTENT = 0x0184;
        //private const int LB_DELETESTRING = 0x0182;

        //TextFormatFlags flags = TextFormatFlags.PreserveGraphicsClipping | TextFormatFlags.LeftAndRightPadding | TextFormatFlags.VerticalCenter;
        #endregion


        #region <Custom Properties>
        // Tip: Always Verify that the new Values are Different from the Old Ones

        private int markedIndex = -1;
        public int MarkedIndex
        {
            get { return markedIndex; }
            set
            {
                if (value != markedIndex)
                {
                    markedIndex = value;
                    Invalidate();
                }
            }
        }

        // Read-only: just return the marked Item, set it using the Index only
        public object MarkedItem
        {
            get { return Items[markedIndex]; }
        }
        #endregion


        #region <Overriden Events>
        protected override void OnDrawItem(DrawItemEventArgs e)
        {
            if (Items.Count == 0) return;

            // Draw Selection:
            if (e.State.HasFlag(DrawItemState.Focus) || e.State.HasFlag(DrawItemState.Selected))
            {
                using (var brush = new SolidBrush(Color.FromArgb(255, 52, 52, 52)))
                {
                    // Background Rectangle
                    e.Graphics.FillRectangle(brush, e.Bounds);

                    // Item Text : Marked Item
                    if (e.Index == markedIndex)
                    {
                        TextRenderer.DrawText(e.Graphics, GetItemText(Items[e.Index]), Font, e.Bounds, Color.Red, flags);
                    }
                    
                    // Other Items (Except Marked)
                    else
                    {
                        TextRenderer.DrawText(e.Graphics, GetItemText(Items[e.Index]), Font, e.Bounds, Color.White, flags);
                    }
                }
            }

            // Draw Unselected:
            else
            {
                using (var brush = new SolidBrush(BackColor))
                using (var markedBrush = new SolidBrush(Color.Khaki))
                {
                    e.Graphics.FillRectangle(brush, e.Bounds);
                    TextRenderer.DrawText(e.Graphics, GetItemText(Items[e.Index]), Font, e.Bounds, Color.White, flags);
                }

                // Draw (Unselected) Marked Item
                if (markedIndex > -1 && e.Index == markedIndex)
                {
                    TextRenderer.DrawText(e.Graphics, GetItemText(Items[e.Index]), Font, e.Bounds, Color.Red, flags);
                }
            }

            e.DrawFocusRectangle();

            base.OnDrawItem(e);
        }

        // Set the Height of the Item (Width: only if needed).
        // This is the Standard Value (Modify as required)
        protected override void OnMeasureItem(MeasureItemEventArgs e)
        {
            if (Items.Count > 0)
            {
                e.ItemHeight = Font.Height   4; // 4 = Text vs Item Rectangle Margin
            }

            base.OnMeasureItem(e);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
        }

        protected override void OnDoubleClick(EventArgs e)
        {
            base.OnDoubleClick(e);

            SetMarkedItem();
        }

        protected override void onm ouseDoubleClick(MouseEventArgs e)
        {
            base.OnMouseDoubleClick(e);

            if (e.Button == MouseButtons.Left)
            {
                SetMarkedItem();
            }
        }
        #endregion

        #region <Methods>
        /// <summary>
        /// WndProc is Overridden in order to Intercept the LB_RESETCONTENT (sent when the ObjectCollection is cleared);<br/> 
        /// and the LB_DELETESTRING (sent when an Item is removed).
        /// This is done to reset the marked Item when the list is cleared or the marked Item is removed (otherwise an Item will remain marked when it shouldn't).
        /// </summary>
        /// <param name="m"></param>
        //protected override void WndProc(ref Message m)
        //{
        //    switch (m.Msg)
        //    {
        //        // List Cleared
        //        case LB_RESETCONTENT:
        //            markedIndex = -1;
        //            break;

        //        // Item Deleted
        //        case LB_DELETESTRING:
        //            if (markedIndex == m.WParam.ToInt32())
        //            {
        //            markedIndex = -1;
        //            }
        //            break;
        //    }
        //}

        private void SetMarkedItem()    // Current Block
        {
            markedIndex = SelectedIndex;
        }

        // Previous Code Line (By Jimmy; using Ternary Operator) <----------------------------------------
        //private void SetMarkedItem() => MarkedIndex = markedIndex == SelectedIndex ? -1 : SelectedIndex;
        #endregion
    }
}

Helpful Related Content

ListBox : Pinvoke LB_(Enums)

How to add multiline Text to a ListBox item

CodePudding user response:

This sample class contains the adjustments needed to make the List work as the standard ListBox, but with the enhancements described in the question.
See also the comments in code.

  • TextRenderer.DrawText() replaces Graphics.DrawString(): this will give a more natural aspect to the rendered list items. No need to use anti-aliasing.
  • OnMeasureItem is also overridden, to provide a custom Height (optionally the Width - when strictly required) for the Items. It's set to ListBox.Font.Height 4 (pretty standard); modify as needed.
  • OnDrawItem() is corrected to handle both the custom Selection colors and the marked Item's colors. Note that this method is called once per Item, so you don't have to loop the entire collection each time, just paint the current Item with correct colors.
  • SetMarkedItem() is modified to toggle the state of a marked Item, in case you double-click it twice.
  • WndProc is overridden to intercept LB_RESETCONTENT (sent when the ObjectCollection is cleared) and LB_DELETESTRING (sent when an Item is removed). This is done to reset the marked Item when the list is cleared or the marked Item is removed (otherwise an Item will remain marked when it shouldn't).
  • When setting a Property value, always verify that the new value is not equal to the old one, before you call Invalidate() (or any other method - or a Property setter) for no reason.
  • A few minor changes, see the code.

public class MyPlaylist : ListBox {

    private const int LB_DELETESTRING = 0x0182;
    private const int LB_RESETCONTENT = 0x0184;

    TextFormatFlags flags = TextFormatFlags.PreserveGraphicsClipping |
                            TextFormatFlags.LeftAndRightPadding |
                            TextFormatFlags.VerticalCenter;

    public MyPlaylist()
    {
        SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
        DrawMode = DrawMode.OwnerDrawVariable;
    }

    protected override void WndProc(ref Message m)
    {
        switch (m.Msg) {
            // List cleared
            case LB_RESETCONTENT:
                markedIndex = -1;
                break;
            // Item deleted
            case LB_DELETESTRING:
                if (markedIndex == m.WParam.ToInt32()) {
                    markedIndex = -1;
                }
                break;
        }
        base.WndProc(ref m);
    }

    private int markedIndex = -1;
    public int MarkedIndex {
        get => markedIndex;
        set {
            if (value != markedIndex) {
                markedIndex = value;
                Invalidate();
            }
        }
    }

    // Read-only: just return the marked Item, set it using the Index only
    public object MarkedItem {
        get => Items[markedIndex];
    }

    // Always verify that the new value is different from the old one
    private Color markedItemForeColor = Color.Orange;
    public Color MarkedItemForeColor {
        get => markedItemForeColor;
        set { 
            if (value != markedItemForeColor) {
                markedItemForeColor = value;
                Invalidate();
            }
        }
    }

    private Color markedItemBackColor = Color.DimGray;
    public Color MarkedItemBackColor {
        get => markedItemBackColor;
        set { 
            if (value != markedItemBackColor) {
                markedItemBackColor = value;
                Invalidate();
            }
        }
    }

    private Color selectionBackColor = Color.DeepSkyBlue;
    public Color SelectionBackColor {
        get => selectionBackColor;
        set { 
            if (value != selectionBackColor) {
                selectionBackColor = value;
                Invalidate();
            }
        }
    }

    private Color selectionForeColor = Color.White;
    public Color SelectionForeColor {
        get => selectionForeColor;
        set { 
            if (value != selectionForeColor) {
                selectionForeColor = value;
                Invalidate();
            }
        }
    }

    // Use TextRenderer to draw the Items - no anti-aliasing needed
    protected override void OnDrawItem(DrawItemEventArgs e)
    {
        if (Items.Count == 0) return;
        if (e.State.HasFlag(DrawItemState.Focus) || e.State.HasFlag(DrawItemState.Selected)) {
            using (var brush = new SolidBrush(selectionBackColor)) {
                e.Graphics.FillRectangle(brush, e.Bounds);
            }
            TextRenderer.DrawText(e.Graphics, GetItemText(Items[e.Index]), Font, e.Bounds, selectionForeColor, flags);
        }
        else {
            var color = markedIndex != -1 && markedIndex == e.Index ? markedItemBackColor : BackColor;
            using (var brush = new SolidBrush(color)) {
                e.Graphics.FillRectangle(brush, e.Bounds);
            }
            var foreColor = markedIndex == e.Index ? markedItemForeColor : ForeColor;
            TextRenderer.DrawText(e.Graphics, GetItemText(Items[e.Index]), Font, e.Bounds, foreColor, flags);
        }
        e.DrawFocusRectangle();
        base.OnDrawItem(e);
    }

    // Set the Height (the Width only if needed) of the Item
    // This is the standard value, modify as required
    protected override void OnMeasureItem(MeasureItemEventArgs e)
    {
        if (Items.Count > 0) {
            e.ItemHeight = Font.Height   4;
        }
        base.OnMeasureItem(e);
    }

    protected override void onm ouseDoubleClick(MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left) {
            SetMarkedItem();
        }
        base.OnMouseDoubleClick(e);
    }

    private void SetMarkedItem() => MarkedIndex = markedIndex == SelectedIndex ? -1 : SelectedIndex;
}
  • Related