1

I have a custom ComboBox in a C# WinForms application, and I have been successful in overriding the background color when its both enabled and disabled, and overriding the border when its enabled, but when the control is disabled I cannot figure out how to change the color of its border.

Currently, I am catching the WndProc messages being sent to the custom control, something like this:

protected override void WndProc (ref Message m)
{
    base.WndProc(ref m);

    if (m.Msg == WM_PAINT)
    {
        // set the enabled border color here, and it works
        using (var g = Graphics.FromHwnd(Handle)
        {
            using (var p = new Pen(Color.Black))
            {
                g.DrawRectangle(p, 0, 0, Width - 1, Height - 1);
            }
        }
    }
    if (m.Msg == WM_CTLCOLORSTATIC)
    {
        // set the disabled background color here
    ]
    if (m.Msg == WM_NCPAINT)
    {
        // try to set the disabled border color here, but its not working
        using (var g = Graphics.FromHwnd(Handle)
        {
            using (var p = new Pen(Color.Black))
            {
                g.DrawRectangle(p, 0, 0, Width - 1, Height - 1);
            }
        }
    }
}

Here are some images to help understand the problem, I have chosen colors that specifically highlight the problem area, these are not the actual colors I would use:

Enabled comboBox:

enter image description here

Disabled comboBox:

enter image description here

Notice the thick SystemGrey border being applied when the comboBox is disabled. This is what I want to remove, it looks worse on older windows systems, these screenshots were taken on Windows 10 but I'm targeting Windows Server 2012 where it produces a strange "halo" type effect that extends outside the control.

Looking at the MSDN, it seems like WM_NCPAINT is the message I want, but stepping through the code the border appears to have already been drawn at this point. I also tried looking at the MSDN for WM_CTLCOLORSTATIC, since the name seems promising, but it doesnt seem like it triggers anything but background colors to be set.

Is there another message I should be looking at, or am I approaching this the wrong way? I tried stepping through and looking at each message, but I just cant tell which one is triggering the call for the border to be set.

Edit: See below for the solution, this is a quick and dirty example of what I wanted to achieve, and what the solution code will do:

enter image description here

Unicorno Marley
  • 1,744
  • 1
  • 14
  • 17
  • [Change ComboBox Border Color](https://stackoverflow.com/a/34886006/3110834). It works well with enabled or disabled control. – Reza Aghaei Jan 25 '20 at 08:55
  • It *doesnt* work, and the reason you dont see it in that post is because youre using the default background color. All you've done is paint a border inside the control on the disabled control. If you switch that background color to any other color but default, you will see the disabled control has a big fat default form color border around it. This is unique to the comboBox control and does not appear on any other type of form control. – Unicorno Marley Jan 25 '20 at 19:20
  • I see where the confusion is here and its my fault. I tried to reproduce this on a fresh form and could not get the same results. Then I realised on my live project, I've set the control to flat style. The flat style border is not preserved through being disabled, you get a "Standard" style border. So that lack of information is whats caused the confusion. I will update the title of the question. You still need to draw 3 rectangles to cover it though. – Unicorno Marley Jan 25 '20 at 21:39
  • No need to handle `WM_CTLCOLORSTATIC`. Just adjustments and drawing inner rectangle. Updated the [linked post](https://stackoverflow.com/a/34886006/3110834) to handle different combinations of `DropDownStyle`, `FLatStyle`, `Enabled`. If you want to have a perfect implementation, you need to do what is done in [`ComboBox.FlatComboAdapter`](http://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/ComboBox.cs,79ca61e52b2766da). – Reza Aghaei Jan 25 '20 at 22:30
  • You need to handle WM_CTLCOLORSTATIC if you want to change the background color of the disabled combo box *without* using DropDownList style which is not suitable for my purpose (I need freetype in the boxes as well as list choices). Notice your DropDown - Flat style list has no background color, if you set it in WM_CTLCOLORSTATIC you will see the same results as the images I posted, thick system color border. Thank you for the link for the break apart of the class, i see the section that causes this, and it will be handy to have in the future if I need to override other things. – Unicorno Marley Jan 25 '20 at 22:48
  • As you can see in the screenshot, all `DropDownStyle` (DropDown/DropDownList) as well as all `Enabled` (true/false) as well as all `FlatStyle` (Flat, Popup, System, Standard) are supported. – Reza Aghaei Jan 25 '20 at 22:51
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/206642/discussion-between-unicorno-marley-and-reza-aghaei). – Unicorno Marley Jan 25 '20 at 22:54
  • I'll rollback the linked post to keep it simple for future readers and post the changes here as answer. – Reza Aghaei Jan 26 '20 at 05:46

2 Answers2

1

This answer relies on Change ComboBox Border Color in Windows Forms.

This post added a few criteria to remove the white inner border from the control when the control have DropDown or DropDownList as DropDownStyle and in all FlatStyle values and both for enabled and disables. This is done by handling WM_PAINT message and drawing the outer and inner border for combo box, like following image.

For demo purpose, all the controls in the image have BackColor = Color.Black and BorderColor = Color.Red and you can see their DropDownStyle, FlatStyle and Enabled as their selected item:

enter image description here

using System;
using System.Drawing;
using System.Windows.Forms;
public class FlatCombo : ComboBox
{
    private const int WM_PAINT = 0xF;
    private int buttonWidth = SystemInformation.HorizontalScrollBarArrowWidth;
    Color borderColor = Color.Blue;
    public Color BorderColor
    {
        get { return borderColor; }
        set { borderColor = value; Invalidate(); }
    }
    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);
        if (m.Msg == WM_PAINT && DropDownStyle != ComboBoxStyle.Simple)
        {
            using (var g = Graphics.FromHwnd(Handle))
            {
                var adjustMent = 0;
                if (FlatStyle == FlatStyle.Popup ||
                   (FlatStyle == FlatStyle.Flat &&
                   DropDownStyle == ComboBoxStyle.DropDownList))
                    adjustMent = 1;
                var innerBorderWisth = 3;
                var innerBorderColor = BackColor;
                if (DropDownStyle == ComboBoxStyle.DropDownList &&
                    (FlatStyle == FlatStyle.System || FlatStyle == FlatStyle.Standard))
                    innerBorderColor = Color.FromArgb(0xCCCCCC);
                if (DropDownStyle == ComboBoxStyle.DropDown && !Enabled)
                    innerBorderColor = SystemColors.Control;

                if (DropDownStyle == ComboBoxStyle.DropDown || Enabled == false)
                {
                    using (var p = new Pen(innerBorderColor, innerBorderWisth))
                    {
                        p.Alignment = System.Drawing.Drawing2D.PenAlignment.Inset;
                        g.DrawRectangle(p, 1, 1, 
                            Width - buttonWidth - adjustMent - 1, Height - 1);
                    }
                }
                using (var p = new Pen(BorderColor))
                {
                    g.DrawRectangle(p, 0, 0, Width - 1, Height - 1);
                    g.DrawLine(p, Width - buttonWidth - adjustMent, 
                        0, Width - buttonWidth - adjustMent, Height);
                }
            }
        }
    }
}

Note:

To learn more about how to render a flat combo box, you can take a look at source code of internal ComboBox.FlatComboAdapter class of .Net Framework.

If you see flickers, for a flicker-free solution you can use BeginPaint and EndPaint. Or as a workaround, in your form add the following code:

private const int WS_EX_COMPOSITED = 0x02000000;
protected override CreateParams CreateParams
{
    get
    {
        var c = base.CreateParams;
        c.ExStyle |= WS_EX_COMPOSITED;
        return c;
    }
}
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
0

Edit: since there seems to be some confusion about why the various other answers out there are not appropriate, I will explain the issue. When a combobox is disabled, the framework generates a 3 pixel wide border around it, using the default windows background color of "Grey". If you are using the default windows form background color, you will never see this border, and simply drawing over the 1 pixel wide black "border" (its not actually the border, its part of the control area) will solve the problem. However, if your form background is, for example, Black, you will see this default form color border. This is what the below solution solves, and I hope this helps someone.

Turns out sometimes you just need to step away for 10 minutes, I figured out how to do this. Its probably not ideal, and its definitely not graceful, but it works.

You can in fact draw a new border while handling the WM_CTLCOLORSTATIC message, i thought you couldnt because the rectangle is so small, but it does in fact get drawn, and while this is definitely not the message thats handling the original border drawing, this comes after it so you can draw right over top.

This code will draw over top of the existing light grey border with a dark grey border:

if (m.Msg == WM_CTLCOLORSTATIC)
{
    // set your other colors for disabled controls

    using (var g = Graphics.FromHwnd(Handle)
    {
        using (var p = new Pen(Color.FromArgb(93, 100, 103))
        {
            // its a fat border so we need to draw 3 rectangles to cover it
            g.DrawRectangle(p, 0, 0, Width - 1, Height - 1);
            g.DrawRectangle(p, 1, 1, Width - 3, Height - 3);
            g.DrawRectangle(p, 2, 2, Width - 5, Height - 5);
        }
    }
]

I know this is disgusting, but I am on hour 4 of trying to draw a border around a control... so if anyone has a cleaner solution, I am more than open to accepting it.

Unicorno Marley
  • 1,744
  • 1
  • 14
  • 17
  • 1
    You just need `if (m.Msg == WM_PAINT | m.Msg == WM_NCPAINT) { }` and use the ClientRectangle as the measure, not the Width and Height of the Control. e.g., `var border = new Rectangle(Point.Empty, new Size(this.ClientRectangle.Width - 1, this.ClientRectangle.Height - 1));`. This, of course, if you want to include the ComboBox Button. Otherwise, subtract `SystemInformation.HorizontalScrollBarArrowWidth` from the Rectangle width. – Jimi Jan 24 '20 at 22:48
  • That is all true for an /enabled/ combo box, but not true for a /disabled/ combo box. And ClientRectangle is not the same size as the big fat border that gets painted around the disabled control. I am doing most of what you said to deal with it in its enabled state. WM_NCPAINT gets called after this one very unique border gets drawn, I tried handling this through that message, with no luck. – Unicorno Marley Jan 24 '20 at 22:57
  • 2
    That works in both enabled and disabled states. It's not exactly a *new thing*. You should also double-check what `WM_CTLCOLORSTATIC` is used for. The ComboBox and its Edit control are not the same thing. – Jimi Jan 24 '20 at 23:00
  • It didnt work for me, its in the original post, I very clearly tried that. This is not the only control I have overridden for custom styling in this project, and trust me, a disabled comboBox is very unique in the way the framework is dealing with its styling. The fact it draws a triple wide border around it in its disabled state, while no other control does that, is one tell-tale sign. A single rectangle will not cover the disabled-state border. Enabled state, yes, it works, and that is how I did it. – Unicorno Marley Jan 24 '20 at 23:06
  • 1
    In the code you posted, it doesn't work because you're using the Control's Width and Height, not the ClientRectangle. Try what I suggested and you'll see (not a *new thing*). What you consider a *triple-border*, is the Edit control inside the Combo. – Jimi Jan 24 '20 at 23:09
  • 1
    BTW, you have to set `m.Result = IntPtr.Zero;` when you process those messages. – Jimi Jan 24 '20 at 23:15
  • You need to set it to whatever the message is expecting. Its not the same for every message, just sending back 0 is not a good choice. – Unicorno Marley Jan 24 '20 at 23:29
  • 1
    [WM_PAINT](https://learn.microsoft.com/en-us/windows/win32/gdi/wm-paint) and [WM_NCPAINT](https://learn.microsoft.com/en-us/windows/win32/gdi/wm-ncpaint) are meant to return `0` when processed. It's not *my choice*. See the Remarks section of both. – Jimi Jan 24 '20 at 23:35