Tom
Tom

Reputation: 299

How do I change background colour of tab control in Winforms?

Is there a way to change the background colour of a tab control in winforms, so that it does not have the white border around it?

I have tried a few different ways, but they all result in the same white border being displayed.

Upvotes: 25

Views: 77513

Answers (7)

user22191764
user22191764

Reputation:

Adding to @janhildebrandt answer because it is missing some crucial parts to actually make it work.

Properties

The DrawMode property of the TabControl has to be set to TabDrawMode.OwnerDrawFixed otherwise the DrawItem event handler won't fire.

Simply override the property in your derived TabControl class like this:

public new TabDrawMode DrawMode
{
    get
    {
        return TabDrawMode.OwnerDrawFixed;
    }
    set
    {
        // No you dont.
    }
}


public MyTabControl() 
{
    base.DrawMode = TabDrawMode.OwnerDrawFixed;
}

DrawItemEventArgs

I don't know in which Version the code was written but storing the Graphics object of the TabItem will not work in 2023 under .Net 4.5 and above and is also not needed.

Instead, consider using a struct like this:

private struct TabItemInfo
{
    public Color            BackColor;
    public Rectangle        Bounds;
    public Font             Font;
    public Color            ForeColor;
    public int              Index;
    public DrawItemState    State;

    public TabItemInfo(DrawItemEventArgs e)
    {
        this.BackColor  = e.BackColor;
        this.ForeColor  = e.ForeColor;
        this.Bounds     = e.Bounds;
        this.Font       = e.Font;
        this.Index      = e.Index;
        this.State      = e.State;
    }
}

private Dictionary<int, TabItemInfo> _tabItemStateMap = new Dictionary<int, TabItemInfo>();

DrawItem event handler

Don't assign an event handler when you are already deriving from the Control itself. Use the OnDrawItem(DrawItemEventArgs) method instead:

protected override void OnDrawItem(DrawItemEventArgs e)
{
    base.OnDrawItem(e);
    if (!_tabItemStateMap.ContainsKey(e.Index)) 
    {
        _tabItemStateMap.Add(e.Index, new TabItemInfo(e));
    }
    else
    {
        _tabItemStateMap[e.Index] = new TabItemInfo(e);
    }
}

Improvements in WndProc

Your TabControl will flicker in design mode.
This is easily avoided by also checking for the WM_ERASEBKGND message. Simply omit it during DesignMode:

private const int WM_PAINT      = 0x000F;
private const int WM_ERASEBKGND = 0x0014;        

// Cache context to avoid repeatedly re-creating the object.
// WM_PAINT is called frequently so it's better to declare it as a member.
private BufferedGraphicsContext _bufferContext = BufferedGraphicsManager.Current;

protected override void WndProc(ref Message m)
{            
    switch (m.Msg)
    {
        case WM_PAINT:
            {
                // Let system do its thing first.
                base.WndProc(ref m);
                
                // Custom paint Tab items.
                HandlePaint(ref m);

                break;
            }                    
        case WM_ERASEBKGND:
            {
                if (DesignMode)
                {
                    // Ignore to prevent flickering in DesignMode.
                }
                else
                {
                    base.WndProc(ref m);
                }
                break;
            }                    
        default:
            base.WndProc(ref m);
            break;
    }
}


private Color _backColor = Color.FromArgb(31, 31, 31);
[Browsable(true)]
[EditorBrowsable(EditorBrowsableState.Always)]
public new Color BackColor
{
    get
    {
        return _backColor;
    }
    set
    {
        _backColor = value;
    }
}


private void HandlePaint(ref Message m)
{
    using (var g = Graphics.FromHwnd(m.HWnd))
    {
        SolidBrush backBrush = new SolidBrush(BackColor);
        Rectangle r = ClientRectangle;
        using (var buffer = _bufferContext.Allocate(g, r))
        {                    
            if (Enabled)
            {
                buffer.Graphics.FillRectangle(backBrush, r);
            }
            else
            {
                buffer.Graphics.FillRectangle(backBrush, r);
            }

            // Paint items
            foreach (int index in _tabItemStateMap.Keys)
            {
                DrawTabItemInternal(buffer.Graphics, _tabItemStateMap[index]);
            }

            buffer.Render();
        }
        backBrush.Dispose();        
    }
}


private void DrawTabItemInternal(Graphics gr, TabItemInfo tabInfo) 
{
    /* Uncomment the two lines below to have each TabItem use the same height.
    ** The selected TabItem height will be slightly taller
    ** which makes unselected tabs float if you choose to 
    ** have a different BackColor for the TabControl background
    ** and your TabItem background. 
    */  
    
    // int fullHeight = _tabItemStateMap[this.SelectedIndex].Bounds.Height;
    // tabInfo.Bounds.Height = fullHeight;
    
    SolidBrush backBrush = new SolidBrush(BackColor);
    
    // Paint selected. 
    // You might want to choose a different color for the 
    // background or the text.
    if ((tabInfo.State & DrawItemState.Selected) == DrawItemState.Selected)
    {
        gr.FillRectangle(backBrush, tabInfo.Bounds);
        gr.DrawString(this.TabPages[tabInfo.Index].Text, tabInfo.Font, 
            SystemBrushes.ControlText, tabInfo.Bounds);
    }
    // Paint unselected.
    else
    {
        gr.FillRectangle(backBrush, tabInfo.Bounds);
        gr.DrawString(this.TabPages[tabInfo.Index].Text, tabInfo.Font, 
            SystemBrushes.ControlText, tabInfo.Bounds);
    }
    
    backBrush.Dispose();
}

