Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#

NeoTabControl Library

Rate me:
Please Sign up or sign in to vote.
4.96/5 (110 votes)
28 Sep 2012CPOL16 min read 193.5K   21.3K   163   131
A custom .NET tab control for WinForms applications with add-in renderer support.

Sample Image

Figure 1-1. NeoTabWindow Control

Introduction

The NeoTabControlLibrary is a custom .NET tab control for the Windows Forms Applications. It's coming with add-in renderer support. So you can change your control's appearance dynamically. I'll provide step-by-step exercises along with a thorough explanation of how you can create your control renderer class. So you can easily create your own control appearance in a few steps (this subject will be described later). At the same time, you can use designs that you've created without recompiling the control library.

  • Mnemonic support. 
  • Supports keyboard navigation.
  • Drag and drop support with a few drag effect style.
  • Supports the 'close' and 'drop-down' smart buttons.
  • Supports the 'drop-down' menu customizations. 
  • Design time support. 
  • Allows you to create your own tooltip customization.
  • Allows you to show or hide an existing tab page items.
  • Add-in renderer support
  • Allows a developer to receive or handle some important messages for the application life cycle. Ex: tab page selections, removing a tab page item from the control container or handling some mouse messages.
  • Supports transparent background color.
  • Allows you to add your tab page items to any of the four sides of the control.
  • Deep clone support.

Backgrounds of the NeoTabWindow and NeoTabPage controls

The NeoTabWindow control derives directly from the Control class, but the NeoTabPage control inherits from the Panel class. At design time, we can do the following to ensure that they behave as we want. At this point, a few attributes can be applied to our class declaration, rather than a specific property. Figure 1-2 shows the NeoTabWindow and NeoTabPage class diagrams.

C#
[Designer(typeof(NeoTabControlLibrary.Design.NeoTabWindowDesigner))]
[DefaultEvent("SelectedIndexChanged"), DefaultProperty("TabPages")]
[ToolboxItem(typeof(NeoTabControlLibrary.Design.NeoTabWindowToolboxItem))]
public class NeoTabWindow : Control, ISupportInitialize, ICloneable
{
    // To do somethings.
}

Sample Image

Figure 1-2. NeoTabWindow and NeoTabPage class diagrams

And the other control: NeoTabPage control class declaration.

C#
[ToolboxItem(false)]
[DesignTimeVisible(false)]
[Designer(typeof(NeoTabControlLibrary.Design.NeoTabPageDesigner))]
[DefaultEvent("Click"), DefaultProperty("Text")]
public class NeoTabPage : Panel, IFormattable, IComparable<NeoTabPage>
{
    // To do somethings.
}

Tab Page Alignments

You can add your tab page items to any of the four sides of the control. To do this, you must override NeoTabPageItemsSide property and specify its side in your renderer class. You can specify one of these values (Top, Left, Bottom, Right). For example;

C#
public override TabPageLayout NeoTabPageItemsSide
{
    get { return TabPageLayout.Left; }
}

You can find a few sample screenshots below for it.

Sample Image

Figure 2. left-aligned NeoTabWindow control, example from the CCleaner renderer assembly.

And the other example shown in Figure 3.

Sample Image

Figure 3. right-aligned NeoTabWindow control, example from the SizDotNET renderer assembly.

In the article files, you can find more examples for this. 

To start working with it.

You have created a new project and want to work with this control library. When you're in the design mode and if you drop a new NeoTabWindow control to your form designer using the Toolbox window, you will see the model dialog box shown in Figure 4 where you can specify the executable directory of your application. After selecting your application directory, click the OK button in this window. If you don't want to work with a custom renderer, you can skip this step by clicking the Cancel button.

Sample Image

Figure 4. Selecting the application directory

After clicking the OK button, you will see the Add-in Manager form shown in Figure 5 where you can use the renderer assemblies that you've created previously. After selecting a renderer assembly in the table you want to work with, click the Apply button. As mentioned above, you can skip this step by clicking the Cancel button if you don't want to work with a custom renderer class.

Sample Image

Figure 5. Exploring the Add-in Manager

You can also access this add-in manager form by using the RendererName property in the properties window. This property is associated with a modal type editor (NeoTabControlLibrary.Design.RendererNameEditor). A modal type editor shows an ellipsis (...) button next to the property value. When this button is clicked, a modal dialog box appears that allows the developer to change the property value. Figure 6 shows the use cases for the NeoTabWindow control case study.

Sample Image

Figure 6. Use cases for NeoTabWindow control

NeoTabWindow Properties

Table 1. Custom properties from the NeoTabWindow control
PropertyDescription
  • SelectedTab
Gets the currently selected tab page.
  • SelectedIndex
Gets or sets the index of the currently selected tab page.
  • IsTooltipEnabled
Determines whether the tooltip effect is enable or not for this tab control.
  • DraggingStyle
Gets or sets the drag and drop style for this control.
  • RendererName
Gets or sets the type name of the renderer class for starting control with a custom renderer.
  • Renderer
Gets or sets the renderer class of the NeoTabWindow control.
  • TooltipRenderer
Gets or sets the tooltip renderer of the control.

NeoTabWindow Events

Table 2. Events from the NeoTabWindow control
Event Description
  • RendererUpdated
Event raised when the current renderer is updated/edited.
  • RendererChanged
Occurs when the value of the Renderer property changes.
  • SelectedIndexChanged
Occurs when the value of the SelectedIndex property changes.
  • SelectedIndexChanging
Occurs when a NeoTabPage control is being selected.
  • TabPageRemoved
Event raised when a NeoTabPage control is removed from this control.
  • TabPageRemoving 
Occurs when a NeoTabPage control is being removed. 
  • DropDownButtonClicked
Event raised when the smart drop-down button is clicked on the control.

NeoTabWindow Methods

Table 3. General Methods from the NeoTabWindow control
MethodDescription
  • Remove 
Removes the specified NeoTabPage control from the control collection.
  • RemoveAt
Removes a NeoTabPage control from the control collection at the specified indexed location if it supports removing.
  • GetHitTest 
Gets the NeoTabPage control at the specified point if it exists in the collection. 
  • ShowTabManager
Allows you to show or hide an existing tab page items.
  • ShowAddInRendererManager
Shows Add-in Manager Form.
  • ShowAddInRendererEditor 
Shows the editor form of the current renderer, if it supports.
  • GetSmartButtonHitTest
