воскресенье, 22 января 2017 г.

C#. Создаем универсальный TextBox с кнопкой

Данный контрол (с возможными незначительными изменениями) добавлен в библиотеку Net.KSUniLib с версии 1.0.3.19.

Namespace: KomeSoftUniCL.Controls.DataBases
Class: KSDBSelectTextBox

Сегодня мы будем создавать интересный контрол, представляющий собой TextBox и встроенной кнопкой:







Такие контролы используются, в основном, в программах, работающих с базами данных. Однако, путем универсализации его функционала, его ареал использования можно существенно расширить.

Итак, что нам нужно от такого контрола?
1) Открытие какой-либо формы в модальном режиме. Какая форма будет открываться, контрол не знает, и какие данные форма будет возвращать, контрол тоже представлять не будет.
2) Вызов диалога открытия файла. Да, реализовать подобный функционал в коде на C# несложно, и примеров в сети по данному вопросу гора. Но согласитесь, если тоже самое можно сделать установкой одного свойства в контроле - это удобно.
3) Вызов диалога сохранения файла. Аргументы те же, что и в п.2.
4) Вызов диалога выбора директории.

Кроме того, мы хотим иметь возможность в коде формы, на которой расположен данный контрол, управлять его выполнением. Например, интерактивно отключить выполнение события выбора без замораживания самого контрола, и т.д.




Иии, начнемс...

Создаем новый User Control. Я, для простоты, буду называть все классы, свойства и т.д. теми же именами, которые использовал в библиотеке. Вы можете использовать любые собственные имена.

Для начала, в режиме дизайнера форм рисуем контрол. Я в работе использую не Visual Studio, а SharpDevelop, поэтому внешний вид каких-то элементов среды может отличаться от таковых в VS, но, я думаю, в целом все достаточно похоже.

Нам необходимо стандартный холст UserControl растянуть так, как будет в итоге выглядеть наш контрол:



















Далее, нам нужно разместить на холсте стандартный TextBox со свойством Dock равным Fill. TextBox займет собой весь холст по ширине, а по высоте - насколько позволит высота строки. Настраиваем TextBox по своему вкусу, устанавливаем тип рамки, шрифт, кегль, цвет текста и т.д.
Затем размещаем на холсте поверх TextBox кнопку или, как в моем случае, Label с установленным значением рамки, заливкой фона, текстом "..." и свойством Cursor со значением Hand. Получится та же кнопка. Мне вариант с Label нравится больше, субъективно лучше выглядит. В итоге получаем следующее:






Обзываем TextBox - CompoentTB, а Label - ComponentLB.


У Label-кнопки необходимо установить свойство Anchor в значения Top и Right, чтобы кнопка была жестко привязана к верхнему правому углу и не двигалась при изменении размера контрола.












Так же необходимо установить у кнопки значения MaximumSize и MinimumSize в значения, равные Size, чтобы она ни при каких обстоятельствах не меняла размер.

По сути, визуальная составляющая контрола готова. Переходим в код.

Перво-наперво, я хочу, чтобы моя кнопка визуально реагировала на нажатия мышью. Пишем в коде конструктора класса обработчики событий MouseDown и MouseUp в виде простых лямбда-выражений:

        #region CONSTRUCTORS
        
        public KSDBSelectTextBox()
        {
            InitializeComponent();
            
            ComponentLB.MouseDown += (object sender, MouseEventArgs e) => ComponentLB.BackColor = Color.LightSalmon;
            ComponentLB.MouseUp += (object sender, MouseEventArgs e) => ComponentLB.BackColor = Color.Snow;
        }
        
        #endregion

Т.е., при нажатии кнопки мыши цвет Label устанавливается в  LightSalmon, а при отпускании - в Snow. Теперь Label стал совсем как настоящая кнопка.

Теперь я хочу вынести из контрола кое-какие параметры встроенного TextBox. В частности, я хочу, чтобы можно было при создании контрола установить значения свойств Multiline и ReadOnly TextBox'a. Создаем свойства класса контрола:

        #region PRIVATE
        
        private Boolean allowMultiline = false;
        private Boolean readOnly = true;

        #endregion

        #region PUBLIC
        
        [Description("Указывает, используется ли Multiline во встроенном TextBox"),Category("_KSProperty")] 
        public Boolean AllowMultiline {
            get {
                return allowMultiline;
            }
            set {
                allowMultiline = value;
            }
        }

        [Description("Указывает, разрешено ли редактировать встроенный TextBox"),Category("_KSProperty")] 
        public Boolean ReadOnly {
            get {
                return readOnly;
            }
            set {
                readOnly = value;
            }
        }

        #endregion

