Reputation: 299
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
Reputation:
Adding to @janhildebrandt answer because it is missing some crucial parts to actually make it work.
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;
}
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>();
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);
}
}
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();
}
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);
}
Using ControlStyles.OptimizedDoubleBuffer
might reduce flickering even further (if you have any).
public MyTabControl()
{
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
}
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
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
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
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
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.
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
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
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