The result value is: 0 ( SmartCloseButton ), 1  ( SmartDropDownButton ), -1 ( Not Found ).

NeoTabPage Properties 

Table 4. Properties from the NeoTabPage control
PropertyDescription
  • IsCloseable 
Determines whether this NeoTabPage control is closeable or not.
  • IsSelectable
Determines whether this NeoTabPage control is selectable or not.
  • Text
Gets or sets to be displayed text for this page.
  • ToolTipText
The text that is shown when the mouse hovers over this tab.

NeoTabPage Events

Table 5. Events from the NeoTabPage control
EventDescription
  • TextChanged
Event raised when the value of the Text property is changed on control.

RendererBase Abstract Class

The RendererBase class allows the developer to create a custom renderer assembly by using this class abstract members. Figure 7 shows the CommonObjects diagrams.

Sample Image

Figure 7. CommonObjects diagrams

Here’s the class declaration:  

C#
public abstract class RendererBase : IDisposable
{
    #region Event

    public event EventHandler RendererUpdated;

    #endregion
    
    #region Destructor

    ~RendererBase()
    {
        GC.SuppressFinalize(this);
    }

    #endregion

    #region Protected Methods

    protected void OnRendererUpdated()
    {
        if (RendererUpdated != null)
            RendererUpdated(this, EventArgs.Empty);
    }

    #endregion

    #region Virtual Methods

    public virtual void OnDrawSmartCloseButton(Graphics gfx, Rectangle closeButtonRct, ButtonState btnState)
    {
        if (!IsSupportSmartCloseButton)
            return;
        Pen pen = null;
        Pen borderPen = null;
        Brush brush = null;
        switch (btnState)
        {
            case ButtonState.Normal:
                pen = new Pen(Color.Black);
                break;
            case ButtonState.Hover:
                pen = new Pen(Color.Black);
                borderPen = new Pen(Color.FromArgb(49, 106, 197));
                brush = new SolidBrush(Color.FromArgb(195, 211, 237));
                break;
            case ButtonState.Pressed:
                pen = new Pen(Color.White);
                borderPen = new Pen(Color.LightGray);
                brush = new SolidBrush(Color.FromArgb(41, 57, 85));
                break;
            case ButtonState.Disabled:
                pen = new Pen(SystemColors.GrayText);
                break;
        }
        if (brush != null)
        {
            gfx.FillRectangle(brush, closeButtonRct);
            brush.Dispose();
            if (borderPen != null)
            {
                gfx.DrawRectangle(borderPen, closeButtonRct.Left, closeButtonRct.Top,
                    closeButtonRct.Width - 1, closeButtonRct.Height - 1);
                borderPen.Dispose();
            }
        }
        if (pen != null)
        {
            // Draw close button lines.
            gfx.DrawLine(pen, closeButtonRct.Left + 3, closeButtonRct.Top + 4,
                closeButtonRct.Right - 5, closeButtonRct.Bottom - 3);
            gfx.DrawLine(pen, closeButtonRct.Left + 4, closeButtonRct.Top + 4,
                closeButtonRct.Right - 4, closeButtonRct.Bottom - 3);
            gfx.DrawLine(pen, closeButtonRct.Right - 4, closeButtonRct.Top + 4,
                closeButtonRct.Left + 4, closeButtonRct.Bottom - 3);
            gfx.DrawLine(pen, closeButtonRct.Right - 5, closeButtonRct.Top + 4,
                closeButtonRct.Left + 3, closeButtonRct.Bottom - 3);
            pen.Dispose();
        }
    }

    public virtual void OnDrawSmartDropDownButton(Graphics gfx, Rectangle dropdownButtonRct, ButtonState btnState)
    {
        if (!IsSupportSmartDropDownButton)
            return;
        Pen pen = null;
        Pen borderPen = null;
        Brush brush = null;
        switch (btnState)
        {
            case ButtonState.Normal:
                pen = new Pen(Color.Black);
                break;
            case ButtonState.Hover:
                pen = new Pen(Color.Black);
                borderPen = new Pen(Color.FromArgb(49, 106, 197));
                brush = new SolidBrush(Color.FromArgb(195, 211, 237));
                break;
            case ButtonState.Pressed:
                pen = new Pen(Color.White);
                borderPen = new Pen(Color.LightGray);
                brush = new SolidBrush(Color.FromArgb(41, 57, 85));
                break;
            case ButtonState.Disabled:
                pen = new Pen(SystemColors.GrayText);
                break;
        }
        if (brush != null)
        {
            gfx.FillRectangle(brush, dropdownButtonRct);
            brush.Dispose();
            if (borderPen != null)
            {
                gfx.DrawRectangle(borderPen, dropdownButtonRct.Left, dropdownButtonRct.Top,
                    dropdownButtonRct.Width - 1, dropdownButtonRct.Height - 1);
                borderPen.Dispose();
            }
        }
        if (pen != null)
        {
            using (Brush fill = new SolidBrush(pen.Color))
            {
                gfx.FillPolygon(fill, new Point[]
                {
                    new Point(dropdownButtonRct.Left + 3, dropdownButtonRct.Top + 6),
                    new Point(dropdownButtonRct.Right - 3, dropdownButtonRct.Top + 6),
                    new Point(dropdownButtonRct.Left + dropdownButtonRct.Width / 2, dropdownButtonRct.Bottom - 3)});
            }
            pen.Dispose();
        }
    }

    #endregion

    #region Abstract Methods

    public abstract void InvokeEditor();

    public abstract void OnRendererBackground(Graphics gfx, Rectangle clientRct);

    public abstract void OnRendererTabPageArea(Graphics gfx, Rectangle tabPageAreaRct);

    public abstract void OnRendererTabPageItem(Graphics gfx, Rectangle tabPageItemRct, 
           string tabPageText, int index, ButtonState btnState);

    #endregion

    #region Virtual Properties

    public virtual int SmartButtonsBetweenSpacing { get { return 2; } }

    public virtual Size SmartButtonsSize { get { return new Size(14, 13); } }
    
    public virtual bool IsSupportSmartCloseButton { get { return false; } }

    public virtual bool IsSupportSmartDropDownButton { get { return false; } }

    public virtual DrawingOffset SmartButtonsAreaOffset 
    {
        get { return new DrawingOffset(0, 0, 3, 6); }
    }

