Creating a Custom TabControl in C# WinForms - Issues with Drag-and-Drop in Design-time

95 Views Asked by At

While creating my custom TabControl (named MainTabControl), I encounter 2 problems:

  1. Can't drag and drop controls from Designer Toolbox, because MainTabControl is automatically selected instead of current TabPage, when my mouse is over. When I release drag, I get the error.

"Cannot add 'Control' to TabControl. Only TabPages can be directly added to TabControls".

  1. When MainTabControl doesn't have tabs it can't be selected with mouse click, only lasso selection works.

Designer

using System;
using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
using System.Security.Permissions;
using System.Windows.Forms;
using System.Windows.Forms.Design;

namespace ThunderbirdLibrary.CustomControls
{
    [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
    public class MainTabControlDesigner : ParentControlDesigner
    {
        DesignerActionListCollection _actionLists;
        DesignerVerbCollection _verbs;
        IDesignerHost _designerHost;
        IComponentChangeService _changeService;
        bool clicked = false;

        public override DesignerActionListCollection ActionLists
        {
            get
            {
                if (_actionLists == null)
                {
                    _actionLists = new DesignerActionListCollection
                    {
                        new MainTabControlActionList(this.Component)
                    };
                }

                return _actionLists;
            }
        }

        public override DesignerVerbCollection Verbs
        {
            get
            {
                if(_verbs == null)
                {
                    _verbs = new DesignerVerbCollection()
                    {
                        new DesignerVerb("Add Tab", new EventHandler(OnAddTab)),
                        new DesignerVerb("Remove Tab", new EventHandler(OnRemoveTab))
                    };

                    MainTabControl mainTabControl = Control as MainTabControl;
                    if (mainTabControl != null)
                    {
                        if (mainTabControl.TabPages.Count == 0) _verbs[1].Enabled = false;

                        else _verbs[1].Enabled = true;
                    }
                }

                return _verbs;
            }
        }

        public override void Initialize(IComponent component)
        {
            base.Initialize(component);

            _designerHost = (IDesignerHost)GetService(typeof(IDesignerHost));
            _changeService = (IComponentChangeService)GetService(typeof(IComponentChangeService));

            if (_changeService != null)
                _changeService.ComponentChanged += new ComponentChangedEventHandler(OnComponentChanged);
        }

        public override void InitializeNewComponent(IDictionary defaultValues)
        {
            base.InitializeNewComponent(defaultValues);
            
            try
            {
                MainTabControl control = Control as MainTabControl;

                OnAddTab(this, EventArgs.Empty);

                control.SelectedIndex = 0;
                ((MainTabPage)control.TabPages[0]).ImageName = "AccountSetup";
            }
            catch
            {
                throw new Exception("Couldn't initialize MainTabControl with MainTabPage");
            }
        }


        // Properties Window by default is updated only after select and deselect the control
        // Here "ComponentChanging" and "ComponentChanged" comes in play and notify IDE designer about the changes
        public void OnAddTab(object sender, EventArgs e)
        {
            MainTabControl parentControl = Control as MainTabControl;

            DesignerTransaction transaction = null;
            try
            {
                transaction = _designerHost.CreateTransaction("Add Tab");

                RaiseComponentChanging(TypeDescriptor.GetProperties(parentControl)["TabPages"]);

                MainTabPage newPage = (MainTabPage)_designerHost.CreateComponent(typeof(MainTabPage));
                newPage.Text = newPage.Name;
                parentControl.TabPages.Add(newPage);
                parentControl.SelectedTab = newPage;

                RaiseComponentChanged(TypeDescriptor.GetProperties(parentControl)["TabPages"], null, null);

                transaction.Commit();
            }
            catch
            {
                MessageBox.Show("Exception occured during adding the tab");
                transaction?.Cancel();
            }

        }

        public void OnRemoveTab(object sender, EventArgs e)
        {
            MainTabControl parentControl = Control as MainTabControl;

            DesignerTransaction transaction = null;
            try
            {
                transaction = _designerHost.CreateTransaction("Remove Tab");

                RaiseComponentChanging(TypeDescriptor.GetProperties(parentControl)["TabPages"]);

                _designerHost.DestroyComponent(parentControl.SelectedTab);

                RaiseComponentChanged(TypeDescriptor.GetProperties(parentControl)["TabPages"], null, null);
                
                transaction.Commit();
            }
            catch
            {
                MessageBox.Show("Exception occured during removing the tab");
                transaction?.Cancel();
            }
        }

        public void OnComponentChanged(object sender, ComponentChangedEventArgs e)
        {
            MainTabControl parentControl = e.Component as MainTabControl;
            
            if(parentControl != null && e.Member.Name == "TabPages")
            {
                foreach (DesignerVerb verb in Verbs)
                {
                    if(verb.Text == "Remove Tab")
                    {
                        if (parentControl.TabPages.Count == 0) verb.Enabled = false;

                        else verb.Enabled = true;
                    }
                }
            }
        }

        // Determine whether to pass click to the control
        protected override bool GetHitTest(Point point)
        {
            ISelectionService selectionService = (ISelectionService)GetService(typeof(ISelectionService));

            object selectedObject = selectionService.PrimarySelection;

            return selectedObject != null && selectedObject.Equals(Control);
        }
    }
}

MainTabControl.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Design;
using System.Linq;
using System.Resources;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Media.Animation;

namespace ThunderbirdLibrary.CustomControls;

[Designer(typeof(MainTabControlDesigner))]
public partial class MainTabControl : TabControl
{
    #region Private Members

