Click here to Skip to main content
15,892,965 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 199.1K   21.3K   163  
A custom .NET tab control for WinForms applications with add-in renderer support.
using System;
using System.Drawing;
using System.Drawing.Text;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;

namespace NeoTabControlLibrary
{
    public class ToolTips : Form
    {
        #region API

        [DllImport("gdi32")]
        private static extern bool DeleteObject(IntPtr hObject);

        [DllImport("gdi32")]
        private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

        [DllImport("gdi32", SetLastError = true)]
        private static extern IntPtr CreateCompatibleDC(IntPtr hdc);

        [DllImport("gdi32")]
        private static extern bool DeleteDC(IntPtr dc);

        [DllImport("user32", CharSet = CharSet.Auto)]
        private static extern IntPtr GetDC(IntPtr hwnd);

        [DllImport("user32", CharSet = CharSet.Auto)]
        private static extern int ReleaseDC(
            IntPtr hwnd,
            IntPtr hdc);

        [DllImport("user32", ExactSpelling = true, SetLastError = true)]
        private static extern bool UpdateLayeredWindow(IntPtr hwnd,
            IntPtr hdcDst,
            ref Point pptDst,
            ref Size psize,
            IntPtr hdcSrc,
            ref Point pprSrc,
            Int32 crKey,
            ref BLENDFUNCTION pblend,
            Int32 dwFlags);

        #endregion

        #region Struct

        [StructLayout(LayoutKind.Sequential)]
        struct BLENDFUNCTION
        {
            public byte BlendOp;
            public byte BlendFlags;
            public byte SourceConstantAlpha;
            public byte AlphaFormat;
        };

        #endregion

        #region Event

        /// <summary>
        /// Event raised when the tooltip operation is succesfully completed.
        /// </summary>
        public event EventHandler TaskCompleted;

        #endregion

        #region Delegate

        public delegate void UIThreadPainterDelegate(Bitmap bmp, byte transparency);

        #endregion

        #region Enums

        public enum StatusState
        {
            Pending, InProgress, Completed
        };

        enum WindowExStyles
        {
            GWL_STYLE = -16,
            GWL_EXSTYLE = (-20),    //GetWindowLong
            WS_EX_DLGMODALFRAME = 0x00000001,
            WS_EX_NOPARENTNOTIFY = 0x00000004,
            WS_EX_TOPMOST = 0x00000008,
            WS_EX_ACCEPTFILES = 0x00000010,
            WS_EX_TRANSPARENT = 0x00000020,
            WS_EX_MDICHILD = 0x00000040,
            WS_EX_TOOLWINDOW = 0x00000080,
            WS_EX_WINDOWEDGE = 0x00000100,
            WS_EX_CLIENTEDGE = 0x00000200,
            WS_EX_CONTEXTHELP = 0x00000400,
            WS_EX_RIGHT = 0x00001000,   //Gives a window generic right-aligned properties.This depends on the window class.
            WS_EX_LEFT = 0x00000000,
            WS_EX_RTLREADING = 0x00002000,  //Displays the window text using right-to-left reading order properties.
            WS_EX_LTRREADING = 0x00000000,
            WS_EX_LEFTSCROLLBAR = 0x00004000,   //Places a vertical scroll bar to the left of the client area.
            WS_EX_RIGHTSCROLLBAR = 0x00000000,
            WS_EX_CONTROLPARENT = 0x00010000,
            WS_EX_STATICEDGE = 0x00020000,
            WS_EX_APPWINDOW = 0x00040000,
            WS_EX_OVERLAPPEDWINDOW = 0x00000300,
            WS_EX_PALETTEWINDOW = 0x00000188,
            WS_EX_LAYERED = 0x00080000,
            WS_EX_NOACTIVATE = 0x08000000
        };

        #endregion

        #region Symbolic Constants