    #endregion

    #region Abstract Properties

    public abstract int ItemObjectsDrawingMargin { get; }

    public abstract int TabPageItemsBetweenSpacing { get; }

    public abstract Color BackColor { get; }

    public abstract Color TabPageItemForeColor { get; }

    public abstract Color SelectedTabPageItemForeColor { get; }

    public abstract Color DisabledTabPageItemForeColor { get; }

    public abstract Color MouseOverTabPageItemForeColor { get; }

    public abstract Font NeoTabPageItemsFont { get; }

    public abstract DrawingOffset TabPageAreaCornerOffset { get; }

    public abstract DrawingOffset TabPageItemsAreaOffset { get; }

    public abstract TabPageLayout NeoTabPageItemsSide { get; }

    public abstract TabPageItemStyle NeoTabPageItemsStyle { get; }

    #endregion

    #region IDisposable Members

    public abstract void Dispose();

    #endregion
}

How can I create my own custom renderer class

In the following exercise, you will create the first renderer assembly for your control. When creating subsequent control renderers for the NeoTabWindow control, use this exercise as the model.

As mentioned, you can use this exercise to create your first renderer assembly and then use it as the base steps to create other assemblies. Without further ado, create the first renderer assembly by following these steps:

  1. The first order of business is to open a new instance of Visual Studio. Once Visual Studio is open, you can see the Start Page screen and any recent projects you have created, as shown in Figure 8.
  2. Sample Image

    Figure 8. Opening Visual Studio
  3. Select File -> New -> Project, as shown in Figure 9.
  4. Sample Image

    Figure 9. Creating a new project
  5. After selecting 'Project', you will see the New Project dialog box.
  6. Navigate to the Visual C# list within the Project Types tree, and choose Windows. Select Class Library from the Templates section located on the right side of the dialog box, as shown in Figure 10.
  7. Sample Image

    Figure 10. New Project dialog box
  8. Now you need to enter two pieces of information. The first piece of information is the name of the solution; enter NeoTabControlLibrary.Renderer.VS2008. For the second piece of information, browse to the location on your hard drive where you want to set up your solution file and the subsequent source code. Finally, notice that the option 'Create directory for solution' is checked. This is the desired option. The New Project dialog box will now look like Figure 11.
  9. Sample Image

    Figure 11. Finalized New Project dialog box
  10. Click the OK button to execute and save the new solution file.
  11. You can now view the new solution within Solution Explorer in Visual Studio, as shown in Figure 12. Notice that the new class library automatically created a new class named Class1. You can keep this class for the time being; however, the following exercises, you will either delete or rename this class.
  12. Sample Image

    Figure 12. The new control renderer solution

    You have finished setting up and organizing the Visual Studio solution for your control renderer application; however, it doesn't contain any drawing implementations at this time. You're now prepared to establish the overall drawing architecture and how this architecture will be implemented in the Visual Studio solution.

  13. Since the solution is now set up, you can add the next vital piece to the overall puzzle; in the following exercise, you'll implement the drawing methods and properties of the RendererBase class.
  14. Return to the Visual Studio solution you created in the previous exercise. Continue to Solution Explorer, and right-click on the Class1.cs name that automatically created by Visual Studio in the previous exercise. Then select Rename, as shown in Figure 13.
  15. Sample Image

    Figure 13. Renaming the class name

    Change the name of this class to VS2008LikeRenderer.cs, as shown in Figure 14.

    Sample Image

    Figure 14. The VS2008LikeRenderer class
  16. You are able to view the class that was renamed; however, you still need to do some additional work within the class file. At the moment, the class simply shows the name of class. Change the code within the class to resemble the following:
  17. C#
    using System;
    
    namespace NeoTabControlLibrary.Renderer.VS2008
    {
        public sealed class VS2008LikeRenderer
        {
            #region Static Constructor
    
            static VS2008LikeRenderer()
            {
    
            }
    
            #endregion
        }
    }

    You have changed the class to that of a public sealed class, and you have added the subsequent static constructor. When creating subsequent your control renderer assemblies, use this exercise as the model. Now you're ready to move along with the implementation of the properties and drawing methods contained in the VS2008LikeRenderer class.

  18. Before you add the abstract methods and properties of the RendererBase class, you need to add a reference to the class library project. The reference you need is the NeoTabControlLibrary.CommonObjects.RendererBase class from the NeoTabControlLibrary.CommonObjects library. To add this reference, right-click the References folder within the project, and select the Add Reference menu item, as shown in Figure 15.
  19. Sample Image

    Figure 15. Adding a reference

    After selecting the Add Reference option, within a few seconds you will see the Add Reference dialog box. Firstly, you select the System.Drawing reference located on the .NET tab, as shown in Figure 16, and then click the OK button.

    Sample Image

    Figure 16. Add Reference dialog box

    This adds the reference to System.Drawing to your class library project. You can now complete this step by adding the NeoTabControlLibrary.CommonObjects reference.

    Right-click again on the References folder within the same project and then select Add Reference menu item. You will see the Add Reference dialog box again. Navigate to the Browse tab within this screen, and choose the location of the NeoTabControlLibrary.CommonObjects.dll library in this tab, as shown in Figure 17, and then click the OK button.

    Sample Image

    Figure 17. Adding an existing item reference

    After clicking the OK button, you will see the reference assemblies under the References folder that you have added. After this point, you have to declare the new namespaces being used at the top of the code. And also, import the System.Drawing.Drawing2D namespace at this section:

    C#
    using System;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using NeoTabControlLibrary.CommonObjects;
    
    namespace NeoTabControlLibrary.Renderer.VS2008
    {
    
    }
  20. You can now implement the abstract methods and properties of the RendererBase class. For the VS2008LikeRenderer class, have it inherit from the RendererBase class, as shown here:
  21. C#
    using System;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using NeoTabControlLibrary.CommonObjects;
    
    namespace NeoTabControlLibrary.Renderer.VS2008
    {
        public sealed class VS2008LikeRenderer : RendererBase
        {
            #region Static Constructor
    
            static VS2008LikeRenderer()
            {
    
            }
    
            #endregion
        }
    }
  22. You have now indicated that the VS2008LikeRenderer class will implement the RendererBase class, but now you have to add the actual implementation. After you finish typing the name of the base class, the Visual Studio integrated development environment (IDE) will give you a shortcut where you can complete the implementation. Figure 18 shows the indicator just below the letter 'R' in the word RendererBase.
  23. Sample Image

    Figure 18. The abstract class indicator
  24. Move your cursor over the indicator, and click the down arrow to display the options shown in Figure 19.
  25. Sample Image

    Figure 19. Choosing the implementation
  26. Click on the 'Implement abstract class RendererBase' item, and then override the drawing methods and properties within this class, the following code is for the complete VS2008LikeRenderer class:
  27. C#
    public sealed class VS2008LikeRenderer : RendererBase
    {
        #region Symbolic Constants
        private static readonly Font MY_FONT;
        private static readonly DrawingOffset[] OFFSETS;
        private static readonly Color[] COLORS;
        private static readonly int[] INTEGERARRAY;
        #endregion
    
        #region Static Constructor
        static VS2008LikeRenderer()
        {
            MY_FONT = new Font("Tahoma", 8.25f, FontStyle.Bold);
            OFFSETS = new DrawingOffset[] {
                // PAGE_AREA_OFFSET
                new DrawingOffset(5, 5, 5, 5),
                // ITEM_AREA_OFFSET
                new DrawingOffset(0, 5, 0, 0) };
            COLORS = new Color[]{
                // BackColor
                SystemColors.Control,
                // TabPageItemForeColor
                Color.Black,
                // SelectedTabPageItemForeColor
                Color.Black,
                // DisabledTabPageItemForeColor
                SystemColors.GrayText,
                // MouseOverTabPageItemForeColor
                Color.Black
            };
            // ItemObjectsDrawingMargin, TabPageItemsBetweenSpacing
            INTEGERARRAY = new int[] { 3, 0 };
        }
        #endregion
    
        #region Helper Methods
        private GraphicsPath CreateRoundRect(Rectangle rect, int radius)
        {
            GraphicsPath gp = new GraphicsPath();
            int x = rect.X;
            int y = rect.Y;
            int width = rect.Width;
            int height = rect.Height;
            if (radius > 0)
            {
                radius = Math.Min(radius, height / 2 - 1);
                radius = Math.Min(radius, width / 2 - 1);
                gp.AddLine(x + radius, y, x + width - (radius * 2), y);
                gp.AddArc(x + width - (radius * 2), y, radius * 2, radius * 2, 270, 90);
                gp.AddLine(x + width, y + radius, x + width, y + height - (radius * 2));
                gp.AddArc(x + width - (radius * 2), y + height - (radius * 2), radius * 2, radius * 2, 0, 90);
                gp.AddLine(x + width - (radius * 2), y + height, x + radius, y + height);
                gp.AddArc(x, y + height - (radius * 2), radius * 2, radius * 2, 90, 90);
                gp.AddLine(x, y + height - (radius * 2), x, y + radius);
                gp.AddArc(x, y, radius * 2, radius * 2, 180, 90);
            }
            else
            {
                gp.AddRectangle(rect);
            }
            gp.CloseFigure();
            return gp;
        }
        #endregion
    
        public override void InvokeEditor()
        {
            throw new NotImplementedException();
        }
    
        public override void OnRendererBackground(Graphics gfx, Rectangle clientRct)
        {
            // Do nothing.
        }
    
        public override void OnRendererTabPageArea(Graphics gfx, Rectangle tabPageAreaRct)
        {
            Rectangle rct = tabPageAreaRct;
            SmoothingMode mode = gfx.SmoothingMode;
            gfx.SmoothingMode = SmoothingMode.AntiAlias;
            rct.Inflate(-2, -2);
            rct.Width -= 1;
            rct.Height -= 1;
            using (Brush brush = new SolidBrush(Color.FromArgb(194, 207, 229)))
            {
                using (Pen pen = new Pen(brush, 4))
                    gfx.DrawRectangle(pen, rct);
            }
            rct = tabPageAreaRct;
            rct.Inflate(-4, -4);
            rct.Width -= 1;
            rct.Height -= 1;
            using (Pen pen = new Pen(Color.FromArgb(161, 180, 214)))
                gfx.DrawRectangle(pen, rct);
            rct = tabPageAreaRct;
            rct.Width -= 1;
            rct.Height -= 1;
            using (GraphicsPath path = CreateRoundRect(rct, 4))
            {
                using (LinearGradientBrush brush =
                    new LinearGradientBrush(Point.Empty, new Point(0, 1),
                        Color.FromArgb(157, 177, 212),
                        Color.FromArgb(153, 175, 212)))
                {
                    Blend bl = new Blend(2);
                    bl.Factors = new float[] { 0.3F, 1.0F };
                    bl.Positions = new float[] { 0.0F, 1.0F };
                    brush.Blend = bl;
                    using (Pen pen = new Pen(brush))
                        gfx.DrawPath(pen, path);
                }
            }
            rct.Inflate(-1, -1);
            using (GraphicsPath path = CreateRoundRect(rct, 4))
            {
                using (Pen pen = new Pen(Color.FromArgb(225, 230, 232)))
                    gfx.DrawPath(pen, path);
            }
            gfx.SmoothingMode = mode;
        }
    
        public override void OnRendererTabPageItem(Graphics gfx, Rectangle tabPageItemRct, string tabPageText, 
          int index, CommonObjects.ButtonState btnState)
        {
            Rectangle itemRct = tabPageItemRct;
            itemRct.Y += 2;
            itemRct.Height -= 2;
            SmoothingMode mode = gfx.SmoothingMode;
            gfx.SmoothingMode = SmoothingMode.AntiAlias;
            using (StringFormat format = new StringFormat(StringFormatFlags.LineLimit))
            {
                format.Alignment = StringAlignment.Center;
                format.LineAlignment = StringAlignment.Center;
                format.HotkeyPrefix = System.Drawing.Text.HotkeyPrefix.Show;
                using (GraphicsPath path = new GraphicsPath())
                {
                    int xOffset;
                    bool isSelected = false;
                    Color textColor = DisabledTabPageItemForeColor;
                    switch (btnState)
                    {
                        case CommonObjects.ButtonState.Normal:
                            textColor = TabPageItemForeColor;
                            goto case CommonObjects.ButtonState.Disabled;
                        case CommonObjects.ButtonState.Pressed:
                            isSelected = true;
                            itemRct.Y -= 2;
                            itemRct.Height += 2;
                            textColor = SelectedTabPageItemForeColor;
                            using (Brush brush = new SolidBrush(BackColor))
                                gfx.FillRectangle(brush, itemRct);
                            goto case CommonObjects.ButtonState.Disabled;
                        case CommonObjects.ButtonState.Disabled:
                            if (index == 0)
                            {
                                xOffset = itemRct.Left + 10 + (itemRct.Height / 2);
                                Point[] points = new Point[]
                                {
                                    new Point(itemRct.Left, itemRct.Bottom),
                                    new Point(itemRct.Left, itemRct.Bottom - 3),
                                    new Point(itemRct.Left + 8, itemRct.Bottom - 17),
                                    new Point(xOffset, itemRct.Top),
                                };
                                path.AddBeziers(points);
                            }
                            else if (isSelected)
                            {
                                xOffset = itemRct.Left + (itemRct.Height / 2);
                                Point[] points = new Point[]
                                {
                                    new Point(itemRct.Left - 10, itemRct.Bottom),
                                    new Point(itemRct.Left - 10, itemRct.Bottom - 3),
                                    new Point(itemRct.Left - 2, itemRct.Bottom - 17),
                                    new Point(xOffset, itemRct.Top),
                                };
                                path.AddBeziers(points);
                            }
                            else
                            {
                                xOffset = itemRct.Left + (itemRct.Height / 2);
                                path.AddLine(itemRct.Left, itemRct.Bottom, itemRct.Left,
                                    itemRct.Top + (itemRct.Height / 2) - 3);
                                Point[] points = new Point[]
                                {
                                    new Point(itemRct.Left, itemRct.Top + (itemRct.Height / 2) - 4),
                                    new Point(itemRct.Left, itemRct.Top + (itemRct.Height / 2) - 5),
                                    new Point(itemRct.Left + 2, itemRct.Top + 2),
                                    new Point(xOffset, itemRct.Top),
                                };
                                path.AddBeziers(points);
                            }
                            path.AddLine(xOffset + 1, itemRct.Top, itemRct.Right - 4, itemRct.Top);
                            path.AddLine(itemRct.Right - 1, itemRct.Top + 2, itemRct.Right - 1, itemRct.Bottom);
                            path.CloseFigure();
                            using (LinearGradientBrush brush = new LinearGradientBrush(itemRct, Color.White,
                                isSelected ? Color.FromArgb(194, 207, 229) : Color.FromArgb(238, 236, 221), 
                                  LinearGradientMode.Vertical))
                            {
                                Blend bl = new Blend(2);
                                bl.Factors = new float[] { 0.4F, 1.0F };
                                bl.Positions = new float[] { 0.0F, 1.0F };
                                brush.Blend = bl;
                                gfx.FillPath(brush, path);
                            }
                            using (Pen pen = new Pen(isSelected ? Color.FromArgb(153, 175, 212) : Color.FromArgb(172, 168, 153)))
                            {
                                gfx.DrawPath(pen, path);
                                if (isSelected)
                                {
                                    pen.Color = Color.FromArgb(194, 207, 229);
                                    gfx.DrawLine(pen, index == 0 ? itemRct.Left + 1 : itemRct.Left - 9, itemRct.Bottom, 
                                      itemRct.Right - 2, itemRct.Bottom);
                                    gfx.DrawLine(pen, index == 0 ? itemRct.Left + 1 : itemRct.Left - 9, itemRct.Bottom + 1, 
                                      itemRct.Right - 2, itemRct.Bottom + 1);
                                }
                                else
                                {
                                    pen.Color = Color.FromArgb(156, 176, 212);
                                    gfx.DrawLine(pen, itemRct.Left, itemRct.Bottom, itemRct.Right - 1, itemRct.Bottom);
                                }
                            }
                            using (Font font = new Font(NeoTabPageItemsFont, isSelected ? FontStyle.Bold : FontStyle.Regular))
                            {
                                itemRct.X += 2;
                                itemRct.Width -= 2;
                                itemRct.Y += 2;
                                itemRct.Height -= 2;
                                if (index == 0)
                                {
                                    itemRct.X += 6;
                                    itemRct.Width -= 6;
                                }
                                using (Brush brush = new SolidBrush(textColor))
                                    gfx.DrawString(tabPageText, font, brush, itemRct, format);
                            }
                            break;
                        case CommonObjects.ButtonState.Hover:
                            if (index == 0)
                            {
                                xOffset = itemRct.Left + 10 + (itemRct.Height / 2);
                                Point[] points = new Point[]
                                {
                                    new Point(itemRct.Left, itemRct.Bottom),
                                    new Point(itemRct.Left, itemRct.Bottom - 3),
                                    new Point(itemRct.Left + 8, itemRct.Bottom - 17),
                                    new Point(xOffset, itemRct.Top),
                                };
                                path.AddBeziers(points);
                            }
                            else
                            {
                                xOffset = itemRct.Left + (itemRct.Height / 2);
                                path.AddLine(itemRct.Left, itemRct.Bottom, itemRct.Left,
                                    itemRct.Top + (itemRct.Height / 2) - 3);
                                Point[] points = new Point[]
                                {
                                    new Point(itemRct.Left, itemRct.Top + (itemRct.Height / 2) - 4),
                                    new Point(itemRct.Left, itemRct.Top + (itemRct.Height / 2) - 5),
                                    new Point(itemRct.Left + 2, itemRct.Top + 2),
                                    new Point(xOffset, itemRct.Top),
                                };
                                path.AddBeziers(points);
                            }
                            path.AddLine(xOffset + 1, itemRct.Top, itemRct.Right - 4, itemRct.Top);
                            path.AddLine(itemRct.Right - 1, itemRct.Top + 2, itemRct.Right - 1, itemRct.Bottom);
                            path.CloseFigure();
                            using (LinearGradientBrush brush = new LinearGradientBrush(itemRct, Color.FromArgb(220,226,231),
                                Color.FromArgb(162, 187, 226), LinearGradientMode.Vertical))
                            {
                                Blend bl = new Blend(2);
                                bl.Factors = new float[] { 0.3F, 1.0F };
                                bl.Positions = new float[] { 0.0F, 1.0F };
                                brush.Blend = bl;
                                gfx.FillPath(brush, path);
                            }
                            using (Pen pen = new Pen(Color.FromArgb(153, 175, 212)))
                            {
                                gfx.DrawPath(pen, path);
                                pen.Color = Color.FromArgb(194, 207, 229);
                                gfx.DrawLine(pen, itemRct.Left, itemRct.Bottom, itemRct.Right - 1, itemRct.Bottom);
                            }
                            using (Font font = new Font(NeoTabPageItemsFont, FontStyle.Regular))
                            {
                                itemRct.X += 2;
                                itemRct.Width -= 2;
                                itemRct.Y += 2;
                                itemRct.Height -= 2;
                                if (index == 0)
                                {
                                    itemRct.X += 6;
                                    itemRct.Width -= 6;
                                }
                                using (Brush brush = new SolidBrush(MouseOverTabPageItemForeColor))
                                    gfx.DrawString(tabPageText, font, brush, itemRct, format);
                            }
                            break;
                    }
                }
            }
            gfx.SmoothingMode = mode;
        }
        public override int ItemObjectsDrawingMargin
        {
            get { return INTEGERARRAY[0]; }
        }
        public override int TabPageItemsBetweenSpacing
        {
            get { return INTEGERARRAY[1]; }
        }
        public override Color BackColor
        {
            get { return COLORS[0]; }
        }
        public override Color TabPageItemForeColor
        {
            get { return COLORS[1]; }
        }
        public override Color SelectedTabPageItemForeColor
        {
            get { return COLORS[2]; }
        }
        public override Color DisabledTabPageItemForeColor
        {
            get { return COLORS[3]; }
        }
        public override Color MouseOverTabPageItemForeColor
        {
            get { return COLORS[4]; }
        }
        public override Font NeoTabPageItemsFont
        {
            get { return MY_FONT; }
        }
        public override DrawingOffset TabPageAreaCornerOffset
        {
            get { return OFFSETS[0]; }
        }
        public override DrawingOffset TabPageItemsAreaOffset
        {
            get { return OFFSETS[1]; }
        }
        public override TabPageLayout NeoTabPageItemsSide
        {
            get { return TabPageLayout.Top; }
        }
        public override TabPageItemStyle NeoTabPageItemsStyle
        {
            get { return TabPageItemStyle.OnlyText; }
        }
        public override void Dispose()
        {
            GC.SuppressFinalize(this);
        }
    }
  28. Now you need to enter some additional class information being used at the top of the class declaration. To do this, you need to connect your renderer class with the AddInRenderer attribute. After you apply this attribute to the class declaration, enter some descriptive information (version number, developer name, display name...) (If your control renderer supports editor, set IsSupportEditor = true, in this example it is false) within the AddInRenderer attribute, as shown here:
  29. C#
    [AddInRenderer("VS2008Like",
      "VS2008Like renderer class, TabPageLayout: Top, TabPageItemStyle: OnlyText.",
      DeveloperName = "Burak Özdiken", VersionNumber = "1.0.0.0")]
    public sealed class VS2008LikeRenderer : RendererBase
    {
    
    }
  30. Finally, to use this renderer assembly for your applications, you'll create a subdirectory named VS2008 under the 'Add-ins' directory. And then, add your assembly file into the folder that you've created, as shown in Figure 20.
  31. Sample Image

    Figure 20. Creating a new renderer subdirectory

    Add your assembly file, as shown in Figure 21.

    Sample Image

    Figure 21. Adding a assembly file
  32. The exercise is now complete, and you have successfully created your first renderer assembly. The final product will now look like Figure 22-1.
  33. Sample Image

    Figure 22-1. The final product 
  34. To support smart 'close' and 'drop-down' buttons, you need to override IsSupportSmartCloseButton and IsSupportSmartDropDownButton properties, as shown here:
  35. C#
    public override bool IsSupportSmartCloseButton
    {
        get { return true; }
    }
    
    public override bool IsSupportSmartDropDownButton
    {
        get { return true; }
    }

    Sample Image

    Figure 22-2. The smart 'close' and 'drop-down' buttons;