Небольшое отступление по использованию атрибутов в данном коде. Поскольку мы создаем визуальный компонент, нам необходимо сделать так, чтобы некоторые особо важные свойства контрола мы могли редактировать не в коде, а в режиме дизайнера форм. Для этого перед необходимыми свойствами мы указываем атрибуты Description (устанавливает описание свойства в PropertyGrid дизайнера форм) и Category (в котором указываем, к какой категории необходимо отнести свойство). Если кастомных свойств у нас немного (в данном контроле их будет всего четыре), удобно создать свою категорию, которая, за счет первого символа подчеркивания, еще и выводится в PropertyGrid первой. Удобно!

























После создания необходимых свойств нам нужно каким-то образом применять устанавливаемые в них значения к встроенному TextBox. Создадим универсальный метод обновления TextBox:

        #region METHODS
        
        public void ReloadComponentTB(){
            ComponentTB.Multiline = this.AllowMultiline;
            ComponentTB.ReadOnly = this.ReadOnly;
            ComponentTB.BackColor = Color.White;
        }
        
        #endregion

Каждый раз при вызове данного метода свойства Multiline и ReadOnly встроенного TextBox устанавливаются равными соответствующим им свойствам контрола. Кроме того, поскольку при установке ReadOnly в true TextBox автоматически меняет заливку на серую. В методе мы насильно перекрашиваем TextBox каждый раз обратно в белый. Можно сделать и по другому, но пока и так сойдет. Далее, мы вызываем метод каждый раз, когда необходимо обновить состояние TextBox:

        [Description("Указывает, используется ли Multiline во встроенном TextBox"),Category("_KSProperty")] 
        public Boolean AllowMultiline {
            get {
                return allowMultiline;
            }
            set {
                allowMultiline = value;
                ReloadComponentTB();
            }
        }

        [Description("Указывает, разрешено ли редактировать встроенный TextBox"),Category("_KSProperty")] 
        public Boolean ReadOnly {
            get {
                return readOnly;
            }
            set {
                readOnly = value;
                ReloadComponentTB();
            }
        }

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

        #region ENUMS
        
        public enum ActionType{
            SelectObject,
            OpenFile,
            SaveFile,
            SelectDirectory
        }
        
        #endregion

Соответственно, используя каждый из элементов перечисления, мы направляем контрол по одному из путей реализации функционала. Создадим свойство с типом данных ActionType:

        #region PRIVATE
        
        private ActionType action = ActionType.SelectObject;

        #endregion
        
        #region PUBLIC
        
        [Description("Указывает тип действия компонента"),Category("_KSProperty")] 
        public ActionType Action {
            get {
                return action;
            }
            set {
                action = value;
            }
        }
        
        #endregion

Реализуем выполнение контрола с Action = ActionType.SelectObject. По логике действий, контрол должен открыть какую-то форму, подцепиться к событию FormClosing, получить какой-то объект и вывести строковое представление полученного объекта в текстовое поле контрола. Создадим два новых свойства, на этот раз - неинтерактивных:


        #region PRIVATE
        
        private Form selectForm = null;
        private Object selectedObject = null;
    
        #endregion
        
        #region PUBLIC

        public Form SelectForm {
            get {
                return selectForm;
            }
            set {
                selectForm = value;
            }
        }

        public Object SelectedObject {
            get {
                return selectedObject;
            }
            set {
                selectedObject = value;
                ReloadComponentTB();
            }
        }

        #endregion

и внесем небольшое изменение в метод ReloadComponent():

        #region METHODS
        
        public void ReloadComponentTB(){
            ComponentTB.Multiline = this.AllowMultiline;
            ComponentTB.ReadOnly = this.ReadOnly;
            ComponentTB.BackColor = Color.White;
            if(this.SelectedObject != null){
                ComponentTB.Text = this.SelectedObject.ToString();
            }
            else{
                ComponentTB.Text = String.Empty;
            }
        }

        #endregion

Логика такова - при перезагрузке контрола в ComponentTB.Text записываем строковое представление SelectedObject, либо, если SelectedObject не установлен - то записываем пустую строку.
Все готово для обработки главного события контрола - выбора через нажатие на кнопку. Сначала создадим метод-обработчик:

        #region METHODS
        
        private void Select(){
            
            switch(this.Action){
                case ActionType.SelectObject:
                    if(this.SelectForm != null){
                        this.SelectForm.FormClosing += (sender, e) => {
                            if(this.SelectForm.Tag != null){
                                this.SelectedObject = this.SelectForm.Tag;
                            }
                        };
                        this.SelectForm.ShowDialog();
                    }
                    break;
            }
        }

        #endregion

