Home > Enterprise >  How to apply modified C# WinForm ListBox border colour correctly when scroll bar appears
How to apply modified C# WinForm ListBox border colour correctly when scroll bar appears

Time:10-25

I've been asked to carry out some modernisation work on a C# WinForms application, and I'm using VS2019 with C#.Net 4.7.2.

The project owner wants to change the border colour of all the legacy Windows controls that were used originally. The initial idea was - using the MetroIT framework - to keep the original Windows controls in the Form Designer, but override them by defining new classes which extend the MetroIT equivalents but changing the class type of the declarations in the Designer's InitializeComponent() method.

That approach doesn't really work as a "drop-in" replacement because the MetroIT controls tend to subclass Control whereas existing code expects the legacy Windows properties/methods to be available.

Instead, therefore, I went down the route of trying to override the OnPaint method. That worked fine for CheckBox and RadioButton, but I'm now struggling to get it to work for a ListBox. The following is as far as I've got; it certainly isn't correct, as I'll explain, but it feels sort of close ?

public class MyListBox : ListBox
{
    public MyListBox()
    {
        SetStyle(ControlStyles.UserPaint, true);
        this.DrawMode = DrawMode.OwnerDrawVariable;
        BorderStyle = BorderStyle.None;
    }
    
    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        Brush textBrush = new SolidBrush(Color.Black);
        float y = 1;
        foreach (String strItem in Items)
        {
            e.Graphics.DrawString(strItem, DefaultFont, textBrush, 1, y);
            y  = DefaultFont.Height;
        }

        Rectangle borderRectangle = this.ClientRectangle;
        ControlPaint.DrawBorder(e.Graphics, borderRectangle, Color.Blue, ButtonBorderStyle.Solid);
    }
}

Initially, with no data in the ListBox, the control is drawn correctly.

enter image description here

However, as soon as a scroll bar appears, the base Windows code draws the scroll bar over the top of my border instead of just inside it (whereas an unmodified ListBox re-draws the border around the top, bottom and right-hand side of the scroll bar):

enter image description here

Is there a way to change my code so that the border draws itself around the edges of the scroll bar instead of excluding it ?

The other way in which my code is wrong is that, as soon as I start to scroll, the border painting code is applying itself to some of the ListBox items:

enter image description here

Is there a way I can complete this, or am I wasting my time because the base scroll bar cannot be modified ?

Thanks

EDIT 1:

Further to the suggestions of @GuidoG:

Yes, to be clear, I really only want to change the border to blue. Happy to leave the listbox items as they would ordinarily be painted WITHOUT any border.

So, to achieve that, I have removed your suggested code to do DrawItem, and now I only have the code to paint the border - but clearly I must be doing something wrong, because the listbox has now reverted to looking like the standard one with the black border.

So here's what I have now:

In my Dialog.Designer.cs InitializeComponent():

    this.FieldsListBox = new System.Windows.Forms.ListBox();
    this.FieldsListBox.FormattingEnabled = true;
    this.FieldsListBox.Location = new System.Drawing.Point(7, 98);
    this.FieldsListBox.Name = "FieldsListBox";
    this.FieldsListBox.Size = new System.Drawing.Size(188, 121);
    this.FieldsListBox.Sorted = true;
    this.FieldsListBox.TabIndex = 11;
    this.FieldsListBox.SelectedIndexChanged  = new System.EventHandler(this.FieldsListBox_SelectedIndexChanged);

In my Dialog.cs Form declaration:

public SelectByAttributesDialog()
{
    InitializeComponent();
    BlueThemAll(this);
}

private void BlueThemAll(Control parent)
{
    foreach (Control item in parent.Controls)
    {
        Blue(item);

        if (item.HasChildren)
            BlueThemAll(item);
    }
}

private void Blue(Control control)
{
    Debug.WriteLine("Blue: "   control.Name.ToString());
    Rectangle r = new Rectangle(control.Left - 1, control.Top - 1, control.Width   1, control.Height   1);

    using (Graphics g = control.Parent.CreateGraphics())
    {
        using (Pen selPen = new Pen(Color.Blue))
        {
            g.DrawRectangle(selPen, r);
        }
    }
}

I know that Blue() is being called for all the controls in this dialog, in the sense that the Debug.WriteLine() is outputting every control name, but no borders are being changed to blue (and certainly not the listbox).

I've been away from Windows Form programming for a very long time so I apologise for that, and I'm clearly doing something wrong, but have no idea what.

CodePudding user response:

Solution 1: let the form drawn blue borders around everything:

This means you dont have to subclass any controls, but you put some code on the form that will draw the borders for you around each control.
Here is an example that works for me

// method that draws a blue border around a control
private void Blue(Control control)
{
    Rectangle r = new Rectangle(control.Left - 1, control.Top - 1, control.Width   1, control.Height   1);

    using (Graphics g = control.Parent.CreateGraphics())
    {
        using (Pen selPen = new Pen(Color.Blue))
        {
            g.DrawRectangle(selPen, r);
        }
    }
}

// recursive method that finds all controls and call the method Blue on each found control
private void BlueThemAll(Control parent)
{
    foreach (Control item in parent.Controls)
    {
        Blue(item);

        if (item.HasChildren)
            BlueThemAll(item);
    }
}

where to call this ?

    // draw the blue borders when the form is resized
    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);

        panel3.Invalidate(); // on panel3 there is a control that moves position because it is anchored to the bottom
        Update();
        BlueThemAll(this);
    }

    // draw the borders when the form is moved, this is needed when the form moved off the screen and back
    protected override void OnMove(EventArgs e)
    {
        base.OnMove(e);

        BlueThemAll(this);
    }

    // draw the borders for the first time
    protected override void OnShown(EventArgs e)
    {
        base.OnShown(e);
        BlueThemAll(this);
    }

If you also want blue borders around each item of the listbox:

To get some border around each item in your listbox use the DrawItem event in stead of the paint event, maybe enter image description here

Solution 2: Subclassing the listbox:

If you need this in a subclassed listbox, then you can do this.
However, this will only work if the listbox has at least one pixel space left around it, because to get around the scrollbar you have to draw on the parent of the listbox, not on the listbox itself. The scrollbar will always draw itself over anything you draw there.

    public class myListBox : ListBox
    {
        public myListBox(): base()
        {
            this.DrawMode = DrawMode.OwnerDrawFixed;
            BorderStyle = BorderStyle.None;
            this.DrawItem  = MyListBox_DrawItem;
        }

        private void MyListBox_DrawItem(object sender, DrawItemEventArgs e)
        {
            e.DrawBackground();
            Brush myBrush = new SolidBrush(e.ForeColor);
            e.Graphics.DrawString(this.Items[e.Index].ToString(), e.Font, myBrush, e.Bounds, StringFormat.GenericDefault);
            e.DrawFocusRectangle();

            Rectangle r = new Rectangle(this.Left - 1, this.Top - 1, this.Width   2, this.Height   2);
            using (Graphics g = this.Parent.CreateGraphics())
            {
                ControlPaint.DrawBorder(g, r, Color.Blue, ButtonBorderStyle.Solid);
            }
        }
    }

and this will look like so

enter image description here

  • Related