How can I load a custom renderer in runtime 

You can load a renderer assembly using the AddInRendererManager.LoadRenderer(string typeName) static method. This static method loads a specific control renderer from a given renderer's type name. It returns a renderer class from a specified type name, as shown here:

C#
private void button1_Click(object sender, EventArgs e)
{
    neoTabWindow1.Renderer = AddInRendererManager.LoadRenderer("MYNETRendererVS2");
}

How can I show a renderer editor for the specified control renderer 

If it supports, you can show a renderer editor using the ShowAddInRendererEditor() method of the NeoTabWindow control, as shown here: 

C#
private void button2_Click(object sender, EventArgs e)
{
    neoTabWindow1.ShowAddInRendererEditor();
}

RendererChanged Event

You can handle this event for each control renderer changes. For example; you might want to change the background color of the form, controls padding, margin ex...

C#
private void neoTabWindow1_RendererChanged(object sender, EventArgs e)
{
    this.BackColor = neoTabWindow1.BackColor;
    string typeName = neoTabWindow1.Renderer.GetType().Name;
    if (typeName.StartsWith("CCleanerRenderer"))
    {
        foreach (NeoTabControlLibrary.NeoTabPage tp in neoTabWindow1.Controls)
            tp.BackColor = Color.White;
    }
    else if (typeName.StartsWith("NeoTabStripRenderer"))
    {
        foreach (NeoTabControlLibrary.NeoTabPage tp in neoTabWindow1.Controls)
            tp.BackColor = Color.White;
    }
    else if (typeName.StartsWith("VS2010LikeRenderer"))
    {
        foreach (NeoTabControlLibrary.NeoTabPage tp in neoTabWindow1.Controls)
            tp.BackColor = Color.White;
    }
    else
    {
        foreach (NeoTabControlLibrary.NeoTabPage tp in neoTabWindow1.Controls)
            tp.BackColor = neoTabWindow1.BackColor;
    }
}