        // STEP_COUNT, STEP_INTERVAL, CHECK_INTERVAL
        private static readonly int[] THREAD_ARRAY = { 9, 20, 1 };
        
        // SHADOW_DEPTH, BORDER_THICKNESS, TOTAL_BORDER_THICKNESS
        private static readonly int[] BORDERS = { 6, 1, 2 };

        private static readonly Size DEFAULT_SIZE = new Size(300 + BORDERS[2] + BORDERS[0],
            65 + BORDERS[2] + BORDERS[0]);
        
        // TITLE_FONT, DESCRIPTION_FONT
        private static readonly Font[] FONTS = { 
            new Font("Tahoma", 9.75f, FontStyle.Bold, GraphicsUnit.Point), 
            new Font("Tahoma", 8.75f, FontStyle.Regular, GraphicsUnit.Point) };

        private const int LWA_ALPHA = 0x00000002;
        private const byte AC_SRC_OVER = 0x00;
        private const byte AC_SRC_ALPHA = 0x01;

        #endregion

        #region Static Members Of The Class

        // TITLE_SIZE, MESSAGE_SIZE
        private static Size[] sizeOfMessages = new Size[2];

        #endregion

        #region Instance Members
        
        // This is the thread where the tooltip task is carried out.
        private Thread thread;
        private Guid id = Guid.NewGuid();

        #endregion

        #region Constructor

        public ToolTips()
        {
            this.SetStyle(ControlStyles.FixedWidth | ControlStyles.FixedHeight, true);
            this.SetStyle(ControlStyles.Selectable | ControlStyles.ResizeRedraw, false);
            this.DoubleBuffered = true;

            this.ShowIcon = false;
            this.ControlBox = false;
            this.MaximizeBox = false;
            this.ShowInTaskbar = false;
            this.StartPosition = FormStartPosition.Manual;
            this.FormBorderStyle = FormBorderStyle.None;
        }

        public ToolTips(string title, string description, int value, int maxvalue)
            : this()
        {
            TITLE = title;
            DESCRIPTION = description;
            VALUE = value;
            MAXVALUE = maxvalue;
        }

        #endregion

        #region Destructor

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

        #endregion

        #region Property

        public int VALUE { get; set; }
        public int MAXVALUE { get; set; }
        public string TITLE { get; set; }
        public string DESCRIPTION { get; set; }

        /// <summary>
        /// Gets or sets the tooltip renderer of the tab control.
        /// </summary>
        public TooltipRenderer TooltipRenderer { get; set; }

        /// <summary>
        /// Use a unique ID to track the tooltip task later, if needed.
        /// </summary>
        public Guid ID
        {
            get
            {
                return id;
            }
        }

        /// <summary>
        /// Track the status of the tooltip task.
        /// </summary>
        public StatusState Status { get; private set; }

        /// <summary>
        /// Gets the status of the tooltip thread.
        /// </summary>
        public string TaskLog { get; private set; }

        protected override CreateParams CreateParams
        {
            get
            {
                CreateParams cp = base.CreateParams;
                // This form has to have the WS_EX_LAYERED extended style.
                cp.ExStyle |= (int)WindowExStyles.WS_EX_LAYERED;
                // Hide from ALT+Tab menu, Turn on WS_EX_TOOLWINDOW style.
                cp.ExStyle |= (int)WindowExStyles.WS_EX_TOOLWINDOW;
                return cp;
            }
        }

        protected override bool ShowWithoutActivation
        {
            // True if the window will not be activated when it's shown; otherwise, false.
            get { return true; }
        }

        #endregion

        #region Override Methods

        protected override Size DefaultSize
        {
            get
            {
                return DEFAULT_SIZE;
            }
        }

        #endregion

        #region Helper Methods

        private void CalculateSize()
        {
            using (Graphics g = CreateGraphics())
            {
                sizeOfMessages[0] = MeasureString(g, TITLE, FONTS[0], Int32.MaxValue);
                sizeOfMessages[1] = MeasureString(g, DESCRIPTION, FONTS[1], 576 - (2 * BORDERS[1]));
            }
        }

