Иногда при создании пользовательского интерфейса не хватает функциональности стандартных элементов управления или не устраивает их дизайн. Пользовательский элемент управления помогает решать эти проблемы. Несколько собственных пользовательских элементов управления можно объединять в библиотеку элементов. В статье Создание библиотеки пользовательского элемента управления описано создание такого элемента из стандартных элементов управления.
Содержание
- Создаём библиотеку и первый пользовательский элемент управления
- Создаём второй пользовательский элемент управления — наш TrackBar
- Разработка тестового приложения
- Заключение
Для примера создадим два пользовательских элемента управления, объединим их в библиотеку DLL и используем их в интерфейсе оконного приложения. Разработаем собственный индикатор прогресса (ProgressBar) и элемент изменения значения (TrackBar). Оба этих элемента, кнопку и таймер разместим на главной форме для тестирования, см. рис. 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
По аналогии с первым элементом в окне редактора элементов добавим стандартный элемент 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:
В главное окно приложения добавим каждый наш пользовательский элемент управления, а также кнопку и таймер в соответствии с макетом рис. 1. После добавления ссылки на нашу библиотеку в наборе элементов будут отображены и наши два элемента. Размещаем их на форме как и стандартные элементы управления перетаскиванием, позиционированием и установкой нужного размера :
В окне свойств для каждого элемента отображаются его свойства, в том числе и те, которые мы создавали сами, а в событиях наше пользовательское событие :
Настроим свойства наших элементов. Добавим методы для обработки событий : нажатия кнопки, окончания цикла таймера, 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 для выбора значения интервала; новое значение передаётся в таймер; остановить выполнение можно нажав на кнопку «Стоп».
Заключение
Пользовательские элементы специально сделаны «неказистыми», так как цель статьи — показать всю технологию их создания и использования, а красоту оставим дизайнерам. Область применения пользовательских элементов — повышение оригинальности собственных приложений. Ранее встречал, например, библиотеку элементов, с помощью которых можно создать панель управления радиоприемником или осциллографом.