Пользовательский элемент управления

Иногда при создании пользовательского интерфейса не хватает функциональности стандартных элементов управления или не устраивает их дизайн. Пользовательский элемент управления помогает решать эти проблемы. Несколько собственных пользовательских элементов управления можно объединять в библиотеку элементов. В статье Создание библиотеки пользовательского элемента управления описано создание такого элемента из стандартных элементов управления.

Содержание

Для примера создадим два пользовательских элемента управления, объединим их в библиотеку DLL и используем их в интерфейсе оконного приложения. Разработаем собственный индикатор прогресса (ProgressBar) и элемент изменения значения (TrackBar). Оба этих элемента, кнопку и таймер разместим на главной форме для тестирования, см. рис. 1

рис. 1 Тестовая форма

Создаём библиотеку и первый пользовательский элемент управления

Начнём с создания библиотеки пользовательских элементов управления. Создадим новый проект для библиотеки по шаблону «Библиотека элементов управления Windows Forms ( .NET Framework )» и назовём, например, QuardProgressBar (имя дано по первому элементу, хотя может быть любым). В открывшееся окно редактора элементов добавим стандартный элемент PictureBox и, установив его свойство Dock в Fill, заполним им всё окно. Переименуем имя класса по умолчанию UserControl1 во что-то более понятное — QwProgBar.cs. Добавим переменные и свойства, обеспечивающие получение и установку их значений. Это переменные, отвечающие за цвет рамок, прямоугольников, текста и значение прогресса. Для элемента PictureBox добавим обработчик события Paint. Добавим метод для установки значения прогресса в котором будем вызывать перерисовку элемента управления PictureBox. Код класса приведён ниже :

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace QuardProgressBar
{
    public partial class QwProgBar : UserControl
    {
        private Color textColor = Color.Black;
        private Color progressColor = Color.LightBlue;
        private Color borderColor = Color.Green;
        private Color borderQwColor = Color.White;
        public int procent = 0;
        public QwProgBar()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Установить значение и перерисовать элемент
        /// </summary>
        /// <param name="pr"></param>
        public void SetProcent(int pr)
        {
            if (pr != procent)
            {
                if ((pr >= 0) && (pr <= 100)) procent = pr;
                Refresh();
            }
        }

        /// <summary>
        /// Цвет рамки прямоугольника, ограничивающего элемент
        /// </summary>
        public Color BorderColor
        {
            get { return borderColor; }
            set { borderColor = value; }
        }

        /// <summary>
        /// Цвет рамок прямоугольников, показывающих прогресс выполнения 
        /// </summary>
        public Color BorderQwColor
        {
            get { return borderQwColor; }
            set { borderQwColor = value; }
        }

        /// <summary>
        /// Цвет текста отображения процента выполнения
        /// </summary>
        public Color TextColor
        {
            get { return textColor; }
            set { textColor = value; }
        }

        /// <summary>
        /// Цвет заполнения прямоугольников, показывающих прогресс выполнения 
        /// </summary>
        public Color ProgressColor
        {
            get { return progressColor; }
            set { progressColor = value; }
        }

        private void pictureBox1_Paint(object sender, PaintEventArgs e)
        {
            if (procent < 0) procent = 0;
            if (procent > 100) procent = 100;
            string s_num = string.Format("{0} %", procent);
            Bitmap picGr = new Bitmap(pictureBox1.Width, pictureBox1.Height);
            Graphics g = Graphics.FromImage(picGr);
            Pen penGr = new Pen(borderColor, 3);
            Pen penWt = new Pen(borderQwColor, 2);
            Font fN = new Font(FontFamily.GenericSansSerif, 20.0F, FontStyle.Bold);
            SolidBrush brGR = new SolidBrush(textColor);
            SolidBrush brFPr = new SolidBrush(progressColor);

            g.Clear(pictureBox1.BackColor);
            Rectangle r = new Rectangle(0, 0, picGr.Width - 1, picGr.Height - 1);
            g.DrawRectangle(penGr, r);

            int rw = (picGr.Width - 8) / 20;
            int rh = (picGr.Height - 8) / 5;
            r.Width = rw; r.Height = rh;
            for (int i = 0; i < procent; i++)
            {
                r.X = (i % 20) * rw + 5;
                r.Y = (i / 20) * rh + 5;
                g.FillRectangle(brFPr, r);
                g.DrawRectangle(penWt, r);
            }

            Point p1 = new Point((int)((picGr.Width - s_num.Length * fN.SizeInPoints) / 2), (int)((picGr.Height - fN.Height) / 2));
            g.DrawString(s_num, fN, brGR, p1);

            e.Graphics.DrawImage(picGr, new Point(0, 0));

            penWt.Dispose();
            brFPr.Dispose();
            brGR.Dispose();
            fN.Dispose();
            penGr.Dispose();
            g.Dispose();
            picGr.Dispose();
        }
    }
}

