How do you change the color of the border on a group box?

1,289

Solution 1

Building on the previous answer, a better solution that includes the label for the group box:

groupBox1.Paint += PaintBorderlessGroupBox;

private void PaintBorderlessGroupBox(object sender, PaintEventArgs p)
{
  GroupBox box = (GroupBox)sender;
  p.Graphics.Clear(SystemColors.Control);
  p.Graphics.DrawString(box.Text, box.Font, Brushes.Black, 0, 0);
}

You might want to adjust the x/y for the text, but for my use this is just right.

Solution 2

Just add paint event.

    private void groupBox1_Paint(object sender, PaintEventArgs e)
    {
        GroupBox box = sender as GroupBox;
        DrawGroupBox(box, e.Graphics, Color.Red, Color.Blue);
    }


    private void DrawGroupBox(GroupBox box, Graphics g, Color textColor, Color borderColor)
    {
        if (box != null)
        {
            Brush textBrush = new SolidBrush(textColor);
            Brush borderBrush = new SolidBrush(borderColor);
            Pen borderPen = new Pen(borderBrush);
            SizeF strSize = g.MeasureString(box.Text, box.Font);
            Rectangle rect = new Rectangle(box.ClientRectangle.X,
                                           box.ClientRectangle.Y + (int)(strSize.Height / 2),
                                           box.ClientRectangle.Width - 1,
                                           box.ClientRectangle.Height - (int)(strSize.Height / 2) - 1);

            // Clear text and border
            g.Clear(this.BackColor);

            // Draw text
            g.DrawString(box.Text, box.Font, textBrush, box.Padding.Left, 0);

            // Drawing Border
            //Left
            g.DrawLine(borderPen, rect.Location, new Point(rect.X, rect.Y + rect.Height));
            //Right
            g.DrawLine(borderPen, new Point(rect.X + rect.Width, rect.Y), new Point(rect.X + rect.Width, rect.Y + rect.Height));
            //Bottom
            g.DrawLine(borderPen, new Point(rect.X, rect.Y + rect.Height), new Point(rect.X + rect.Width, rect.Y + rect.Height));
            //Top1
            g.DrawLine(borderPen, new Point(rect.X, rect.Y), new Point(rect.X + box.Padding.Left, rect.Y));
            //Top2
            g.DrawLine(borderPen, new Point(rect.X + box.Padding.Left + (int)(strSize.Width), rect.Y), new Point(rect.X + rect.Width, rect.Y));
        }
    }

Solution 3

Just set the paint action on any object (not just buttons) to this method to draw a border.

    private void UserControl1_Paint(object sender, PaintEventArgs e)
    {
        ControlPaint.DrawBorder(e.Graphics, this.ClientRectangle, Color.Red, ButtonBorderStyle.Solid);

    }

It still wont be pretty and rounded like the original, but it is much simpler.

Solution 4

FWIW, this is the implementation I used. It's a child of GroupBox but allows setting not only the BorderColor, but also the thickness of the border and the radius of the rounded corners. Also, you can set the amount of indent you want for the GroupBox label, and using a negative indent indents from the right side.

using System;
using System.Drawing;
using System.Windows.Forms;

namespace BorderedGroupBox
{
    public class BorderedGroupBox : GroupBox
    {
        private Color _borderColor = Color.Black;
        private int _borderWidth = 2;
        private int _borderRadius = 5;
        private int _textIndent = 10;

        public BorderedGroupBox() : base()
        {
            InitializeComponent();
            this.Paint += this.BorderedGroupBox_Paint;
        }

        public BorderedGroupBox(int width, float radius, Color color) : base()
        {
            this._borderWidth = Math.Max(1,width);
            this._borderColor = color;
            this._borderRadius = Math.Max(0,radius);
            InitializeComponent();
            this.Paint += this.BorderedGroupBox_Paint;
        }

        public Color BorderColor
        {
            get => this._borderColor;
            set
            {
                this._borderColor = value;
                DrawGroupBox();
            }
        }

        public int BorderWidth
        {
            get => this._borderWidth;
            set
            {
                if (value > 0)
                {
                    this._borderWidth = Math.Min(value, 10);
                    DrawGroupBox();
                }
            }
        }

        public int BorderRadius
        {
            get => this._borderRadius;
            set
            {   // Setting a radius of 0 produces square corners...
                if (value >= 0)
                {
                    this._borderRadius = value;
                    this.DrawGroupBox();
                }
            }
        }

