Случилось недавно так, что понадобилось в одном проекте использовать компонент TabControl. Стандартный компонент, ничего необычного, достаточно удобный. Нюанс заключался в том, что нужно было использовать свой тип вкладок на основе перегруженного TabPage. Кроме этого, необходимо было позволить пользователю добавлять вкладки самому в процессе работы. Выглядеть оно должно было примерно так:
Радует, что стандартные компоненты позволяют делать с ними достаточно много извращений.
Линк на тестовый проект с примером. Под2011 2010 студию.
Начнем с кастомного типа вкладок.
К сожалению, TabControl, в отличие от DataGridViewColumn, не позволяет указывать тип вложенного элемента через внутреннюю переменную. Так это делается в DataGridViewColumn:
Проект должен быть изменен под Net 4 Full profile, т.к. нужно подключать две сборки — System.Design и System.Drawing.Design.
Увы, сделать аналогично в TabControl не получится, придется переопределять тип TabPageCollection, объявленный внутри TabControl. Итак, объявляем сам класс и необходимые методы, далее разберем по очереди. Да, и сразу предупрежу — внутри файла с кодом не было строчки «using System.Windows.Forms;», поэтому обращение к стандартным компонентам в коде примера идет с приставкой «System.Windows.Forms.». А TabControl и TabPage это кастомные переопределенные типы.
И простенький TabPage:
Думаю, тут все понятно. Переходим к следующему этапу.
Добавление вкладок пользователем.
Для обеспечения этой возможности мы пошли самым простым путем: в TabControl постоянно присутствует дополнительная пустая вкладка с заголовком "+". При её нажатии генерируется событие добавления вкладки.
Во-первых, в типе TabPageCollection перегружаем метод Clear — при очищении вкладок нам НЕ надо удалять вкладку "+".
В коде TabControl объявляем переменную, которая хранит имя вкладки "+".
Объявляем свойство AllowUserToAddTab компонента для того чтобы можно было включать/отключать новый режим работы. Ну и собственно метод, проверяющий наличие такой вкладки и в случае необходимости добавляющий/удаляющий её.
Описываем делегат для события добавления вкладки:
Событие и метод для добавления вкладки:
Перебирается список всех подписчиков на событие добавления, если хоть один из них вернет False — добавление отменяется.
В классе TabPageCollectionEditor надо перегрузить методы GetItems и SetItems. Они нужны для передачи массива вкладок из редактора в компонент и обратно, при этом надо исключать вкладку "+".
Далее добавили еще одну возможность: отслеживание первого открытия вкладки (по первому заходу на вкладку инициализировались формочки)
Тут добавляется еще одно событие, список и обработчик событий SelectedIndexChanged/Enter/Click:
Конечно, дорабатывать тут еще можно много чего — например, в TabPageControlCollection перегружать методы для более точного определения номера вкладки (чтобы исключать вкладку "+").
К сожалению, я не нашел способа реализовать функциональность вкладки "+" через перегрузку методов отрисовки компонента, так что не обессудьте за кривой способ. Имхо как альтернатива установке DevExpress, например — сойдет.
Радует, что стандартные компоненты позволяют делать с ними достаточно много извращений.
Линк на тестовый проект с примером. Под
Начнем с кастомного типа вкладок.
К сожалению, 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, например — сойдет.