    private readonly Size DefaultItemSize;
    //private int mouseAreaTriggerOffset = 3;
    private readonly int standardOffset;

    private Rectangle imageRec;
    private Rectangle textRec;
    private Rectangle closeButtonRec;
    private Rectangle closeAreaRec;
    
    private int ellipsisWidth;
    private bool isRecalculatingTabSize;

    private Point mouseDownLocation;

    #endregion

    #region Properties

    [Editor(typeof(MainTabPageCollectionEditor), typeof(UITypeEditor))]
    public new TabPageCollection TabPages
    {
        get
        {
            return base.TabPages;
        }
    }

    #endregion

    #region Constructor
    public MainTabControl()
    {
        InitializeComponent();

        this.DrawMode = TabDrawMode.OwnerDrawFixed;
        this.SizeMode = TabSizeMode.Fixed;
        SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true);

        isRecalculatingTabSize = false;

        // Set Sizes of header parts that (probably) won't be recalculated
        DefaultItemSize = new Size(250, 33);

        standardOffset = (int)(this.DefaultItemSize.Width * 0.02);

        imageRec = new Rectangle(
            standardOffset,
            standardOffset,
            DefaultItemSize.Height / 2,
            DefaultItemSize.Height / 2);

        closeAreaRec = new Rectangle(
            DefaultItemSize.Width - standardOffset - DefaultItemSize.Height / 2,
            (DefaultItemSize.Height - DefaultItemSize.Height / 2) / 2,
            DefaultItemSize.Height / 2,
            DefaultItemSize.Height / 2);

        using (Graphics g = this.CreateGraphics())
        {
            closeButtonRec = new Rectangle(
                new Point(
                    DefaultItemSize.Width - closeAreaRec.Width - standardOffset + closeButtonRec.Width / 4,
                    (DefaultItemSize.Height - closeAreaRec.Height) / 2
                ),
                g.MeasureString("x", Font).ToSize());

            ellipsisWidth = (int)g.MeasureString("...", Font).Width;


            textRec = new Rectangle(
                2 * standardOffset + imageRec.Width,
                (int)((DefaultItemSize.Height - g.MeasureString("Test String", Font).Height) / 2),
                0, 
                0);
        }
    }

    #endregion

    #region EventHandler

    protected override void OnControlRemoved(ControlEventArgs e)
    {
        base.OnControlRemoved(e);

        if (e.Control is MainTabPage)
        {
            RecalculateTabSize();
        }
    }

    protected override void OnControlAdded(ControlEventArgs e)
    {
        base.OnControlAdded(e);

        if(e.Control is MainTabPage)
        {
            RecalculateTabSize();
        }

    }

    protected override void OnSizeChanged(EventArgs e)
    {
        if (!isRecalculatingTabSize)
        {
            base.OnSizeChanged(e);

            isRecalculatingTabSize = true;
            RecalculateTabSize();
            isRecalculatingTabSize = false;
        }
    }

    protected override void OnMouseDown(MouseEventArgs e)
    {
        base.OnMouseDown(e);

        mouseDownLocation = e.Location;
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        base.OnMouseMove(e);

        for (int i = 0; i < this.TabCount; i++)
        {
            Rectangle tabRec = this.GetTabRect(i);
            Rectangle absoluteCloseArea = new Rectangle()
            {
                Size = closeAreaRec.Size,
                Location = new Point()
                {
                    X = tabRec.X + closeAreaRec.X,
                    Y = tabRec.Y + ItemSize.Height / 4
                }
            };

            MainTabPage page = TabPages[i] as MainTabPage;

            if (absoluteCloseArea.Contains(e.Location))
            {
                page.isMouseOverCloseArea = true;
                Invalidate(absoluteCloseArea);
            }
            else if (page.isMouseOverCloseArea)
            {
                page.isMouseOverCloseArea = false;
                Invalidate(absoluteCloseArea);
            }
        }
    }