Прогресс выполнения отображаем путем рисования прямоугольников по 20 штук в 5 строках и выводом значения в процентах в центре элемента (функция pictureBox1_Paint). Проверяем нахождение значение процента в пределах от 0 до 100 и выводим в строке s_num. Создаем в памяти копию области для рисования чтобы не было мерцания, создаём кисти, перья, шрифт. Очищаем холст, рисуем ограничивающий прямоугольник, в зависимости от значения процента рисуем нужное количество маленьких прямоугольников и в центре выводим значение прогресса в процентах. Готовое изображение из памяти переносим на переданное устройство и уничтожаем уже не нужные созданные объекты явно чтобы освободить память. В результате первый элемент библиотеки готов.

Создаём второй пользовательский элемент управления — наш TrackBar

Для создания второго элемента добавим в нашу библиотеку ещё один пользовательский элемент управления, см. рис. 2

рис. 2 Добавление в библиотеку ещё одного пользовательского элемента

По аналогии с первым элементом в окне редактора элементов добавим стандартный элемент PictureBox и, установив его свойство Dock в Fill, заполним им всё окно. Переименуем имя класса по умолчанию UserControl1 на этот раз в CircleTrackBar.cs. Добавим переменные и свойства, обеспечивающие получение и установку их значений.

Логика работы элемента : диапазон возможных значений от минимального до максимального (minValue — maxValue) отображается вокруг окружности с индикатором выбранного значения и его текстовым значением в центре (value). Тип значения — int. Изменять значение можно кликом мышки по окружности с индикатором. Значение меняется в зависимости от выбранного шага (tickFrequence). По умолчанию — 1. Но, например, для диапазона 0 — 1000 можем поставить 100 или 50 и значение будет изменятся так : 0, 100, 200, …, 1000 или 0, 50, 100, 150, …, 950, 1000. Индикатор выбранного значения отображается в секторе окружности в 270 градусов.

