Случилось недавно так, что понадобилось в одном проекте использовать компонент TabControl. Стандартный компонент, ничего необычного, достаточно удобный. Нюанс заключался в том, что нужно было использовать свой тип вкладок на основе перегруженного TabPage. Кроме этого, необходимо было позволить пользователю добавлять вкладки самому в процессе работы. Выглядеть оно должно было примерно так:
Радует, что стандартные компоненты позволяют делать с ними достаточно много извращений.
Линк на тестовый проект с примером. Под 2011 студию.
Начнем с кастомного типа вкладок.
К сожалению, 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, например — сойдет.
Автор: Sellec