Customizing the drop-down menu

You can customize your drop-down menu items using the DropDownButtonClicked event, as shown here:

Sample Image

Figure 22-3. Customizing drop-down menu
C#
private void neoTabWindow1_DropDownButtonClicked(object sender, 
    NeoTabControlLibrary.DropDownButtonClickedEventArgs e)
{
    for (int i = 0; i < e.ContextMenu.Items.Count; i++)
    {
        ToolStripItem item = e.ContextMenu.Items[i];
        switch (i)
        {
            case 0:
                item.Image = NeoTabControlClient.Properties.Resources.Close;
                break;
            case 1:
                break;
            default:
                if (item.Enabled)
                    item.Image = NeoTabControlClient.Properties.Resources.InsertTabControlHS;
                else
                    item.Image = NeoTabControlClient.Properties.Resources.Locked;
                break;
        }
    }
}

How can I add my own tab page controls to the control container in runtime

To add a new tab page control, you can use the following code lines, as shown here:

C#
NeoTabPage myTabPage = new NeoTabPage();
myTabPage.Text = "My tab page control";
neoTabWindow1.Controls.Add(myTabPage);

You can also add your tab page items using the NeoTabWindow.TabPages property, as shown here:

C#
NeoTabPage[] myTabPageItems = 
    new NeoTabPage[]{
    new NeoTabPage(),
    new NeoTabPage(),
    new NeoTabPage()
};
neoTabWindow1.TabPages.AddRange(myTabPageItems);