        public int LabelIndent
        {
            get => this._textIndent;
            set
            {
                this._textIndent = value;
                this.DrawGroupBox();
            }
        }

        private void BorderedGroupBox_Paint(object sender, PaintEventArgs e) =>
            DrawGroupBox(e.Graphics);

        private void DrawGroupBox() =>
            this.DrawGroupBox(this.CreateGraphics());

        private void DrawGroupBox(Graphics g)
        {
            Brush textBrush = new SolidBrush(this.ForeColor);
            SizeF strSize = g.MeasureString(this.Text, this.Font);

            Brush borderBrush = new SolidBrush(this.BorderColor);
            Pen borderPen = new Pen(borderBrush,(float)this._borderWidth);
            Rectangle rect = new Rectangle(this.ClientRectangle.X,
                                            this.ClientRectangle.Y + (int)(strSize.Height / 2),
                                            this.ClientRectangle.Width - 1,
                                            this.ClientRectangle.Height - (int)(strSize.Height / 2) - 1);

            Brush labelBrush = new SolidBrush(this.BackColor);

            // Clear text and border
            g.Clear(this.BackColor);

            // Drawing Border (added "Fix" from Jim Fell, Oct 6, '18)
            int rectX = (0 == this._borderWidth % 2) ? rect.X + this._borderWidth / 2 : rect.X + 1 + this._borderWidth / 2;
            int rectHeight = (0 == this._borderWidth % 2) ? rect.Height - this._borderWidth / 2 : rect.Height - 1 - this._borderWidth / 2;
            // NOTE DIFFERENCE: rectX vs rect.X and rectHeight vs rect.Height
            g.DrawRoundedRectangle(borderPen, rectX, rect.Y, rect.Width, rectHeight, (float)this._borderRadius);

            // Draw text
            if (this.Text.Length > 0)
            {
                // Do some work to ensure we don't put the label outside
                // of the box, regardless of what value is assigned to the Indent:
                int width = (int)rect.Width, posX;
                posX = (this._textIndent < 0) ? Math.Max(0-width,this._textIndent) : Math.Min(width, this._textIndent);
                posX = (posX < 0) ? rect.Width + posX - (int)strSize.Width : posX;
                g.FillRectangle(labelBrush, posX, 0, strSize.Width, strSize.Height);
                g.DrawString(this.Text, this.Font, textBrush, posX, 0);
            }
        }

        #region Component Designer generated code
        /// <summary>Required designer variable.</summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>Clean up any resources being used.</summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
                components.Dispose();

            base.Dispose(disposing);
        }

        /// <summary>Required method for Designer support - Don't modify!</summary>
        private void InitializeComponent() => components = new System.ComponentModel.Container();
        #endregion
    }
}