Добавим методы обработки событий pictureBox1_MouseClick и pictureBox1_Paint. Перерисовка элемента реализована в методе pictureBox1_Paint. Она вызывается принудительно после клика мышкой в области окружности для отображения выбранного нового значения.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace QuardProgressBar
{
    public partial class CircleTrackBar : UserControl
    {
        public event EventHandler ValueChanged;

        private int value = 0;
        private int tickFrequence = 1;
        private int minValue = 0;
        private int maxValue = 10;
        private Color borderColor = Color.Black;

        /// <summary>
        /// Цвет линий элемента
        /// </summary>
        public Color BorderColor
        {
            get { return borderColor; }
            set { borderColor = value; }
        }
        

        /// <summary>
        /// Выбранное значение элемента 
        /// </summary>
        public int Value
        {
            get { return value; }
            set { this.value = value; }
        }

        /// <summary>
        /// Шаг значения элемента
        /// </summary>
        public int TickFrequence
        {
            get { return tickFrequence; }
            set { tickFrequence = value; }
        }

        /// <summary>
        /// Минимальное значение параметра
        /// </summary>
        public int MinValue
        {
            get { return minValue; }
            set { minValue = value; }
        }

        /// <summary>
        /// Максимальное значение параметра 
        /// </summary>
        public int MaxValue
        {
            get { return maxValue; }
            set { maxValue = value; }
        }

        public CircleTrackBar()
        {
            InitializeComponent();
        }
        private void HandleValueChanged(object sender, EventArgs e)
        {
            this.OnValueChanged(EventArgs.Empty);
        }
        protected virtual void OnValueChanged(EventArgs e)
        {
            EventHandler handler = this.ValueChanged;
            if (handler != null)
            {
                handler(this, e);
            }
        }
        private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
        {
            int w = pictureBox1.Width;
            int h = pictureBox1.Height;
            Rectangle rc = new Rectangle((int)(w / 8), (int)(h / 8), (int)(0.75 * w), (int)(0.75 * h));
            if (rc.Contains(new Point(e.X, e.Y)))
            {
                double sn = e.X - w / 2, cs = e.Y - h / 2, tn = sn / cs;
                double atan = Math.Atan(tn);
                int ang = (int)(atan * 180 / Math.PI), ra = 0, old_value = value;
                if (sn < 0)
                {
                    if (cs < 0) ra = 45 + (90 - ang);
                    else
                    {
                        if (-ang > 45) ra = -ang - 45; else ra = 0;
                    }
                }
                else
                {
                    if (cs < 0) ra = 135 - ang;
                    else
                    {
                        ra = 225 + (90 - ang);
                        if (ra > 270) ra = 270;
                    }
                }
                value = (int)((ra * (maxValue - minValue)) / 270);
                if (tickFrequence > 1) value = (value / tickFrequence) * tickFrequence;
                if (old_value != value)
                {
                    OnValueChanged(new EventArgs());
                }
                Refresh();
            }
        }

        private void pictureBox1_Paint(object sender, PaintEventArgs e)
        {
            string s_num = value.ToString();
            Bitmap picGr = new Bitmap(pictureBox1.Width, pictureBox1.Height);
            Graphics g = Graphics.FromImage(picGr);
            Pen penGr = new Pen(borderColor, 3);
            Font fN = new Font(FontFamily.GenericSansSerif, 10.0F, FontStyle.Bold);
            SolidBrush brBC = new SolidBrush(borderColor);

            g.Clear(pictureBox1.BackColor);
            Rectangle r = new Rectangle(picGr.Width / 4, picGr.Height / 4, picGr.Width / 2, picGr.Height / 2);
            g.DrawEllipse(penGr, r);
            float dw = (float)(270.0 / (maxValue - minValue));
            Point pc = new Point((int)(picGr.Width / 2 - Math.Sin(Math.PI * ((135 - dw * value) / 180)) * (picGr.Width / 4)), (int)(picGr.Height / 2 - Math.Cos(Math.PI * ((135 - dw * value) / 180)) * (picGr.Width / 4)));
            //s_num = pc.X.ToString() + " " + pc.Y.ToString() + " " + Math.Sin(Math.PI * 30 / 180);
            r.X = pc.X - 3; r.Y = pc.Y - 3; r.Width = 7; r.Height = 7;
            g.DrawEllipse(penGr, r);

            int stp = (int)((maxValue - minValue) / 5);
            for (int i = minValue; i < (1 + maxValue); i += stp)
            {
                string si = i.ToString();
                pc = new Point((int)(picGr.Width / 2 - Math.Sin(Math.PI * ((135 - dw * i) / 180)) * (fN.SizeInPoints + picGr.Width / 4)), (int)(picGr.Height / 2 - Math.Cos(Math.PI * ((135 - dw * i) / 180)) * (fN.SizeInPoints + picGr.Width / 4)));
                if (i < maxValue / 2)
                {
                    pc.X -= (int)((0 + si.Length) * fN.SizeInPoints);
                }
                pc.Y -= (int)(fN.Height / 2);
                g.DrawString(si, fN, brBC, pc);
            }

            Point p1 = new Point((int)((picGr.Width - s_num.Length * fN.SizeInPoints) / 2), (int)((picGr.Height - fN.Height) / 2));
            g.DrawString(s_num, fN, brBC, p1);

            e.Graphics.DrawImage(picGr, new Point(0, 0));

            brBC.Dispose();
            fN.Dispose();
            penGr.Dispose();
            g.Dispose();
            picGr.Dispose();
        }
    }
}