How can I get or set a tab page control in the control container

You can access the selected tab page control using the SelectedTab property. If it returns a null value, your tab control does not contain any tab pages control.

C#
NeoTabPage selectedTab = neoTabWindow1.SelectedTab;
MessageBox.Show(selectedTab.Text);

To change the selected index of the control, you can use the following code lines.

C#
// This selects the second tab page control.
neoTabWindow1.SelectedIndex = 2;

How can I remove a tab page item from my tab control

To remove a specific tab page control from the control container, you can use the Remove(NeoTabPage toBeRemoved) method, as shown here:

C#
NeoTabPage myTabPage = new NeoTabPage();
myTabPage.Text = "My tab page control";
neoTabWindow1.Controls.Add(myTabPage);
// Removing an existing tab page control.
NeoTabPage toBeRemoved = myTabPage;
neoTabWindow1.Remove(toBeRemoved);

If you want to remove a tab page item from the control collection at the specified indexed location, you can use the RemoveAt(int toBeRemovedIndex) method, as shown here:

C#
// Remove at the second indexed location.
neoTabWindow1.RemoveAt(2);

You can also use the TabPages.Remove(), Controls.Remove() and TabPages.RemoveAt(), Controls.RemoveAt() methods; However, if you use this methods for a control removing, you cannot use the TabPageRemoving, TabPageRemoved, SelectedIndexChanging, SelectedIndexChanged events.