To make it work, you also have to extend the base Graphics class (Note: this is derived from some code I found on here once when I was trying to create a rounded-corners Panel control, but I can't find the original post to link here):

static class GraphicsExtension
{
    private static GraphicsPath GenerateRoundedRectangle(
        this Graphics graphics,
        RectangleF rectangle,
        float radius)
    {
        float diameter;
        GraphicsPath path = new GraphicsPath();
        if (radius <= 0.0F)
        {
            path.AddRectangle(rectangle);
            path.CloseFigure();
            return path;
        }
        else
        {
            if (radius >= (Math.Min(rectangle.Width, rectangle.Height)) / 2.0)
                return graphics.GenerateCapsule(rectangle);
            diameter = radius * 2.0F;
            SizeF sizeF = new SizeF(diameter, diameter);
            RectangleF arc = new RectangleF(rectangle.Location, sizeF);
            path.AddArc(arc, 180, 90);
            arc.X = rectangle.Right - diameter;
            path.AddArc(arc, 270, 90);
            arc.Y = rectangle.Bottom - diameter;
            path.AddArc(arc, 0, 90);
            arc.X = rectangle.Left;
            path.AddArc(arc, 90, 90);
            path.CloseFigure();
        }
        return path;
    }

    private static GraphicsPath GenerateCapsule(
        this Graphics graphics,
        RectangleF baseRect)
    {
        float diameter;
        RectangleF arc;
        GraphicsPath path = new GraphicsPath();
        try
        {
            if (baseRect.Width > baseRect.Height)
            {
                diameter = baseRect.Height;
                SizeF sizeF = new SizeF(diameter, diameter);
                arc = new RectangleF(baseRect.Location, sizeF);
                path.AddArc(arc, 90, 180);
                arc.X = baseRect.Right - diameter;
                path.AddArc(arc, 270, 180);
            }
            else if (baseRect.Width < baseRect.Height)
            {
                diameter = baseRect.Width;
                SizeF sizeF = new SizeF(diameter, diameter);
                arc = new RectangleF(baseRect.Location, sizeF);
                path.AddArc(arc, 180, 180);
                arc.Y = baseRect.Bottom - diameter;
                path.AddArc(arc, 0, 180);
            }
            else path.AddEllipse(baseRect);
        }
        catch { path.AddEllipse(baseRect); }
        finally { path.CloseFigure(); }
        return path;
    }

    /// <summary>
    /// Draws a rounded rectangle specified by a pair of coordinates, a width, a height and the radius
    /// for the arcs that make the rounded edges.
    /// </summary>
    /// <param name="brush">System.Drawing.Pen that determines the color, width and style of the rectangle.</param>
    /// <param name="x">The x-coordinate of the upper-left corner of the rectangle to draw.</param>
    /// <param name="y">The y-coordinate of the upper-left corner of the rectangle to draw.</param>
    /// <param name="width">Width of the rectangle to draw.</param>
    /// <param name="height">Height of the rectangle to draw.</param>
    /// <param name="radius">The radius of the arc used for the rounded edges.</param>
    public static void DrawRoundedRectangle(
        this Graphics graphics,
        Pen pen,
        float x,
        float y,
        float width,
        float height,
        float radius)
    {
        RectangleF rectangle = new RectangleF(x, y, width, height);
        GraphicsPath path = graphics.GenerateRoundedRectangle(rectangle, radius);
        SmoothingMode old = graphics.SmoothingMode;
        graphics.SmoothingMode = SmoothingMode.AntiAlias;
        graphics.DrawPath(pen, path);
        graphics.SmoothingMode = old;
    }

    /// <summary>
    /// Draws a rounded rectangle specified by a pair of coordinates, a width, a height and the radius
    /// for the arcs that make the rounded edges.
    /// </summary>
    /// <param name="brush">System.Drawing.Pen that determines the color, width and style of the rectangle.</param>
    /// <param name="x">The x-coordinate of the upper-left corner of the rectangle to draw.</param>
    /// <param name="y">The y-coordinate of the upper-left corner of the rectangle to draw.</param>
    /// <param name="width">Width of the rectangle to draw.</param>
    /// <param name="height">Height of the rectangle to draw.</param>
    /// <param name="radius">The radius of the arc used for the rounded edges.</param>

    public static void DrawRoundedRectangle(
        this Graphics graphics,
        Pen pen,
        int x,
        int y,
        int width,
        int height,
        int radius)
    {
        graphics.DrawRoundedRectangle(
            pen,
            Convert.ToSingle(x),
            Convert.ToSingle(y),
            Convert.ToSingle(width),
            Convert.ToSingle(height),
            Convert.ToSingle(radius));
    }
}

Solution 5

I'm not sure this applies to every case, but thanks to this thread, we quickly hooked into the Paint event programmatically using:

GroupBox box = new GroupBox();
[...]
box.Paint += delegate(object o, PaintEventArgs p)
{
    p.Graphics.Clear(someColorHere);
};

Cheers!

Share:
1,289
jpillora
Author by

jpillora

Updated on August 08, 2021

Comments

  • jpillora
    jpillora almost 3 years

    I'm using the MVC model in my flex project.

    What I'd like is to bind a value object's class properties to a view mxml, then change that view by altering the value object.

    What happens:

    1. Set the selected value to 'c' - index 2
    2. Add 'x,y,z,' before 'c'
    3. Hit enter -> now index 5
    4. Hit enter -> now index is -1
    5. See 4.

    Why does only the first update work ? I know I'm probably missing something obvious...

    Edit: Running Example

    (P.S. first post and im not sure how to turn on MXML highlighting)

    <?xml version="1.0" encoding="utf-8"?>
    <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
                   xmlns:s="library://ns.adobe.com/flex/spark" 
                   xmlns:mx="library://ns.adobe.com/flex/mx" 
                   creationComplete="created(event)"
                   width="160" height="220">
    
        <fx:Script>
            <![CDATA[
                import mx.collections.ArrayList;
                import mx.events.FlexEvent;
    
                import spark.events.IndexChangeEvent;
    
                //===================================
                //     Pretend Value Object Class
                [Bindable] private var list:ArrayList = null;
                [Bindable] private var index:int = 0;
                //===================================
    
                protected function created(event:FlexEvent):void {
                    ddValues.addEventListener(FlexEvent.ENTER, update);
                    update();
                }
    
                private function update(... args):void {
                    //note selected item
    
                    trace("dropdown index: " + dd.selectedIndex);
                    var s:String = dd.selectedItem as String;
                    trace("selected item: " + s);
                    //build new list from csv
                    list = new ArrayList(ddValues.text.split(","));
                    trace("new list: " + ddValues.text);
                    trace("selected item: " + s);
                    //if exists in new list, set value object index
                    var newIndex:int = 0;
                    if(list)
                    list.toArray().forEach(function(ss:String, i:int, a:Array):void { 
                        if(s == ss) newIndex = i;; 
                    });
                    index = newIndex;
                    trace("new index: " + index + "  (dropdown index: " + dd.selectedIndex + ")");
                    trace("===");
                }
    
    
                protected function ddChange(event:IndexChangeEvent):void
                {
                    trace("selected item: " + (dd.selectedItem as String) + "  (dropdown index: " + dd.selectedIndex + ")");
                    trace("===");
                }
    
            ]]>
        </fx:Script>
        <s:Panel width="100%" height="100%" title="Drop Down Bug">
            <s:layout>
                <s:VerticalLayout gap="10" paddingLeft="10" paddingTop="10" paddingRight="10" paddingBottom="10"/>
            </s:layout>
            <s:DropDownList id="dd" dataProvider="{list}" selectedIndex="@{index}" change="ddChange(event)"></s:DropDownList>
            <s:Label text="Label: {dd.selectedItem as String}" paddingTop="5" paddingBottom="5"/>
            <s:Label text="Code Index: {index}" paddingTop="5" paddingBottom="5"/>
            <s:Label text="DropDown Index: {dd.selectedIndex}" paddingTop="5" paddingBottom="5"/>
            <s:TextInput id="ddValues" text="a,b,c,d,e"/>
        </s:Panel>
    </s:Application>
    

    And heres the output Edited code and added traces. Heres the output that shows my problem:

    dropdown index: -1
    selected item: null
    new list: a,b,c,d,e
    selected item: null
    new index: 0  (dropdown index: 0)
    ===
    selected item: c  (dropdown index: 2)
    ===
    dropdown index: 2
    selected item: c
    new list: a,b,x,y,z,c,d,e
    selected item: c
    new index: 5  (dropdown index: 5)
    ===
    dropdown index: 5
    selected item: c
    new list: a,b,x,y,z,c,d,e
    selected item: c
    new index: 5  (dropdown index: 5)
    ===
    dropdown index: -1
    selected item: null
    new list: a,b,x,y,z,c,d,e
    selected item: null
    new index: 0  (dropdown index: 0)
    ===
    
    • Amy Blankenship
      Amy Blankenship over 12 years
      I read the statexment x100->null about 10 times, and I have no idea what it means. Do you have a variable called x100? I didn't see it defined.
    • Pranav Hosangadi
      Pranav Hosangadi over 12 years
      @AmyBlankenship Hit enter again x100 would mean hit enter again 100 times :)
    • JeffryHouser
      JeffryHouser over 12 years
      You can't really daisy chain binding. So, you can bind to an object; but not that objects properties. It most likely works the first time due to grabbing the value on initial setup. You could have your object dispatch 'change' events for each property. Listen for that event and change the view that way.
    • Amy Blankenship
      Amy Blankenship over 12 years
      So when you hit enter (100 times), exactly what is null at that point?
    • jpillora
      jpillora over 12 years
      Yeah. I mean't all subsequent times you run update() sorry about that.
    • jpillora
      jpillora over 12 years
      Edited for clarity. Also changed my 'GetItemIndex' to an 'iterate,compare and save index', same result though.
    • jpillora
      jpillora over 12 years
      @www.Flextras.com I didn't think I was daisy chaining bindings: 'index' <--two-way-bind--> dropdown index | 'list' one-way-bind--> dropdown list ? (edit: didn't realise there was no formatting in comments)
  • Kodiak
    Kodiak over 12 years
    Hi! you wrote "now null somehow...", but what do the other traces display (selected item, list...)?
  • jpillora
    jpillora over 12 years
    "the selected item will be null, because the item that was selected was in the old list" I checked for that, before and after, I print 's' and it remains that same - because Strings are copied by value not by reference, correct ?
  • jpillora
    jpillora over 12 years
    And yes, I think I have failed at MVC haha. Though I thought that was idea of flex binding, to bind the model to to view, but the logic that determines the values inside the model is done by the controller ?
  • jpillora
    jpillora over 12 years
    "It does not have write access to the model" So essentially, two way binding breaks MVC ? Based off what you wrote, altering the value of 'index' causing a change to the drop down is valid, though altering 'index' by manually changing the drop down is invalid ?
  • Amy Blankenship
    Amy Blankenship over 12 years
    When the dataprovider is completely replaced, none of the items within it are selected. The selectedItem was in a different ListCollectionView, and hence there is no way to know that the exact same string in a totally new ListCollectionView was the one you wanted selected. So the selectedItem will be right the first time, but on subsequent updates it should be null.
  • Amy Blankenship
    Amy Blankenship over 12 years
    Two-way binding does not, in itself, break MVC. For instance, if a View has a subcomponent which is changed based on the properties of the main View and the subcomponent changes view State, then you might well use two-way binding to propogate that change in state back to the main component's properties.
  • Amy Blankenship
    Amy Blankenship over 12 years
    MVC means that you have a strict deliniation between the responsibilities of each component. The model is strictly there as a data store, and has no knowledge of the View or Controller. The View only displays data and requests changes to it. It only gets the exact data it needs to display or enable edits to. The Controller makes changes to the data based on the requests by the View. Sometimes Controllers know about Views, sometimes they don't, depending on the implementation.
  • jpillora
    jpillora over 12 years
    Sorry for slow reply Looks like what I want won't work with binding, I've got it working with manually creating binding functions using the FlexEvent.VALUE_COMMIT event. Thanks for the clarification, I'll keep those things in mind.
  • Yousaf
    Yousaf over 12 years
    Thanks @Mick Bruno, you saved me some serious time :)
  • dwo
    dwo over 10 years
    Thx! To remove the border, use box.Parent.BackColor, as I did.
  • A.Pissicat
    A.Pissicat about 7 years
    Is there a way to thicken the border and round the rectangle ?
  • 4mla1fn
    4mla1fn almost 6 years
    two questions: 1) i gather i can only use BorderedGroupBox for programmatically-built forms; i.e. not something built with the IDE; and 2) what is graphics.GenerateCapsule()?
  • NetXpert
    NetXpert almost 6 years
    1) I use these controls in Visual Studio's designer without any problems... 2) lol -- I don't think I've ever used that much radius, didn't see the reference so didn't copy the code for it (woops!):
  • 4mla1fn
    4mla1fn almost 6 years
    thanks brett. for other newbies, i found this link which explained how to create a custom control: blackwasp.co.uk/vscontrolintoolbox.aspx . quick and easy process. i'll enjoy using this control.
  • Jim Fell
    Jim Fell over 5 years
    Added GroupBox groupBox = (GroupBox)sender; so that same paint event can be used to redraw borders on all group boxes the same.
  • Jim Fell
    Jim Fell over 5 years
    Fixed for drawing wide borders... g.DrawRoundedRectangle(borderPen, (0 == this._borderWidth % 2) ? rect.X + this._borderWidth / 2 : rect.X + 1 + this._borderWidth / 2, rect.Y, rect.Width - this._borderWidth, (0 == this._borderWidth % 2) ? rect.Height - this._borderWidth / 2 : rect.Height - 1 - this._borderWidth / 2, (float)this._borderRadius);
  • NetXpert
    NetXpert over 5 years
    @JimFell -- thanks! I've applied your suggestion to the code above (with attribution!). :)
  • DCOPTimDowd
    DCOPTimDowd over 4 years
    @A.Pissicat The second parameter for the Pen is width. Add that to change the thickness.
  • compound eye
    compound eye almost 3 years
    I added a tweak to Jim Fell's code that worked a little better for me, it was too long to add as a comment so I have posted it below