        private Size MeasureString(Graphics g, string val, Font font, int maxWidth)
        {
            SizeF sizeF = g.MeasureString(val, font, maxWidth);
            return new Size(
                (int)sizeF.Width < maxWidth ? (int)sizeF.Width + 1 : maxWidth,
                (int)sizeF.Height + 1);
        }

        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;
        }

        private void DrawContent(Graphics g)
        {
            g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
            try
            {
                int y = BORDERS[1] + 8;
                int x = BORDERS[1] + 6;
                int x2 = Width - 12 - BORDERS[0] - BORDERS[2];
                using (StringFormat format = new StringFormat(StringFormatFlags.LineLimit))
                {
                    format.Trimming = StringTrimming.EllipsisCharacter;
                    format.HotkeyPrefix = System.Drawing.Text.HotkeyPrefix.Hide;

                    #region Title and ProgressBar
                    if (!String.IsNullOrEmpty(TITLE))
                    {
                        Rectangle titleRect = new Rectangle(
                            x,
                            y,
                            x2 - 55,
                            sizeOfMessages[0].Height);
                        using (Brush titleBrush = new SolidBrush(TooltipRenderer.TitleForeColor))
                            g.DrawString(TITLE, FONTS[0], titleBrush, titleRect, format);

                        #region ProgressBar
                        Rectangle progressRect = new Rectangle(
                            titleRect.Right + 5,
                            y,
                            50,
                            sizeOfMessages[0].Height / 3);
                        using (ProgressBarPainter pPainter =
                            new ProgressBarPainter(TooltipRenderer.BarBorderColor,
                            TooltipRenderer.BarBackgroundColorStart, TooltipRenderer.BarBackgroundColorEnd,
                            TooltipRenderer.BarProgressColorStart, TooltipRenderer.BarProgressColorEnd))
                        {
                            pPainter.VALUE = VALUE;
                            pPainter.MAXVALUE = MAXVALUE;
                            pPainter.PaintProgressBar(g, progressRect);
                        }
                        y = titleRect.Bottom + 1;
                        #endregion
                    }
                    #endregion

                    #region Message
                    if (!String.IsNullOrEmpty(DESCRIPTION))
                    {
                        Rectangle messageRect = new Rectangle(
                            x,
                            y + 4,
                            x2,
                            sizeOfMessages[1].Height);
                        using (Brush messageBrush = new SolidBrush(TooltipRenderer.MessageForeColor))
                            g.DrawString(DESCRIPTION, FONTS[1], messageBrush, messageRect, format);
                    }
                    #endregion
                }
            }
            catch { ;}
        }

        private void DrawShadow(Graphics g)
        {
            if (BORDERS[0] > 0)
            {
                Rectangle shadowRect = new Rectangle(
                    BORDERS[0],
                    BORDERS[0],
                    Width - BORDERS[0],
                    Height - BORDERS[0]);
                if (shadowRect.Width > 0 && shadowRect.Height > 0)
                {
                    using (GraphicsPath shadowPath = CreateRoundRect(shadowRect, TooltipRenderer.Radius))
                    {
                        using (PathGradientBrush shadowBrush = new PathGradientBrush(shadowPath))
                        {
                            Color[] colors = new Color[4];
                            float[] positions = new float[4];
                            ColorBlend sBlend = new ColorBlend();
                            colors[0] = Color.FromArgb(0, 0, 0, 0);
                            colors[1] = Color.FromArgb(32, 0, 0, 0);
                            colors[2] = Color.FromArgb(64, 0, 0, 0);
                            colors[3] = Color.FromArgb(128, 0, 0, 0);
                            positions[0] = 0.0f;
                            positions[1] = 0.015f;
                            positions[2] = 0.030f;
                            positions[3] = 1.0f;
                            sBlend.Colors = colors;
                            sBlend.Positions = positions;
                            shadowBrush.InterpolationColors = sBlend;
                            shadowBrush.CenterPoint = new Point(
                                shadowRect.Left + (shadowRect.Width / 2),
                                shadowRect.Top + (shadowRect.Height / 2));
                            g.FillPath(shadowBrush, shadowPath);
                        }
                    }
                }
            }
        }