Но перерисовка элемента в этом случае уже не самое главное. Нужно проинформировать программу о новом выборе пользователя. Для этого создадим своё пользовательское событие : public event EventHandler ValueChanged; и реализуем его генерацию и вызов. Событие создаем внутри нашего класса по ключевому слову event на основе стандартного делегата EventHandler. Событие вызывается в функции protected virtual void OnValueChanged(EventArgs e). Документация рекомендует модификаторы protected virtual. Внутри функции нужно обязательно проверить наличие хотя бы одного подписчика на данное событие, иначе будет выброшено исключение. В соответствии с логикой работы элемента событие случается (генерируется) тогда, когда мы меняем значение value по клику мышки : if (old_value != value) { OnValueChanged(new EventArgs()); }. На этом разработка второго элемента завершена. Скомпилируем нашу библиотеку.

Разработка тестового приложения

Перейдём к проверке работы наших элементов. Создадим обычное приложение Windows Forms ( .NET Framework ) — TestQwProgressBar. Подключим ссылку на нашу библиотеку в этот тестовый проект, см. рис. 3:

рис. 3 Добавление ссылки на библиотеку пользовательских элементов

В главное окно приложения добавим каждый наш пользовательский элемент управления, а также кнопку и таймер в соответствии с макетом рис. 1. После добавления ссылки на нашу библиотеку в наборе элементов будут отображены и наши два элемента. Размещаем их на форме как и стандартные элементы управления перетаскиванием, позиционированием и установкой нужного размера :

рис. 4 Добавление пользовательских элементов на форму

В окне свойств для каждого элемента отображаются его свойства, в том числе и те, которые мы создавали сами, а в событиях наше пользовательское событие :

рис. 5 Свойства QwProgBar
рис. 6 Свойства CircleTrackBar

Настроим свойства наших элементов. Добавим методы для обработки событий : нажатия кнопки, окончания цикла таймера, ValueChanged для CircleTrackBar для реакции на изменение значения. Должно получиться примерно как в коде ниже :

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using QuardProgressBar;

namespace TestQwProgressBar
{
    public partial class MainFormQw : Form
    {
        int curent_procent = 0;
        bool isTimer = false;
        public MainFormQw()
        {
            InitializeComponent();
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            curent_procent++;
            qwProgBar1.SetProcent(curent_procent);
            curent_procent %= 100;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (!isTimer)
            {
                if (circleTrackBar1.Value > 0) timer1.Interval = circleTrackBar1.Value;
                timer1.Start();
                isTimer = true;
                button1.Text = "СТОП";
            }
            else
            {
                timer1.Stop();
                isTimer = false;
                button1.Text = "СТАРТ";
            }
        }

        private void circleTrackBar1_ValueChanged(object sender, EventArgs e)
        {
            //MessageBox.Show("circleTrackBar1_ValueChanged");
            if (circleTrackBar1.Value > 0) timer1.Interval = 10 * circleTrackBar1.Value;
        }
    }
}

Таймер выставим на 500 мс, максимальное значение circleTrackBar1 выставим на 100, TickFrequence — 10, цвета как нравится, но разные. Логика работы программы : по кнопке Старт циклически запускается таймер; каждое его завершение соответствует 1 % прогресса, значение которого передается нашему индикатору прогресса (функция timer1_Tick); квадратики в индикаторе прогресса начинают появляться; скорость их появления можно изменить кликая по circleTrackBar1 для выбора значения интервала; новое значение передаётся в таймер; остановить выполнение можно нажав на кнопку «Стоп».

рис. 7 Тестовая программа в работе

Заключение

Пользовательские элементы специально сделаны «неказистыми», так как цель статьи — показать всю технологию их создания и использования, а красоту оставим дизайнерам. Область применения пользовательских элементов — повышение оригинальности собственных приложений. Ранее встречал, например, библиотеку элементов, с помощью которых можно создать панель управления радиоприемником или осциллографом.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *