1

I want my TabControl in a Windows Forms app to look something like this: https://technet.microsoft.com/en-us/library/dn531164.aspx

I have an app that has process flow and each TabPage is a process phase that I want to represent with these nice looking arrows. I know about OnPaint and System.Drawing, but I cannot make those tabs look decent.

I tried to handle TabControl.DrawItem event and to draw an arrow, but I am not satisfied with the look.

private void tabControl1_DrawItem(object sender, DrawItemEventArgs e)
{
    Rectangle rect = e.Bounds;

    int offset = 10;
    Point p1 = e.Bounds.Location;
    Point p2 = new Point(e.Bounds.X + e.Bounds.Width - offset, e.Bounds.Y);
    Point p3 = new Point(e.Bounds.X + e.Bounds.Width, e.Bounds.Y + (e.Bounds.Height / 2));
    Point p4 = new Point(p2.X, e.Bounds.Bottom);
    Point p5 = new Point(e.Bounds.X, e.Bounds.Y+e.Bounds.Height);
    Point p6 = new Point(e.Bounds.X + offset, p3.Y);

    GraphicsPath path = new GraphicsPath();

    path.AddLine(p1, p2);
    path.AddLine(p2, p3);
    path.AddLine(p3, p4);
    path.AddLine(p4, p5);
    path.AddLine(p5, p6);
    path.AddLine(p6, p1);
    e.Graphics.FillPath(Brushes.Black, path);
};

Is there any other approach to make this work as described?

Konamiman
  • 49,681
  • 17
  • 108
  • 138
vpetrovic
  • 527
  • 4
  • 22
  • I doubt that you can get rid of the divider bars. So it will never get that cool interleaved look, even when you draw the tabs correctly, ie with the overlapping triangles in the right color. - I'm afraid you need to overlay the whole tab by a custom drawn panel and catch the mouseclick, mapping them on the tab.. – TaW Jul 02 '15 at 14:33
  • I found this nice article: http://www.codeproject.com/Articles/91387/Painting-Your-Own-Tabs-Second-Edition and put my code in TabStyleChromeProvider.AddTabBorder method. Result is very promissing. I would like to get some help with this. I think we can make this style look like those process flow arrows. – vpetrovic Jul 02 '15 at 15:04
  • Interesting but I don't think I will get into it.. Sounds like it does so much more than what you actually need and uses so much more lines of code.. - Did you have a look at my code example..? – TaW Jul 02 '15 at 22:13

1 Answers1

1

Sometimes I go crazy and pick up a challenge just for fun :-)

Here is the result:

enter image description here enter image description here

I overlay the TabControl tabControl1 with a Panel tp. (Do pick better names!) Note that this must happen rather late or else the TabControl will pop to the top.

I call them to order in the Shown event:

private void Form1_Shown(object sender, EventArgs e)
{
    tp.BringToFront();
    tabControl1.SendToBack();
}

You probably could create the Panel in the designer to avoid these motions..

In addition to the Panel I need a List<GraphicsPath> which is used both for drawing and for precise hit testing..:

Panel tp = null;
List<GraphicsPath> tabAreas = new List<GraphicsPath>();

You can call this function to prepare both the Panel and the List:

void makeTabPanel(TabControl tab)
{
    tp = new Panel();
    tp.Size = new Size(tab.Width, tab.ItemSize.Height);
    tp.Paint += tp_Paint;
    tp.MouseClick += tp_MouseClick;
    tp.Location =  tab.Location;
    tab.Parent.Controls.Add(tp);

    int tabs = tabControl1.TabPages.Count;
    float w = tabControl1.Width / tabs;
    float h = tp.Size.Height;
    float y0 = 0;    float y1 = h / 2f;    float y2 = h;
    float d = 5;          //  <--- this is the gap
    float e = 8;          //  <- this is the extrusion
    float w1 = w - d;
    tabAreas = new List<GraphicsPath>();
    for (int t = 0; t < tabs; t++)
    {
        int t1 = t + 1;
        float e1 = t == 0 ? 0 : e;    // corrections for start and end..
        float e2 = t == tabs - 1 ? 0 : e;
        float e3 = t == tabs - 1 ? d : 0;
        List<PointF> points = new List<PointF>();
        points.Add(new PointF(t * w, y0));
        points.Add(new PointF(t1 * w - d + e3, y0));
        points.Add(new PointF(t1 * w -d + e2 + e3, y1));
        points.Add(new PointF(t1 * w- d + e3, y2));
        points.Add(new PointF(t * w, y2));
        points.Add(new PointF(t * w + e1, y1));
        GraphicsPath gp = new GraphicsPath(FillMode.Alternate);
        gp.AddPolygon(points.ToArray());
        tabAreas.Add(gp);
    }
}

After this the actual events are rather simple:

void tp_MouseClick(object sender, MouseEventArgs e)
{
    for (int t = 0; t < tabAreas.Count; t++)
    {
        if (tabAreas[t].IsVisible(e.Location) )
           { tabControl1.SelectedIndex = t; break;}
    }
    tp.Invalidate();
}

void tp_Paint(object sender, PaintEventArgs e)
{
    StringFormat fmt = new StringFormat() 
      { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };

    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.Clear(Color.White);   // **
    float w = tp.Width / tabAreas.Count;
    Size sz = new System.Drawing.Size( (int)w, e.ClipRectangle.Height);
    for (int t = 0; t < tabAreas.Count; t++)
    {
        Rectangle rect = new Rectangle((int)(t * w ), 0, sz.Width, sz.Height);            
        bool selected = tabControl1.SelectedIndex == t ;
        Brush brush = selected ?  Brushes.DarkGoldenrod : Brushes.DarkGray;   // **
        e.Graphics.FillPath(brush, tabAreas[t]);

        e.Graphics.DrawString(tabControl1.TabPages[t].Text,
                tabControl1.Font, Brushes.White, rect, fmt);
    }
}

Pick your colors here (**)

Update: Using TextRenderer as Lars suggests works just as well. (At least ;-)

TextFormatFlags flags = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter ;
TextRenderer.DrawText(e.Graphics, tabControl1.TabPages[t].Text,
    tabControl1.Font, rect, Color.White, flags);

enter image description here

TaW
  • 53,122
  • 8
  • 69
  • 111
  • DrawString? You know [TextRenderer](https://msdn.microsoft.com/en-us/library/system.windows.forms.textrenderer(v=vs.110).aspx) replaced it (except for printing). – LarsTech Jul 02 '15 at 22:37
  • Oh well, 'replace' is a bit exaggerated. DrawString works just fine. I just did a pixel by pixel comparison for the case at hand and am not so sure I prefer the slightly wider results of TextRenderer. (See for yourself!) - But it does have more options, so, certainly an improvement to keep in mind, expecially because of the greatly expanded TextFormatFlags. – TaW Jul 02 '15 at 23:14
  • Hi TaW! Wow according to the photo this looks amazing! Thanks for this! I just cannot make this work at my simple test form. I put TabControl with 3 tabs, called makeTabPanel after InitializeComponent, then added Shown event handler, but the panel never shows and Paint is never called, so Im seeing classic TabControl. Another thing: "sz" variable is not declared, I don't know what Size it is. – vpetrovic Jul 03 '15 at 07:41
  • EDIT: I called makeTabPanel in Form_Shown, because ItemSize property is 0 before control is shown. Now it works fine! Thanks a lot again! I will try to make UserControl out of this. – vpetrovic Jul 03 '15 at 07:53
  • Is there any reson why tabs are flickering when changing selection? I don't see any heavy operation in Paint handler, there is a Brush object and FillPath, DrawString calls. I set DoubleBuffered property to true but it is the same. I think that last tab flickers the most (there are 3 tabs in my example) – vpetrovic Jul 03 '15 at 14:16
  • You are talking about your tabpages, right? If so, I don't know. They should behave just as without the overlay. Do they or is the flicker new?? If it is the overlay, please check how often it is called and if you call it more often than you expect.. – TaW Jul 03 '15 at 14:18
  • I implemented your solution, I am talking about flickering regions (process arrows) that the code above is painting. It flickers like there is a lot of objects to paint, so the last is late to display propely... – vpetrovic Jul 03 '15 at 14:23
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/82312/discussion-between-taw-and-vpetrovic). – TaW Jul 03 '15 at 14:24