Pull to refresh

Расширение функциональности стандартного WinForms TabControl

Reading time6 min
Views5.7K
Случилось недавно так, что понадобилось в одном проекте использовать компонент TabControl. Стандартный компонент, ничего необычного, достаточно удобный. Нюанс заключался в том, что нужно было использовать свой тип вкладок на основе перегруженного TabPage. Кроме этого, необходимо было позволить пользователю добавлять вкладки самому в процессе работы. Выглядеть оно должно было примерно так:
image

Радует, что стандартные компоненты позволяют делать с ними достаточно много извращений.


Линк на тестовый проект с примером. Под 2011 2010 студию.

Начнем с кастомного типа вкладок.
К сожалению, TabControl, в отличие от DataGridViewColumn, не позволяет указывать тип вложенного элемента через внутреннюю переменную. Так это делается в DataGridViewColumn:

        public class CustomDataGridViewColumn : DataGridViewTextBoxColumn
        {
            public CustomDataGridViewColumn()
            {
                this->CellTemplate = CustomDataGridViewCell;
            }
        }


Проект должен быть изменен под Net 4 Full profile, т.к. нужно подключать две сборки — System.Design и System.Drawing.Design.

Увы, сделать аналогично в TabControl не получится, придется переопределять тип TabPageCollection, объявленный внутри TabControl. Итак, объявляем сам класс и необходимые методы, далее разберем по очереди. Да, и сразу предупрежу — внутри файла с кодом не было строчки «using System.Windows.Forms;», поэтому обращение к стандартным компонентам в коде примера идет с приставкой «System.Windows.Forms.». А TabControl и TabPage это кастомные переопределенные типы.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Drawing.Design;
using System.ComponentModel.Design;

namespace WindowsFormsApplication1
{
    public class TabControl : System.Windows.Forms.TabControl
    {
        //Объявляем новый тип коллекции вкладок. 
        public new class TabPageCollection : System.Windows.Forms.TabControl.TabPageCollection
        {
            public TabPageCollection(TabControl owner)
                : base(owner)
            {

            }
        }

        public TabControl()
        {

        }

        private TabPageCollection mTabCollection = null;

        //Переопределяем свойство для доступа к коллекции вкладок, указывая в атрибуте Editor тип редактора коллекции.
        [Editor(typeof(TabPageCollectionEditor), typeof(UITypeEditor))]
        public new System.Windows.Forms.TabControl.TabPageCollection TabPages
        {
            get
            {
                if (mTabCollection == null) mTabCollection = new TabPageCollection(this);
                return mTabCollection;
            }
        }
    }

    //Редактор коллекции вкладок тоже пришлось переопределять. В нем всего два перегруженных метода.
    public class TabPageCollectionEditor : System.ComponentModel.Design.CollectionEditor
    {
        public TabPageCollectionEditor(Type type)
            : base(type)
        {

        }

        protected override Type CreateCollectionItemType()
        {
            return typeof(TabPage);
        }

        protected override Type[] CreateNewItemTypes()
        {
            return new Type[] { typeof(TabPage) };
        }
    }
}


И простенький TabPage:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WindowsFormsApplication1
{
    public class TabPage : System.Windows.Forms.TabPage
    {
        public TabPage()
        {

        }
    }
}


Думаю, тут все понятно. Переходим к следующему этапу.

Добавление вкладок пользователем.
Для обеспечения этой возможности мы пошли самым простым путем: в TabControl постоянно присутствует дополнительная пустая вкладка с заголовком "+". При её нажатии генерируется событие добавления вкладки.

Во-первых, в типе TabPageCollection перегружаем метод Clear — при очищении вкладок нам НЕ надо удалять вкладку "+".

            public override void Clear()
            {
                System.Windows.Forms.TabPage page = null;
                if (this.ContainsKey(TabControl.KeyPageAllowAddName)) page = this[TabControl.KeyPageAllowAddName];

                base.Clear();

                if (page != null) this.Add(page);
            }


В коде TabControl объявляем переменную, которая хранит имя вкладки "+".

        public static string KeyPageAllowAddName = "___page_allow_to_add_name___";


