Figure 1: TabControl with Icons
Figure 2: TabControl with customized retreat/forward card buttons
Figure 3: TabControl with customized hover event
Figure 4: TabControl size changing with font
Introduction
There have been a bunch of articles about how to customize(repaint) a TabControl
by adding icons to it or changing its colors. I appreciate these authors because I got many useful ideas from them about how to deal with such problems. But after I tried to repaint Microsoft's TabControl
for about 2 days, I finally found that it is too hard to paint what I really want. I needed more interface to change the Control's color, selectcards' size and so on. It is very difficult to maintain a proper size of SelectCard
when the font changes. When the Cards' total length is bigger than the container, the two Cards' scroll buttons (retreat and forward buttons to show the selectcards which are hidden) are kind of ugly. So finally I made a Customized tabcontrol
by painting the Cards on plain UserControl
's panel and joining some panels together to represent the Tabpages.
By now, this Control has kind of solved my problems. I can change the colors, font, size, and Icons by giving some proper parameters.
Background
Since this is a Control inherited from panels, we have to write our own TabPages which is also inherited from panel, fire events, calculate the body's and selectcards' location and size, which is kind of complicated than repainting the tabcontrol, but it can help us to paint out what we really want: proper size, color, icons, font, etc.
Using the Control
By setting the CardFont
, IconImg
, and colors of the selectcards' colors, we can get our tabControl
paint out.
The selectcards' height is calculated with the Cards' text font.
myTabControl.CardFont = new Font("MS UI Gothic", 11F); .......
We give the background colors and cards' colors to paint our desired Tabs...
myTabControl.TabDeactiveBgColor1 = Color.YellowGreen; .......
We set the Icon's image.
MyTabPage p3 = new MyTabPage("TabPage3");
p3.IconImg = Image.FromFile(@"D:\Icon\ark_addfile.png");
myTabControl.Controls.Add(p3);
MyTabPage Inheriting from Plain Panel
Since we do not want to repaint the tabcontrol
, we have to construct a Control which represents the TabPages. Choosing panel will help us to get a relatively light feeling when we run the control... (forgive me for my poor English). In fact, this is just a simple panel which kind of loads us down with trivial details when we paint the whole tabControl
body...
public class MyTabPage : Panel{
Image imgIcon;
int iTabIndex;
public MyTabPage(string sName) {
this.Text = sName;
}
public MyTabPage() {
}
public Image IconImg {
set {
this.imgIcon = value;
}
get {
return this.imgIcon;
}
}
}
We need to add two event handlers to deal with the event that user adds MyTabpage
to MyTabControl
.
this.ControlAdded += new ControlEventHandler(MyTabControl_ControlAdded);
this.ControlRemoved += new ControlEventHandler(MyTabControl_ControlRemoved);
When mytabpage
is added to our control, we will add it to a List
which we maintain though the control's lifecycle.
private void MyTabControl_ControlAdded(object sender, ControlEventArgs e) {
if (e.Control is MyTabPage) {
MyTabPage mtpAdded = (MyTabPage)e.Control;
m_lTabPages.Add((MyTabPage)e.Control);
Invalidate();
}
}
private void MyTabControl_ControlRemoved(object sender, ControlEventArgs e) {
if (e.Control is MyTabPage) {
MyTabPage mtpAdded = (MyTabPage)e.Control;
m_lTabPages.Remove((MyTabPage)e.Control);
Invalidate();
}
}
Calculate MyTabControl Body's Location and Size
When we give up repainting Microsoft's tabcontrol, we have to calculate the location and size of the control's body and each selectCards...
We get our selectcards' height by measuring the font which you can set to the control...
m_iCardHeight = (int)g.MeasureString("0", m_fontCard).Height + 5;
Then, we can paint the body out ...
Rectangle TabArea = new Rectangle(4, m_iCardHeight + 7,
this.Size.Width - 8, this.Size.Height - m_iCardHeight - 11);
Brush br = new SolidBrush(SystemColors.Control);
g.FillRectangle(br, TabControlArea);
br.Dispose();
Paint the Selectcards
This part is quite like the other articles about repainting the tabcontrols. Just one difference is that we need to set a variable to represent the start point of our cards since we're going to modify this variable to move the hiding cards out.
We get our selectcards' height by measuring the font which you can set to the control.
if (nIndex == m_lTabPages.Count - 1) {
m_iStartPos = m_iStandardCardStartPoint;
} else {
m_iStartPos = recBounds.X + recBounds.Width;
}
And we need to mark down each selectcard's area rectangle to a dictionary so that we can respond to the select event or the cards' scroll event.
tabTextArea.Width = g.MeasureString(tabPage.Text, m_fontCard).Width;
tabTextArea.Height = g.MeasureString(tabPage.Text, m_fontCard).Height + 10;
recBounds.Height = m_iCardHeight + 5;
m_dicIndexRectTabs.Add(nIndex, recBounds);
Paint our Scroll Button
This is the hardest part of the code. Since I don't want the MouseHover
or MouseMove
event to load our control down, I add two small panels to deal with the hover and click event... then paint the button out at the same location where the panels are.
internal void DrawScrollButton(Rectangle recCardArea, Graphics g) {
int iSBtnSideLength = recCardArea.Height / 2 + 3;
m_recRetreat
= new Rectangle(recCardArea.X + recCardArea.Width - iSBtnSideLength * 2,
recCardArea.Y + recCardArea.Height - iSBtnSideLength + 3,
iSBtnSideLength, iSBtnSideLength);
m_recForward
= new Rectangle(recCardArea.X + recCardArea.Width - iSBtnSideLength,
recCardArea.Y + recCardArea.Height - iSBtnSideLength + 3,
iSBtnSideLength, iSBtnSideLength);
this.m_pnlRetreat.Size = m_recRetreat.Size;
this.m_pnlRetreat.Location = m_recRetreat.Location;
this.m_pnlForward.Size = m_recForward.Size;
this.m_pnlForward.Location = m_recForward.Location;
Brush br;
if (m_iStandardCardStartPoint == 5) {
br = new SolidBrush(Color.LightGray);
} else {
if (m_bRetreatMouseHovered) {
br = new SolidBrush(Color.LightCoral);
} else {
br = new SolidBrush(Color.FromArgb(((int)(((byte)(194)))),
((int)(((byte)(203)))), ((int)(((byte)(166))))));
}
}
g.FillRectangle(br, m_recRetreat);
foreach (int iCardIndex in m_dicIndexRectTabs.Keys) {
if (m_dicIndexRectTabs[iCardIndex].X == 5) {
int iNowCardsLength = 0;
for (int iLoop = iCardIndex; iLoop < m_dicIndexRectTabs.Count; iLoop++) {
iNowCardsLength += m_dicIndexRectTabs[iLoop].Width;
}
if (iNowCardsLength < this.Width - 10) {
br = new SolidBrush(Color.LightGray);
} else {
if (m_bForwardMouseHovered) {
br = new SolidBrush(Color.LightCoral);
} else {
br = new SolidBrush(Color.FromArgb(((int)(((byte)(194)))),
((int)(((byte)(203)))), ((int)(((byte)(166))))));
}
}
break;
}
}
g.FillRectangle(br, m_recForward);
br.Dispose();
Pen border = new Pen(Color.FromArgb(((int)(((byte)(234)))),
((int)(((byte)(243)))), ((int)(((byte)(206))))));
g.DrawRectangle(border, m_recRetreat);
g.DrawRectangle(border, m_recForward);
border.Dispose();
Point[] pt = new Point[3];
pt[0] = new Point(m_recRetreat.Right - 4, m_recRetreat.Top + 4);
pt[1] = new Point(m_recRetreat.Left + 4, m_recRetreat.Top + iSBtnSideLength / 2);
pt[2] = new Point(m_recRetreat.Right - 4, m_recRetreat.Top + iSBtnSideLength - 4);
g.DrawLines(new Pen(Color.White), pt);
pt[0] = new Point(m_recForward.Left + 4, m_recForward.Top + 4);
pt[1] = new Point(m_recForward.Right - 4, m_recForward.Top + iSBtnSideLength / 2);
pt[2] = new Point(m_recForward.Left + 4, m_recForward.Top + iSBtnSideLength - 4);
g.DrawLines(new Pen(Color.White), pt);
}
Cards' Location Changed When the Scroll Button is Clicked
When we click the scroll button, the cards' location should change to move out one card which is hidden left or right when the control's size is not long enough to contain so many selectcards...
private void dealCardForward() {
foreach (int iCardIndex in m_dicIndexRectTabs.Keys) {
if (m_dicIndexRectTabs[iCardIndex].X == 5) {
int iNowCardsLength = 0;
for (int iLoop = iCardIndex; iLoop < m_dicIndexRectTabs.Count; iLoop++) {
iNowCardsLength += m_dicIndexRectTabs[iLoop].Width;
}
if (iNowCardsLength < this.Width - 10) {
return;
}
m_iStandardCardStartPoint -= m_dicIndexRectTabs[iCardIndex].Width;
m_iStartPos = m_iStandardCardStartPoint;
break;
}
}
}
private void dealCardRetreat() {
foreach (int iCardIndex in m_dicIndexRectTabs.Keys) {
if (m_dicIndexRectTabs[iCardIndex].X == 5) {
if (iCardIndex == 0) {
return;
}
m_iStandardCardStartPoint += m_dicIndexRectTabs[iCardIndex - 1].Width;
m_iStartPos = m_iStandardCardStartPoint;
break;
}
}
}
Points of Interest
Sorry for the long code with poor English explanation. Maybe you will think that it's stupid to write a control adding all the events, attribute properties by ourselves. But when we just cannot get what we really want by repainting others' controls. writing one by ourselves is also an option, isn't it? Indeed this will cost us more time (this cost me about 2 days to write it out), but it will be easy to edit by ourselves. :)
I Have a Question to Ask
If you try this code, you will find that when you add or edit the tabpages it is kind of hard to bring out the tabpage you want to edit..... since this. I believe this is not like the usual control to use... Can anyone tell me how to solve this problem?????