Логика такова: если свойство SelectForm установлено, то открывается форма. Какая форма будет открыта, контрол не знает. К событию FormClosing подключаем лямбда-выражение, которое перед закрытием дочерней формы проверяет у нее свойство Tag и, если оно установлено, то устанавливает его значение в значение свойства SelectedObject контрола. Таким образом, дочерняя форма должна возвращать в свойстве Tag какой-то объект. Как это сделать, мы увидим в самом конце.
Осталось только подключить метод Select в качестве обработчика события Click кнопки:

        #region CONSTRUCTORS
        
        public KSDBSelectTextBox()
        {
            InitializeComponent();
            
            ComponentLB.MouseDown += (object sender, MouseEventArgs e) => ComponentLB.BackColor = Color.LightSalmon;
            ComponentLB.MouseUp += (object sender, MouseEventArgs e) => ComponentLB.BackColor = Color.Snow;
            ComponentLB.Click += (sender, e) => this.Select();
            
            ReloadComponentTB();
        }
        
        #endregion

На данном этапе контрол уже умеет открывать дочернюю форму и получать от нее какой-то объект. Однако этого мало, необходимо расширить функционал для поддержки всех типов действий. Создадим интерактивное свойство Filter для действий OpenFile и SaveFile:

        #region PRIVATE
        
        private String filter = String.Empty;

        #endregion
        
        #region PUBLIC

        [Description("Указывает фильтр для действий OpenFile и SaveFile"),Category("_KSProperty")] 
        public String Filter {
            get {
                return filter;
            }
            set {
                filter = value;
            }
        }

        #endregion

Расширим метод Select() для поддержки всех необходимых действий:

        private void Select(){
            
            switch(this.Action){
                case ActionType.SelectObject:
                    if(this.SelectForm != null){
                        this.SelectForm.FormClosing += (sender, e) => {
                            if(this.SelectForm.Tag != null){
                                this.SelectedObject = this.SelectForm.Tag;
                            }
                        };
                        this.SelectForm.ShowDialog();
                    }
                    break;
                case ActionType.OpenFile:
                    OpenFileDialog OFD = new OpenFileDialog();
                    OFD.Filter = this.Filter;
                    if(OFD.ShowDialog() == DialogResult.OK){
                        this.SelectedObject = OFD.FileName;
                    }
                    break;
                case ActionType.SaveFile:
                    SaveFileDialog SFD = new SaveFileDialog();
                    SFD.Filter = this.Filter;
                    if(SFD.ShowDialog() == DialogResult.OK){
                        this.SelectedObject = SFD.FileName;
                    }
                    break;
                case ActionType.SelectDirectory:
                    FolderBrowserDialog FBD = new FolderBrowserDialog();
                    if(FBD.ShowDialog() == DialogResult.OK){
                        this.SelectedObject = FBD.SelectedPath;
                    }
                    break;
            }
            
        }

Самое основное мы сделали. Мы создали четыре интерактивных свойства, и обработку нажатия на кнопку. Осталось совсем немного: создать два события, одно из которых должно выполняться до основного обработчика, а второе - после. Причем событие, выполняющееся до основного обработчика, должно уметь остановить его выполнение.

        #region EVENTS
        
        public delegate void BeforeSelectHandler(object sender, CancelEventArgs args);
        [Description("Событие, возникающее до выбора"),Category("_KSAction")]
        public event BeforeSelectHandler BeforeSelect;
        public Boolean OnBeforeSelect(){
            Boolean result = false;
            if(BeforeSelect != null){
                CancelEventArgs args = new CancelEventArgs();
                BeforeSelect(this, args);
                result = args.Cancel;
            }
            return result;
        }
        
        public delegate void AfterSelectHandler(object sender, EventArgs args);
        [Description("Событие, возникающее после выбора"),Category("_KSAction")]
        public event AfterSelectHandler AfterSelect;
        public void OnAfterSelect(){
            if(AfterSelect != null){
                AfterSelect(this, new EventArgs());
            }
        }
        
        #endregion