Further improvements

SolidBrush

Instead of re-creating SolidBrush objects you might want to consider declaring them as members of your class.

Example:

private SolidBrush _backBrush;
private SolidBrush _tabBackBrush;
private SolidBrush _tabForeBrush;


private Color _tabBackColor = Color.FromArgb(31, 31, 31);
public Color TabBackColor
{
    get
    {
        return _tabBackColor;
    }
    set
    {
        _tabBackColor = value;
        _tabBackBrush?.Dispose();
        _tabBackBrush = new SolidBrush(_tabBackColor);
    }
}


private Color _tabForeColor = Color.FromArgb(241, 241, 241);
public Color TabForeColor
{
    get
    {
        return _tabForeColor;
    }
    set
    {
        _tabForeColor = value;
        _tabForeBrush?.Dispose();
        _tabForeBrush = new SolidBrush(_tabForeColor);
    }
}

private Color _backColor = Color.FromArgb(31, 31, 31);
[Browsable(true)]
[EditorBrowsable(EditorBrowsableState.Always)]
public new Color BackColor
{
    get
    {
        return _backColor;
    }
    set
    {
        _backColor = value;
        _backBrush?.Dispose();
        _backBrush = new SolidBrush(_backColor);
    }
}

protected override void Dispose(bool disposing)
{
    _backBrush.Dispose();
    _tabBackBrush.Dispose();
    _tabForeBrush.Dispose();
    
    base.Dispose(disposing);
}

SetStyle

Using ControlStyles.OptimizedDoubleBuffer might reduce flickering even further (if you have any).

public MyTabControl() 
{
    this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
}

Features

TextAlignment within the TabItem

Pass a StringFormat object when drawing the text of your TabItem to position the text like you do with a Label

private StringFormat _tabTextFormat = new StringFormat();


private void UpdateTextAlign()
{
    switch (this.TextAlign)
    {
        case ContentAlignment.TopLeft:
            _tabTextFormat.Alignment = StringAlignment.Near;
            _tabTextFormat.LineAlignment = StringAlignment.Near;
            break;
        case ContentAlignment.TopCenter:
            _tabTextFormat.Alignment = StringAlignment.Center;
            _tabTextFormat.LineAlignment = StringAlignment.Near;
            break;
        case ContentAlignment.TopRight:
            _tabTextFormat.Alignment = StringAlignment.Far;
            _tabTextFormat.LineAlignment = StringAlignment.Near;
            break;
        case ContentAlignment.MiddleLeft:
            _tabTextFormat.Alignment = StringAlignment.Near;
            _tabTextFormat.LineAlignment = StringAlignment.Center;
            break;
        case ContentAlignment.MiddleCenter:
            _tabTextFormat.Alignment = StringAlignment.Center;
            _tabTextFormat.LineAlignment = StringAlignment.Center;
            break;
        case ContentAlignment.MiddleRight:
            _tabTextFormat.Alignment = StringAlignment.Far;
            _tabTextFormat.LineAlignment = StringAlignment.Center;
            break;
        case ContentAlignment.BottomLeft:
            _tabTextFormat.Alignment = StringAlignment.Near;
            _tabTextFormat.LineAlignment = StringAlignment.Far;
            break;
        case ContentAlignment.BottomCenter:
            _tabTextFormat.Alignment = StringAlignment.Center;
            _tabTextFormat.LineAlignment = StringAlignment.Far;
            break;
        case ContentAlignment.BottomRight:
            _tabTextFormat.Alignment = StringAlignment.Far;
            _tabTextFormat.LineAlignment = StringAlignment.Far;
            break;
    }
}


private ContentAlignment _textAlign = ContentAlignment.TopLeft;
public ContentAlignment TextAlign
{
    get
    {
        return _textAlign;
    }
    set
    {
        if (value != _textAlign)
        {
            _textAlign = value;
            UpdateTextAlign();
        }                
    }
}


private void DrawTabItemInternal(Graphics gr, TabItemInfo tabInfo) 
{
    if ((tabInfo.State & DrawItemState.Selected) == DrawItemState.Selected)
    {
        gr.FillRectangle(_tabBackBrush, tabInfo.Bounds);
        gr.DrawString(this.TabPages[tabInfo.Index].Text, tabInfo.Font, 
            _tabForeBrush, tabInfo.Bounds, _tabTextFormat);
    }
    else
    {
        gr.FillRectangle(_tabBackBrush, tabInfo.Bounds);
        gr.DrawString(this.TabPages[tabInfo.Index].Text, tabInfo.Font, 
            _tabForeBrush, tabInfo.Bounds, _tabTextFormat);
    }
}

Upvotes: 2

lou34964
lou34964

Reputation: 1

Unfortunately, the back color property is handled when the control is drawn. My suggestion is to do what I have done and create a user control to mimic the tab controller.

I used a menu strip as the tabs and had a second user control docked as fill to the parent user control. In the second user control, I was able to add whatever I needed for said tab.

The part that is harder with it is that you have to build all the functionality to make it work as a tab control.

Upvotes: 0

Scott Pearson
Scott Pearson

Reputation: 1

Drop a Panel on top of (not inside) the tab control and set the color in the properties. Call Panelx.Hide() and Panelx.Show() as needed.

Upvotes: 0

Jan Hildebrandt
Jan Hildebrandt

Reputation: 31

First of all you need to make a deriving class from TabControl. So far so good but now it gets dirty.

Because TabControl won't call OnPaint, we have do override WndProc to handle the WM_PAINT message. In there we go ahead and paint our background with the color we like.

 protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);
        if(m.Msg == (int) WindowsMessages.Win32Messages.WM_PAINT)
        {
            using (Graphics g = this.CreateGraphics())
            {
                //Double buffering stuff...
                BufferedGraphicsContext currentContext;
                BufferedGraphics myBuffer;
                currentContext = BufferedGraphicsManager.Current;
                myBuffer = currentContext.Allocate(g,
                   this.ClientRectangle);

                Rectangle r = ClientRectangle;

                //Painting background
                if(Enabled)
                    myBuffer.Graphics.FillRectangle(new SolidBrush(_backColor), r);
                else
                    myBuffer.Graphics.FillRectangle(Brushes.LightGray, r);

                //Painting border
                r.Height = this.DisplayRectangle.Height +1; //Using display rectangle hight because it excludes the tab headers already
                r.Y = this.DisplayRectangle.Y - 1; //Same for Y coordinate
                r.Width -= 5;
                r.X += 1;

                if(Enabled)
                    myBuffer.Graphics.DrawRectangle(new Pen(Color.FromArgb(255, 133, 158, 191), 1), r);
                else
                    myBuffer.Graphics.DrawRectangle(Pens.DarkGray, r);

                myBuffer.Render();
                myBuffer.Dispose();

                //Actual painting of items after Background was painted
                foreach (int index in ItemArgs.Keys)
                {
                    CustomDrawItem(ItemArgs[index]);
                }

            }
        }    
    }

Im doing further drawing in this method so it looks a little overkill for this problem but just ignore the unnecessary stuff. Also notice the foreach loop. I'll come to this later.

The Problem is that TabControl paints its items (the tab headers) before its own WM_PAINT so our background will be drawn on top, which renders them invisible. To solve this I made an EventHandler for DrawItem which looks as the following:

    private void DrawItemHandler(object sender, DrawItemEventArgs e)
    {
        //Save information about item in dictionary but dont do actual drawing
        if (!ItemArgs.ContainsKey(e.Index))
            ItemArgs.Add(e.Index, e);
        else
            ItemArgs[e.Index] = e;
    }

I am saving the DrawItemEventArgs into a dictionary (which is called "ItemArgs" in my case) so I can access them later. Thats where the foreach from a few seconds ago comes into play. It calls a method where I am painting the tab headers which takes the DrawItemEventArgs which we saved before as a parameter to paint the items in correct state and position.

So, in a nutshell we are intercepting the Drawing of tab headers to delay it until we are finished drawing the background.

This solution is not optimal but it works and its the only thing you can do to get more control over TabControl (lol) without painting it from scratch.

Upvotes: 3

SherrillL
SherrillL

Reputation: 125

Easier still (IMO): add a paint handler to the TabPage (not the top level TabControl, but the TabPage(s) within it, then paint the background rectangle in the color you want.

  1. Either in the designer or "by hand", add a Paint event handler to the TabPage:

    Page1.Paint += tabpage_Paint; // custom paint event so we get the backcolor we want
    
  2. In the paint method, paint the page rectangle the color you want (in my case, I want it to follow the standard BackColor):

    // force the tab background to the current BackColor
    private void tabpage_Paint(object sender, PaintEventArgs e)
    {
        SolidBrush fillBrush = new SolidBrush(BackColor);
    
        e.Graphics.FillRectangle(fillBrush, e.ClipRectangle);
    }
    

Upvotes: 0

user203570
user203570

Reputation:

TabControl has very poor support for customization. I've used this custom tab control with good success. The code is pretty usable if you want to change the look as I did.

Upvotes: 5

Bek Raupov
Bek Raupov

Reputation: 3777

I can only think of changing Appearance property to Buttons

MSDN TabControl Appearance

Upvotes: 3

Related Questions