How can I show the tab page manager

you can show the tab manager using the ShowTabManager() method of the NeoTabWindow control, as shown here:

C#
private void button3_Click(object sender, EventArgs e)
{
    neoTabWindow1.ShowTabManager();
}

Image 26

Figure 22-4. Show / Hide Tab Manager 

Note: If your tab page items are not closeable, you cannot use the tab manager for these items.

Accessing tab page items from the control mouse events

To access a tab page item from a control mouse event, You can use the GetHitTest() method, as shown here:

C#
private void neoTabWindow1_MouseMove(object sender, MouseEventArgs e)
{
    int itemIndex = -1;
    Rectangle itemRectangle;
    NeoTabControlLibrary.CommonObjects.ButtonState itemState;
    NeoTabControlLibrary.NeoTabPage tp = neoTabWindow1.GetHitTest(e.Location,
        out itemRectangle, out itemState, out itemIndex);
    if (tp != null && tp.IsSelectable)
    {
        // Change the text value of the Form1.
        this.Text = tp.Text;
    }
}

Sample Image

Figure 23. Mouse move activity diagram

Drag & Drop support

If the value of the AllowDrop property is true for your tab control, you can drag and drop a tab page item onto the other tab page items, as shown in Figure 24.

  1. TabPageItemEffect
  2. Sample Image

    Figure 24. DragDropStyle: TabPageItemEffect
  3. You can also use the 'PageEffect' style, as shown in Figure 25.
  4. Sample Image

    Figure 25. DragDropStyle: PageEffect

Prevent the selection of a tab item

To prevent selection, you can use the following code lines, as shown here:

The first easy way:

C#
neoTabPage5.IsSelectable = false;

The second way is:

C#
private void neoTabWindow1_SelectedIndexChanging(object sender, 
  NeoTabControlLibrary.SelectedIndexChangingEventArgs e)
{
    // Do not select the Output tab.
    if (e.TabPage == neoTabPage5)
        e.Cancel = true;
}

To prevent removing:

The first easy way:

C#
neoTabPage3.IsCloseable = false;

The second way is:

C#
private void neoTabWindow1_TabPageRemoving(object sender, 
  NeoTabControlLibrary.TabPageRemovingEventArgs e)
{
    // Do not remove the Toolbox tab.
    if (e.TabPage == neoTabPage3)
        e.Cancel = true;
}

Keyboard Support and Navigation

Sample Image

Figure 26. Keyboard support

You can navigate between the tab page items using the keyboard shortcuts.

KeysDescription
EndSelects last tab.
HomeSelects first tab.
Left and Tab+Control+Shift keysSelects the tab on the left side of the currently selected tab.
Right and Tab+Control keysSelects the tab on the right side of the currently selected tab.

Tooltips

If the value of the IsTooltipEnabled property is true, the tooltip text that is shown when the mouse cursor is over a tab page item, as shown in Figure 27.

Sample Image

Figure 27. Working with tooltips

You can customize your own tooltips using the TooltipRenderer property, as shown in Figure 28.

Sample Image

Figure 28. Using the TooltipRenderer property

After the customizing, it will now look like Figure 29.

Sample Image

Figure 29. Customizing the tooltips

Clone Support

Sample Image

Figure 30. Dolly, copy clone sheep

To clone a NeoTabWindow control from an existing NeoTabWindow control, you can use the following example, as shown here:

C#
NeoTabPage[] newTabPageItems = 
    new NeoTabPage[]{
    new NeoTabPage(){ Text = "Mario Andretti" },
    new NeoTabPage(){ Text = "Graham Hill" },
    new NeoTabPage(){ Text = "Emerson Fittipaldi" },
    new NeoTabPage(){ Text = "Michael Schumacher" }
};
// Create a new clone from my existing tab control with four new tab page control.
NeoTabWindow myClonedControl = neoTabWindow1.Clone() as NeoTabWindow;
myClonedControl.Controls.AddRange(newTabPageItems);
// Show it within the form of the newly created.
myClonedControl.Dock = DockStyle.Fill;
Form newFrm = new Form();
newFrm.ShowIcon = false;
newFrm.ShowInTaskbar = false;
newFrm.Text = "Formula-1 Racers";
newFrm.Controls.Add(myClonedControl);
newFrm.Show();
Figure 31. Control cloning