Объявляем свойство AllowUserToAddTab компонента для того чтобы можно было включать/отключать новый режим работы. Ну и собственно метод, проверяющий наличие такой вкладки и в случае необходимости добавляющий/удаляющий её.


        public TabControl()
        {
            this.Enter += new EventHandler((sender, e) => { CheckAllowUserToAddTab(); });
            this.Selecting += new System.Windows.Forms.TabControlCancelEventHandler(TabControl_Selecting);
        }

        private bool mAllowUserToAddTab = false;
        [Browsable(true), Description("Позволяет пользователю добавлять вкладки. Добавляется элемент +, по щелчку на который создается событие OnUserAddedTab."), Category("Action")]
        public virtual bool AllowUserToAddTab
        {
            get { return mAllowUserToAddTab; }
            set { mAllowUserToAddTab = value; }
        }

       void CheckAllowUserToAddTab()
        {
            if (mAllowUserToAddTab)
            {
                System.Windows.Forms.TabPage page_allow_to_add = TabPages[KeyPageAllowAddName];

                if (mAllowUserToAddTab)
                {
                    if (page_allow_to_add == null)
                    {
                        page_allow_to_add = new TabPage();
                        page_allow_to_add.Name = KeyPageAllowAddName;
                        page_allow_to_add.Text = "+";
                        TabPages.Insert(0, page_allow_to_add);
                    }
                }
                else
                {
                    if (page_allow_to_add != null) TabPages.Remove(page_allow_to_add);
                }
            }
        }



Описываем делегат для события добавления вкладки:

    public delegate bool TabPageAdding(TabControl control, TabPage page);


Событие и метод для добавления вкладки:

        public event TabPageAdding PageAdding;
        void TabControl_Selecting(object sender, System.Windows.Forms.TabControlCancelEventArgs e)
        {
            if (TabPages.Count > 0 && e.TabPage.Name == KeyPageAllowAddName)
            {
                e.Cancel = true;

                TabPage page = new TabPage();
                if (PageAdding != null)
                    foreach (TabPageAdding _delegate in PageAdding.GetInvocationList())
                    {
                        try
                        {
                            if (!_delegate.Invoke(this, page)) return;
                        }
                        catch (Exception ex) { System.Windows.Forms.MessageBox.Show(ex.Message); }
                    }

                TabPages.Add(page);
            }
        }

Перебирается список всех подписчиков на событие добавления, если хоть один из них вернет False — добавление отменяется.

В классе TabPageCollectionEditor надо перегрузить методы GetItems и SetItems. Они нужны для передачи массива вкладок из редактора в компонент и обратно, при этом надо исключать вкладку "+".

        protected override object[] GetItems(object editValue)
        {
            try
            {
                object[] values = base.GetItems(editValue);
                List<object> values2 = new List<object>();
                foreach (var element in values)
                {
                    if (element.GetType() == typeof(TabPage))
                    {
                        TabPage tp = (TabPage)element;
                        if (tp.Name == TabControl.KeyPageAllowAddName) continue;
                    }
                    values2.Add(element);
                }
                return values2.ToArray();
            }
            catch (Exception ex){System.Windows.Forms.MessageBox.Show(ex.Message);}
            return base.GetItems(editValue);
        }

        protected override object SetItems(object editValue, object[] value)
        {
            try
            {
                List<object> values2 = new List<object>();
                foreach (var element in value)
                {
                    if (element.GetType() == typeof(TabPage))
                    {
                        TabPage tp = (TabPage)element;
                        if (tp.Name == TabControl.KeyPageAllowAddName) continue;
                    }
                    values2.Add(element);
                }
                return base.SetItems(editValue, values2.ToArray());
            }
            catch (Exception ex) { System.Windows.Forms.MessageBox.Show(ex.Message); }
            return base.SetItems(editValue, value);
        }


Далее добавили еще одну возможность: отслеживание первого открытия вкладки (по первому заходу на вкладку инициализировались формочки)

Тут добавляется еще одно событие, список и обработчик событий SelectedIndexChanged/Enter/Click:

        public TabControl()
        {
            this.Enter += new EventHandler(TabControl_PageEvent);
            this.Click += new EventHandler(TabControl_PageEvent);
            this.SelectedIndexChanged += new EventHandler(TabControl_PageEvent);

            this.Enter += new EventHandler((sender, e) => { CheckAllowUserToAddTab(); });
            this.Selecting += new System.Windows.Forms.TabControlCancelEventHandler(TabControl_Selecting);
        }

        ....

        private Dictionary<System.Windows.Forms.TabPage, bool> mLoaded = new Dictionary<System.Windows.Forms.TabPage, bool>();
        public delegate void TabPageLoadedEventHandler(TabControl control, System.Windows.Forms.TabPage page);
        public event TabPageLoadedEventHandler PageLoad;
        void TabControl_PageEvent(object sender, EventArgs e)
        {
                if (this.SelectedTab != null && !mLoaded.ContainsKey(this.SelectedTab))
                {
                    mLoaded.Add(this.SelectedTab, true);
                    if (PageLoad != null) PageLoad(this, this.SelectedTab);
                }
        }


Конечно, дорабатывать тут еще можно много чего — например, в TabPageControlCollection перегружать методы для более точного определения номера вкладки (чтобы исключать вкладку "+").
К сожалению, я не нашел способа реализовать функциональность вкладки "+" через перегрузку методов отрисовки компонента, так что не обессудьте за кривой способ. Имхо как альтернатива установке DevExpress, например — сойдет.
Tags:
Hubs:
+3
Comments4

Articles

Change theme settings