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

в 13:27, , рубрики: .net, forms, framework, tabcontrol, метки: , , , ,

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

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

Линк на тестовый проект с примером. Под 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


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js