        private void DrawBackground(Graphics g)
        {
            Rectangle rct = new Rectangle(0, 0,
                Width - BORDERS[0] - BORDERS[1],
                Height - BORDERS[0] - BORDERS[1]);
            if (rct.Width > 0 && rct.Height > 0)
            {
                using (GraphicsPath path = CreateRoundRect(rct, TooltipRenderer.Radius))
                {
                    using (Brush brush = new LinearGradientBrush(
                        rct, TooltipRenderer.DarkBackgroundColor, TooltipRenderer.LightBackgroundColor,
                        LinearGradientMode.Vertical))
                        g.FillPath(brush, path);
                    using (Pen pen = new Pen(TooltipRenderer.BorderColor))
                        g.DrawPath(pen, path);
                }
            }
        }

        /// <summary>
        /// Creates a new transparent layered window.
        /// </summary>
        /// <param name="bmp">To be layered bitmap</param>
        /// <param name="transparency">Alpha value</param>
        private void GDIWindow(Bitmap bmp, byte transparency)
        {
            IntPtr hDC = GetDC(IntPtr.Zero);
            try
            {
                IntPtr hMemDC = CreateCompatibleDC(hDC);
                try
                {
                    IntPtr hBmp = bmp.GetHbitmap(Color.FromArgb(0));
                    try
                    {
                        IntPtr previousBmp = SelectObject(hMemDC, hBmp);
                        try
                        {
                            Point ptDst = new Point(Left, Top);
                            Size size = new Size(bmp.Width, bmp.Height);
                            Point ptSrc = new Point(0, 0);
                            BLENDFUNCTION blend = new BLENDFUNCTION();
                            blend.BlendOp = AC_SRC_OVER;
                            blend.BlendFlags = 0;
                            blend.SourceConstantAlpha = transparency;
                            blend.AlphaFormat = AC_SRC_ALPHA;
                            UpdateLayeredWindow(
                                Handle,
                                hDC,
                                ref ptDst,
                                ref size,
                                hMemDC,
                                ref ptSrc,
                                0,
                                ref blend,
                                LWA_ALPHA);
                        }
                        finally
                        {
                            SelectObject(hDC, previousBmp);
                        }
                    }
                    finally
                    {
                        DeleteObject(hBmp);
                    }
                }
                finally
                {
                    DeleteDC(hMemDC);
                }
            }
            finally
            {
                ReleaseDC(IntPtr.Zero, hDC);
            }
        }

        /// <summary>
        /// Tooltip worker method.
        /// </summary>
        private void DoTask()
        {
            Size size = DefaultSize;
            using (Bitmap overlay = new Bitmap(size.Width, size.Height))
            {
                using (Graphics g = Graphics.FromImage(overlay))
                {
                    g.SmoothingMode = SmoothingMode.AntiAlias;
                    DrawShadow(g);
                    DrawBackground(g);
                    DrawContent(g);
                }
                /* Initial alpha value is 25. */
                byte transparency = 25;
                for (int i = 1; i <= THREAD_ARRAY[0]; i++)
                {
                    Invoke(new UIThreadPainterDelegate(GDIWindow), 
                        new object[] { overlay, transparency });
                    transparency += 25;
                    // Pause specified milliseconds before the next pass.
                    Thread.Sleep(TimeSpan.FromMilliseconds(THREAD_ARRAY[1]));
                }
                DateTime startTime = DateTime.Now;
                do
                {
                    /* Still waiting for 3 seconds time limit to pass. */
                    Thread.Sleep(TimeSpan.FromSeconds(THREAD_ARRAY[2]));
                }
                while (DateTime.Now.Subtract(startTime).TotalSeconds < 3);
                transparency = 200;
                for (int i = 1; i <= THREAD_ARRAY[0] - 1; i++)
                {
                    Invoke(new UIThreadPainterDelegate(GDIWindow), 
                        new object[] { overlay, transparency });
                    transparency -= 25;
                    // Pause specified milliseconds before the next pass.
                    Thread.Sleep(TimeSpan.FromMilliseconds(THREAD_ARRAY[1]));
                }
            }
        }