    protected override void OnMouseClick(MouseEventArgs e)
    {
        base.OnMouseClick(e);

        for (int i = 0; i < this.TabCount; i++)
        {
            Rectangle tabRec = this.GetTabRect(i);
            Rectangle absoluteCloseArea = new Rectangle()
            {
                Size = closeAreaRec.Size,
                Location = new Point()
                {
                    X = tabRec.X + closeAreaRec.X,
                    Y = tabRec.Y + ItemSize.Height / 4
                }
            };

            if (absoluteCloseArea.Contains(e.Location) && absoluteCloseArea.Contains(mouseDownLocation))
            {
                TabPages.RemoveAt(i);
            }
        }
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        for(int i = 0; i < TabCount; i++)
        {
            MainTabPage mainTabPage = TabPages[i] as MainTabPage;

            if(mainTabPage != null)
            {
                Rectangle tabRec = this.GetTabRect(i);

                // Background
                using (Brush recBrush = new SolidBrush(i == SelectedIndex ?
                    mainTabPage.SelectedTabBackColor : mainTabPage.TabBackColor))
                using (Pen recPen = new Pen(Brushes.Black))
                {
                    e.Graphics.FillRectangle(recBrush, tabRec);
                    e.Graphics.DrawRectangle(recPen, tabRec);
                }


                // Image
                if (mainTabPage.HasImage)
                {
                    using(Image image = (Image)Resource.ResourceManager.GetObject(mainTabPage.ImageName))
                    {
                        e.Graphics.DrawImage(image, new Rectangle(
                            tabRec.Left + imageRec.X,
                            tabRec.Top + imageRec.Y,
                            imageRec.Width,
                            imageRec.Height));
                    }
                }

                // Text
                bool isTabSelected = i == this.SelectedIndex;
                var trimmingResult = TrimString(textRec.Size, Font, mainTabPage.Text, isTabSelected, e.Graphics);

                e.Graphics.DrawString(trimmingResult.text, Font, Brushes.White,
                    tabRec.Left + textRec.X, tabRec.Top + textRec.Y);

                // Close button AND Close Area
                if (mainTabPage.IsClosable && trimmingResult.isCloseButton)
                {
                    if (mainTabPage.isMouseOverCloseArea)
                    {
                        SolidBrush closeAreaColor = (i == this.SelectedIndex)
                            ? new SolidBrush(mainTabPage.SelectedtabCloseAreaColor)
                            : new SolidBrush(mainTabPage.TabCloseAreaColor);

                        e.Graphics.FillRectangle(closeAreaColor, new Rectangle(
                            tabRec.Left + closeAreaRec.X,
                            tabRec.Top + closeAreaRec.Y,
                            closeAreaRec.Width,
                            closeAreaRec.Height));

                        closeAreaColor.Dispose();
                    }

                    e.Graphics.DrawString("x", Font, Brushes.White,
                        tabRec.Left + closeButtonRec.X, tabRec.Top + closeButtonRec.Y);
                }

                // Upperline
                if (mainTabPage.IsUpperLine && isTabSelected)
                {
                    using (Pen pen = new Pen(mainTabPage.SelectedTabUpperLine, 3))
                    {
                        e.Graphics.DrawLine(pen, tabRec.Left + standardOffset, 4, tabRec.Right - standardOffset, 4);
                    }
                }
            }
        }
    }

    #endregion

    #region Helper Methods

    private (string text, bool isCloseButton) TrimString(Size maxSize, Font font, string text, bool isSelected, Graphics g)
    {
        int currentWidth = (int)g.MeasureString(text, font).Width;

        if (currentWidth < textRec.Width)
        {
            return (text, true);
        }
        else
        {
            // Decide if text area merge with close button area size
            int availableWidthForText = isSelected ? textRec.Width : textRec.Width + standardOffset + closeAreaRec.Width;

            while (currentWidth + ellipsisWidth >= availableWidthForText && text.Length > 0)
            {
                text = text.Substring(0, text.Length - 1);
                currentWidth = (int)g.MeasureString(text, font).Width;
            }

            return (string.Concat(text, "..."), false || isSelected);
        }
    }

    public void RecalculateTabSize()
    {
        if (TabPages.Count == 0)
            return;

        int averageTabWidth = this.Size.Width / this.TabPages.Count;

        if (averageTabWidth <= 0) 
            return;

        this.ItemSize = averageTabWidth > DefaultItemSize.Width ?
                    DefaultItemSize : new Size(averageTabWidth - 1, DefaultItemSize.Height);

        // Recalculate text, closeArea and close Button Rectangles
        textRec = new Rectangle(
            textRec.X,
            textRec.Y,
            this.ItemSize.Width - 4 * standardOffset - imageRec.Width - closeAreaRec.Width,
            this.ItemSize.Height / 2);

        closeAreaRec = new Rectangle(
            ItemSize.Width - standardOffset - ItemSize.Height / 2,
            (ItemSize.Height - ItemSize.Height / 2) / 2,
            ItemSize.Height / 2,
            ItemSize.Height / 2);

        closeButtonRec = new Rectangle(
            new Point(
                ItemSize.Width - closeAreaRec.Width - standardOffset + closeButtonRec.Width / 4,
                (ItemSize.Height - closeAreaRec.Height) / 2),
            closeAreaRec.Size);
    }

    #endregion
}

1

There are 1 best solutions below

0
NeverDoomedEnough On

I've found a workaround.