The Problem
Now, Microsoft says right off for the BackColor property that "This member is not meaningful for this control." Why? Apparently they don't want developers monkeying with standard forms where the user should be able to change their Window's appearance through control panel.
Fair enough, but sometimes the app needs to look different. For instance, for our most recent update we have a background image; having the battleship gray across it doesn't look so pretty. The more annoying part is that in the IDE, the form looks perfect. I can't tell you how annoying it is that Microsoft knows what we want, because they put it right in the IDE, but when it's running, they say we can't have it. (Maybe they'll get it right for Windows 7.) ;)
Now the easiest solution is to allow an override for BackColor and BackgroundImage in the TabControl, but that would require Microsoft to realize the error of their ways. Instead, we will have to code around their "behavior by design". ;)
The Research
Initially, I found this post on setting the DrawMode to OwnerDrawFixed (I wonder if that means they admit the other is broken ;) ) It requires you to attach to the DrawItem event. This did lead me down the right path, but it only changed the tabs and then only their color.
The next post I found had a comment by LauraM that showed me how to change the extended part of the tab strip: the strip to the right of the tabs. This got me closer, but what I really needed to do was get the image from the parent and bring it through.
At this point it was all up to me. What I needed was more complex than what it appeared everyone else needed: I needed to have the underlying image or background color show through, depending on which was on the parent control, and if there were images on the tabs I needed those to show through as well. All this was in addition to setting the background color of the tabs to begin with.
The Solution
I started with the code that these posts brought me and altered it to make a more versatile setup. For this setup, there are three initial steps:
- Change the DrawMode property to OwnerDrawFixed.
- Attach to the DrawItem event. With the versatility of the method we will create, you will be able to use it for each tab control on the form.
- Create the method signatures.
(At this point if you just want code the entire method is at the bottom.)
Note that as soon as you change the DrawMode, the perfect TabControl you had in the IDE now becomes completely blank. It appears you can either have it work right in the IDE or in run mode, but not both. It's the kind of thrilling craziness that makes you glad you signed up for this career. ;)

The Code
As we want this to work for all our TabControls, we want two signatures: one that allows us to pass in an associated ImageList as needed, and one that doesn't require one. The one with fewer parameters will simply call the other with a null value:
/// <summary>Draws the specified tab control.</summary>
/// <param name="tabControl">The control on which to draw.</param>
/// <param name="e">The event arguments.</param>
private void DrawTabControlTabs(TabControl tabControl, DrawItemEventArgs e)
{
DrawTabControlTabs(tabControl, e, null);
}
/// <summary>Draws the specified tab control.</summary>
/// <param name="tabControl">The control on which to draw.</param>
/// <param name="e">The event arguments.</param>
/// <param name="images">The images for the tab control.</param>
private void DrawTabControlTabs(TabControl tabControl, DrawItemEventArgs e, ImageList images)
{
...
}
Now each of the TabControls that use this must have the DrawItem event filled out. These events do nothing more than call DrawTabControlTabs, passing the TabControl to draw, and the DrawItemEventArgs parameter that was passed to the event. Those that have icons, need to pass a third parameter of an ImageList.
/// <summary>Occurs when the items is drawn.</summary>
/// <param name="sender">The object that fired the event.
/// <param name="e">The arguments.</param>
private void tabSections_DrawItem(object sender, DrawItemEventArgs e)
{
DrawTabControlTabs(tabSections, e, this.icons);
}
Now, luckily, with everything that's going on it all fits in the one method. It's a bit longer than I typically like in a method (I love the nice short methods), but it's reasonable. The method consists of six basic steps.
Step 1: Get the bounding rectangle for the end of the tab strip
// Get the bounding end of tab strip rectangles.
Rectangle tabstripEndRect = tabControl.GetTabRect(tabControl.TabPages.Count - 1);
RectangleF tabstripEndRectF = new RectangleF(tabstripEndRect.X + tabstripEndRect.Width, tabstripEndRect.Y - 5,
tabControl.Width - (tabstripEndRect.X + tabstripEndRect.Width), tabstripEndRect.Height + 5);
Step 2: Fill the end using the parent's background image or color
Note that for using the background image we need to get the source coordinates on the parent control or form so we can pull the part of the image precisely under the area we want to draw it.
// First, do the end of the tab strip.
// If we have an image use it.
if (tabControl.Parent.BackgroundImage != null)
{
RectangleF src = new RectangleF(tabstripEndRectF.X + tabControl.Left, tabstripEndRectF.Y + tabControl.Top, tabstripEndRectF.Width, tabstripEndRectF.Height);
e.Graphics.DrawImage(tabControl.Parent.BackgroundImage, tabstripEndRectF, src, GraphicsUnit.Pixel);
}
// If we have no image, use the background color.
else
{
using (Brush backBrush = new SolidBrush(tabControl.Parent.BackColor))
{
e.Graphics.FillRectangle(backBrush, tabstripEndRectF);
}
}
Step 3: Pull the tab's color and name and apply the background color to the tab
Now it's worth noting at this point that the DrawItem event is fired for the drawing of each TabPage in the TabControl. Since this is a UI method (typically where the speed bottleneck is the user) it's not of great concern that in the previous steps we are drawing the parts of the TabControl that only need to be drawn once per control as many times as we have tabs. I did not see a speed issue with this and storing state may get a little nasty so I am leaving it. You may wish to do something differently. The code from this step on is for the tabs themselves.
// Set up the page and the various pieces.
TabPage page = tabControl.TabPages[e.Index];
Brush BackBrush = new SolidBrush(page.BackColor);
Brush ForeBrush = new SolidBrush(page.ForeColor);
string TabName = page.Text;
// Set up the offset for an icon, the bounding rectangle and image size and then fill the background.
int iconOffset = 0;
Rectangle tabBackgroundRect = e.Bounds;
e.Graphics.FillRectangle(BackBrush, tabBackgroundRect);
Step 4: Put any icons that exist on the tabs
Now this works whether you used an index or a key, but make sure you pass an ImageList to the method or this code won't get called.
// If we have images, process them.
if (images != null)
{
// Get sice and image.
Size size = images.ImageSize;
Image icon = null;
if (page.ImageIndex > -1)
icon = images.Images[page.ImageIndex];
else if (page.ImageKey != "")
icon = images.Images[page.ImageKey];
// If there is an image, use it.
if (icon != null)
{
Point startPoint = new Point(tabBackgroundRect.X + 2 + ((tabBackgroundRect.Height - size.Height) / 2),
tabBackgroundRect.Y + 2 + ((tabBackgroundRect.Height - size.Height) / 2));
e.Graphics.DrawImage(icon, new Rectangle(startPoint, size));
iconOffset = size.Width + 4;
}
}
Step 5: Spit out the text
// Draw out the label.
Rectangle labelRect = new Rectangle(tabBackgroundRect.X + iconOffset, tabBackgroundRect.Y + 3,
tabBackgroundRect.Width - iconOffset, tabBackgroundRect.Height - 3);
StringFormat sf = new StringFormat();
sf.Alignment = StringAlignment.Center;
e.Graphics.DrawString(TabName, e.Font, ForeBrush, labelRect, sf);
Step 6: Clean-up
//Dispose objects
sf.Dispose();
BackBrush.Dispose();
ForeBrush.Dispose();
The Fixed Form
Now here is the fixed form with the background image pulled through (and on other tabs the background color). Also icons show where appropriate.

The Method
Here is the full method. It's annoying as hell that we have to code this to make these things work, but there's not much to it. Happily this is fairly extensible supporting background image or backcolor bleed through and images by key or index. Hopefully if you were in the same boat I was, you will find this code helpful to the point you can quickly get on with the important parts of coding your project and not the workarounds.
private void DrawTabControlTabs(TabControl tabControl, DrawItemEventArgs e, ImageList images)
{
// Get the bounding end of tab strip rectangles.
Rectangle tabstripEndRect = tabControl.GetTabRect(tabControl.TabPages.Count - 1);
RectangleF tabstripEndRectF = new RectangleF(tabstripEndRect.X + tabstripEndRect.Width, tabstripEndRect.Y - 5,
tabControl.Width - (tabstripEndRect.X + tabstripEndRect.Width), tabstripEndRect.Height + 5);
// First, do the end of the tab strip.
// If we have an image use it.
if (tabControl.Parent.BackgroundImage != null)
{
RectangleF src = new RectangleF(tabstripEndRectF.X + tabControl.Left, tabstripEndRectF.Y + tabControl.Top, tabstripEndRectF.Width, tabstripEndRectF.Height);
e.Graphics.DrawImage(tabControl.Parent.BackgroundImage, tabstripEndRectF, src, GraphicsUnit.Pixel);
}
// If we have no image, use the background color.
else
{
using (Brush backBrush = new SolidBrush(tabControl.Parent.BackColor))
{
e.Graphics.FillRectangle(backBrush, tabstripEndRectF);
}
}
// Set up the page and the various pieces.
TabPage page = tabControl.TabPages[e.Index];
Brush BackBrush = new SolidBrush(page.BackColor);
Brush ForeBrush = new SolidBrush(page.ForeColor);
string TabName = page.Text;
// Set up the offset for an icon, the bounding rectangle and image size and then fill the background.
int iconOffset = 0;
Rectangle tabBackgroundRect = e.Bounds;
e.Graphics.FillRectangle(BackBrush, tabBackgroundRect);
// If we have images, process them.
if (images != null)
{
// Get sice and image.
Size size = images.ImageSize;
Image icon = null;
if (page.ImageIndex > -1)
icon = images.Images[page.ImageIndex];
else if (page.ImageKey != "")
icon = images.Images[page.ImageKey];
// If there is an image, use it.
if (icon != null)
{
Point startPoint = new Point(tabBackgroundRect.X + 2 + ((tabBackgroundRect.Height - size.Height) / 2),
tabBackgroundRect.Y + 2 + ((tabBackgroundRect.Height - size.Height) / 2));
e.Graphics.DrawImage(icon, new Rectangle(startPoint, size));
iconOffset = size.Width + 4;
}
}
// Draw out the label.
Rectangle labelRect = new Rectangle(tabBackgroundRect.X + iconOffset, tabBackgroundRect.Y + 3,
tabBackgroundRect.Width - iconOffset, tabBackgroundRect.Height - 3);
StringFormat sf = new StringFormat();
sf.Alignment = StringAlignment.Center;
e.Graphics.DrawString(TabName, e.Font, ForeBrush, labelRect, sf);
//Dispose objects
sf.Dispose();
BackBrush.Dispose();
ForeBrush.Dispose();
}



15 comments:
Super duper cool. 1000 Thanxs.
You're welcome. :)
How do you rotate the label text 90 degrees if your TabControl has the tabs on the right (Alignment=Right)?
I'm not sure exactly what you are looking for. If you want the labels right-aligned, you won't need to rotate the text. You will simply need to find the total width and subtract the length of the text.
Hope that helps.
Thank you for this code.
But, Is there another issue for coloring the edges of tabControl? Because the edges (the borders) are always gray.
Riccardo
I'm sure there is a way to draw the borders differently, however I have not tried anything on this score. I would guess there would be several things that could be added to this code, but the code itself should provide a good base of how to perform the draw and find the positioning.
Great article, and I share your frustration with having to jump through these hoops!
I have a related problem: how do I determine, from the TabControl::OnClick() event, which particular TabPage tab the mouse is in? (This is so that I can allow the user to remove just the SelectedTab - ie not the others - by clicking on it. And *this* is because the TabControl provides no generic tab UI Close box. More hoops.)
I checked out the code we used and we don't trap the click event, so I can't exactly say how it would work. I would guess you could simply query the SelectedTab property which should give you the information for which you are looking.
Another strange TabControl behavior in 2008: If you add a brand new Form, its BackColor property is set to Control by default (grey), then add a TabControl on it, the back color of all TabPages is white, even if their BackColor property is Transparent. Now if you set a TabPage's BackColor to any color then back to Transparent, the white background is gone, now it's really transparent, revealing the color of the Form. Does this make sense?
Another strange TabControl behavior in 2008: If you add a brand new Form, its BackColor property is set to Control by default (grey), then add a TabControl on it, the back color of all TabPages is white, even if their BackColor property is Transparent. Now if you set a TabPage's BackColor to any color then back to Transparent, the white background is gone, now it's really transparent, revealing the color of the Form. Does this make sense?
It seems to make sense. I've seen some weird behavior with the TabControl and that definitely seems familiar.
Hey thx mate, i followed up the comment/posting chain from the Forum. I'm totally new to VB and so i thought it couldn't be real that the tabcontainer could not be coloured. So, your way and the explanations of the Forum Users helped me out of the dark place.
Glad to hear it was helpful.
Thank you very much for providing a workaround for yet another annoying Microsoft "feature". Seems every week I stumble upon a new one.. sometimes it makes me wonder if the MS staff is made up of thousands of (poorly) trained monkeys, actually. Last week, it was the fact that Labels can be set to transparent background in the IDE, but of course it won't work runtime until you realise you have to explicitly set their parents (and that TextBoxes can't be transparent at all). The most disturbing bug I've found so far though is the fact that the Calendar.GetWeekOfYear function does NOT (contrary to what its documentation states) follow the ISO standard. Oh, and the one where you HAVE to set the current culture to en-US, or your Excel automation code will break during runtime. And don't forget about the fact that MS provide all sorts of fancy localisation and culture-specific stuff and pride themselves in following standards, yet they insist on Sunday being the first day of the week in the DayOfWeek struct. >:|
Well, enough ranting. Thanks again! :)
hehe. Welcome to Microsoft's development framework that is "by design". ;)
Post a Comment