        /// <summary>
        /// Tooltip thread method.
        /// </summary>
        private void StartTaskAsync()
        {
            try
            {
                DoTask();
                Status = StatusState.Completed;
                Invoke(new EventHandler(OnTaskCompleted),
                    new object[] { this, EventArgs.Empty });
                // Write log message.
                TaskLog = String.Format("Tooltip task completed !!!, {0} is completed succesfully at {1:F}",
                    Thread.CurrentThread.Name, DateTime.Now);
            }
            catch (ThreadAbortException)
            {
                // Write log message.
                TaskLog = String.Format("Tooltip task aborted !!!, {0} is destroyed and stopped at {1:F}",
                    Thread.CurrentThread.Name, DateTime.Now);
            }
        }

        private void OnTaskCompleted(object sender, EventArgs e)
        {
            // Signal that the tooltip operation is completed.
            if (TaskCompleted != null)
                TaskCompleted(this, EventArgs.Empty);
        }

        #endregion

        #region General Methods

        /// <summary>
        /// Stops the currently running tooltip task.
        /// </summary>
        /// <param name="cancelWaitTime">
        /// How long the thread will wait before aborting a tooltip thread that hasn't responded to the cancellation message.
        /// TimeSpan.Zero means polite stops are not enabled.
        /// </param>
        public void StopAsyncTooltip(TimeSpan cancelWaitTime)
        {
            // Perform no operation if tooltip task isn't running.
            if (Status != StatusState.InProgress)
                return;
            // Try the polite approach.
            if (cancelWaitTime != TimeSpan.Zero)
            {
                DateTime startTime = DateTime.Now;
                // Write log message.
                TaskLog = String.Format("Tooltip task cancellation pending request !!!, {0} is aborting now {1:F}", 
                    thread.Name, startTime);
                do
                {
                    /* Still waiting for the time limit to pass.
                       Allow other threads to do some work. */
                    Thread.Sleep(TimeSpan.FromSeconds(THREAD_ARRAY[2]));
                }
                while (DateTime.Now.Subtract(startTime).TotalSeconds < cancelWaitTime.Seconds);
            }
            if (thread.IsAlive)
            {
                // Tell the task thread to abort itself immediately, raises a ThreadAbortException in the task thread after calling the Thread.Join() method.
                thread.Abort();
                // Wait for the tooltip thread to finish.
                thread.Join();
            }
        }

        /// <summary>
        /// Starts a new tooltip thread in background.
        /// </summary>
        public void StartAsyncTooltip()
        {
            if (Status == StatusState.InProgress)
                throw new InvalidOperationException("Already in progress...");
            else
            {
                // Calculate message sizes.
                CalculateSize();
                // Initialize the new task.
                Status = StatusState.InProgress;
                /* Create the thread and run it in the background so
                   it will terminate automatically if the application ends. */
                thread = new Thread(StartTaskAsync);
                thread.Name = String.Format("Tooltip thread({0})", ID);
                thread.IsBackground = true;
                // Start the thread.
                thread.Start();
                // Write log message.
                TaskLog = String.Format("Tooltip task started !!!, {0} is started at {1:F}", 
                    thread.Name, DateTime.Now);
            }
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

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