Image 35

History

  • Sept 28, 2012 - Updated 
    • Added an example for the clone support.
    • Created a new renderer assembly named VS2005LikeRenderer
  • Sept 27, 2012 - Updated 
    • Created a new collection class named NeoTabPageHidedMembersCollection
    • Added the ability to show or hide an existing tab page items. 
    • Inserted a new menu item within the drop-down menu to showing the tab manager.
    • Added a new interface to the NeoTabPage class, named IFormattable.
    • Added a new interface to the NeoTabPage class, named IComparable<NeoTabPage>
    • Updated NeoTabPage class. 
    • Updated NeoTabWindow class. 
  • Sept 25, 2012 - Updated 
    • Created a new renderer assembly named TelerikRenderer
  • Sept 18, 2012 - Updated 
    • Added smart 'close' and 'drop-down' button support. 
    • Created a new event named DropDownButtonClicked within the NeoTabWindow class. 
    • Updated NeoTabWindow class. 
    • Created a new renderer assembly named OrderedListRenderer
    • Updated RendererBase class. 
  • Sept 14, 2012 - Updated 
    • Fixed InvalidOperationException and cross-threading errors. 
    • Added clone support descriptions. 
  • Sept 13, 2012 - Updated 
    • Added editor support for control renderer assemblies. 
    • Created a new event named RendererUpdated within the NeoTabWindow class. 
    • Created a new renderer assembly named VS2012LikeRenderer with editor support. 
    • Created a new DataGridViewEditorButtonColumn for the Add-in Manager table. 
    • Updated AddInRendererManager class. 
    • Updated RendererBase class. 
    • Fixed names of the CCleaner renderer resources 
  • Sept 10, 2012 - First release. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) ARELTEK
Turkey Turkey
Since 1998...

MCPD - Enterprise Application Developer

“Hesaplı hareket ettiğini zanneden ve onunla iftihar eyliyen dar kafalar; kurtulmağa, yükselmeğe elverişli hiç bir eser vücüda getirmezler. Kurtuluş ve yükselişi, ancak varlığına dayanan ve mülkü milletin gizli kapalı hazinelerini verimli hale getirmesini bilen, şahsi menfaatini millet menfaati uğruna feda eden, ruhu idealist, dimağı realist şahsiyetlerde aramalıdır.”

Nuri Demirağ, 1947

Comments and Discussions

 
GeneralMy vote of 5 Pin
Aamer Alduais15-Sep-12 22:37
Aamer Alduais15-Sep-12 22:37 
GeneralRe: My vote of 5 Pin
Burak Ozdiken16-Sep-12 0:57
Burak Ozdiken16-Sep-12 0:57 
QuestionMy vote of 5 Pin
Akram El Assas14-Sep-12 12:46
Akram El Assas14-Sep-12 12:46 
AnswerRe: My vote of 5 Pin
Burak Ozdiken14-Sep-12 23:05
Burak Ozdiken14-Sep-12 23:05 
GeneralMy vote of 5 Pin
ertan200214-Sep-12 11:04
ertan200214-Sep-12 11:04 
GeneralRe: My vote of 5 Pin
Burak Ozdiken14-Sep-12 11:39
Burak Ozdiken14-Sep-12 11:39 
QuestionLoading control in designer Pin
Bojan Bajcetic (Molon Labe)14-Sep-12 0:22
Bojan Bajcetic (Molon Labe)14-Sep-12 0:22 
AnswerRe: Loading control in designer Pin
Burak Ozdiken14-Sep-12 0:39
Burak Ozdiken14-Sep-12 0:39 
GeneralRe: Loading control in designer Pin
Bojan Bajcetic (Molon Labe)14-Sep-12 1:24
Bojan Bajcetic (Molon Labe)14-Sep-12 1:24 
GeneralRe: Loading control in designer Pin
Burak Ozdiken14-Sep-12 1:37
Burak Ozdiken14-Sep-12 1:37 
GeneralRe: Loading control in designer Pin
Bojan Bajcetic (Molon Labe)14-Sep-12 2:17
Bojan Bajcetic (Molon Labe)14-Sep-12 2:17 
GeneralRe: Loading control in designer Pin
Burak Ozdiken14-Sep-12 3:11
Burak Ozdiken14-Sep-12 3:11 
GeneralRe: Loading control in designer Pin
Bojan Bajcetic (Molon Labe)14-Sep-12 3:46
Bojan Bajcetic (Molon Labe)14-Sep-12 3:46 
GeneralRe: Loading control in designer Pin
Burak Ozdiken14-Sep-12 3:57
Burak Ozdiken14-Sep-12 3:57 
GeneralMy vote of 2 Pin
programmerdon13-Sep-12 6:34
programmerdon13-Sep-12 6:34 
GeneralRe: My vote of 2 Pin
Burak Ozdiken13-Sep-12 6:54
Burak Ozdiken13-Sep-12 6:54 
GeneralRe: My vote of 2 Pin
programmerdon13-Sep-12 7:06
programmerdon13-Sep-12 7:06 
GeneralRe: My vote of 2 Pin
Burak Ozdiken13-Sep-12 7:18
Burak Ozdiken13-Sep-12 7:18 
GeneralRe: My vote of 2 Pin
Adrian Cole13-Sep-12 17:23
Adrian Cole13-Sep-12 17:23 
GeneralRe: My vote of 2 Pin
Burak Ozdiken14-Sep-12 2:55
Burak Ozdiken14-Sep-12 2:55 
QuestionVery Nice ! Pin
pham.quocduy13-Sep-12 4:37
pham.quocduy13-Sep-12 4:37 
AnswerRe: Very Nice ! Pin
Burak Ozdiken13-Sep-12 11:34
Burak Ozdiken13-Sep-12 11:34 
QuestionYou rock Pin
netnutch11-Sep-12 5:37
netnutch11-Sep-12 5:37 
AnswerRe: You rock Pin
Burak Ozdiken12-Sep-12 23:55
Burak Ozdiken12-Sep-12 23:55 
GeneralMy vote of 5 Pin
herves10-Sep-12 23:55
herves10-Sep-12 23:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.