Метод-инициатор события BeforeSelect возвращает булево значение, указывающее основному обработчику, выполняться ему или нет. Внесем изменения в основной обработчик:

        #region METHODS
        
        private void Select(){
            if(this.OnBeforeSelect()){
                return;
            }
            
            switch(this.Action){
                case ActionType.SelectObject:
                    if(this.SelectForm != null){
                        this.SelectForm.FormClosing += (sender, e) => {
                            if(this.SelectForm.Tag != null){
                                this.SelectedObject = this.SelectForm.Tag;
                            }
                        };
                        this.SelectForm.ShowDialog();
                    }
                    break;
                case ActionType.OpenFile:
                    OpenFileDialog OFD = new OpenFileDialog();
                    OFD.Filter = this.Filter;
                    if(OFD.ShowDialog() == DialogResult.OK){
                        this.SelectedObject = OFD.FileName;
                    }
                    break;
                case ActionType.SaveFile:
                    SaveFileDialog SFD = new SaveFileDialog();
                    SFD.Filter = this.Filter;
                    if(SFD.ShowDialog() == DialogResult.OK){
                        this.SelectedObject = SFD.FileName;
                    }
                    break;
                case ActionType.SelectDirectory:
                    FolderBrowserDialog FBD = new FolderBrowserDialog();
                    if(FBD.ShowDialog() == DialogResult.OK){
                        this.SelectedObject = FBD.SelectedPath;
                    }
                    break;
            }
            
            this.OnAfterSelect();
        }
        
        #endregion

Таким образом, если OnBeforeSelect() вернет true, основной обработчик выполняться не будет.
Все, контрол успешно закончен. Можно проверять на форме. Создадим форму и четыре наших новых контрола: ksSelectObject, ksOpenFile, ksSaveFile и ksSelectDirectory:














В соответствии с названиями устанавливаем каждому из контролов значения Action.












Для второго и третьего контролов в дизайнере задаем значение свойства Filter сообразно с тем, как это свойство задается при вызове OpenFileDialog и SaveFileDialog.












Для четвертого контрола необходимо указать только тип действия. Проверим:
























OpenFile - работает!





















SaveFile - работает!



























SelectDirectory - работает!
Осталось проверить первый контрол. Для этого создадим дополнительный класс и дочернюю форму:

    public class SimpleClass
    {
        private Int32 simpleID = 0;
        private String simpleProp = String.Empty;

        public Int32 SimpleID {
            get {
                return simpleID;
            }
            set {
                simpleID = value;
            }
        }

        public String SimpleProp {
            get {
                return simpleProp;
            }
            set {
                simpleProp = value;
            }
        }
        
        public SimpleClass(Int32 _id, String _prop)
        {
            this.SimpleID = _id;
            this.SimpleProp = _prop;
        }
        
        public override string ToString()
        {
            return String.Format("Объект #{0}. Имя {1}", this.SimpleID, this.SimpleProp);
        }
    }

На дочернюю форму разместим четыре LinkLabel для примера:


















В обработчике события Load дочерней формы создадим четыре объекта нашего проверочного класса SimpleClass и пропишем обработчики Click:

    public partial class ChildForm : Form
    {
        public ChildForm()
        {
            //
            // The InitializeComponent() call is required for Windows Forms designer support.
            //
            InitializeComponent();
            
            //
            // TODO: Add constructor code after the InitializeComponent() call.
            //
        }
        
        void ClickHandler(SimpleClass objResult){
            this.Tag = objResult;
            this.Close();
        }
        
        void ChildFormLoad(object sender, EventArgs e)
        {
            SimpleClass objOne = new SimpleClass(1, "Первый объект");
            SimpleClass objTwo = new SimpleClass(2, "Второй объект");
            SimpleClass objThree = new SimpleClass(3, "Третий объект");
            SimpleClass objFour = new SimpleClass(4, "Четвертый объект");
            
            linkLabel1.Text = objOne.ToString();
            linkLabel1.Click += (object objSender, EventArgs objE) => ClickHandler(objOne);
            linkLabel2.Text = objTwo.ToString();
            linkLabel2.Click += (object objSender, EventArgs objE) => ClickHandler(objTwo);
            linkLabel3.Text = objThree.ToString();
            linkLabel3.Click += (object objSender, EventArgs objE) => ClickHandler(objThree);
            linkLabel4.Text = objFour.ToString();
            linkLabel4.Click += (object objSender, EventArgs objE) => ClickHandler(objFour);
        }
    }

Остается только указать первому контролу, что ему нужно открывать форму ChildForm и все:

        ksSelectObject.SelectForm = new ChildForm();

Иии... Все работает!















Таким образом, путем не слишком сложных манипуляций мы получили многофункциональный универсальный контрол для баз данных и многих других приложений

Комментариев нет:

Отправить комментарий