Создание компонента пользовательского интерфейса "с нуля"

Сегодня создадим новый компонент пользовательского интерфейса что называется "с нуля". Это означает, что мы не будем отталкиваться от уже имеющихся компонентов в системе. В качестве "базового" класса для нашего элемента будет служить класс Control, который является базовым для всех элементов которые так или иначе взаимодействуют с пользователем.

Делать мы будем вот такой вот переключатель-ползунок. Выключенное и включенное состояние можно видеть на рисунке ниже.

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

В моей программе он будет использоваться на форме настроек, изображая соответственно включенную или выключенную настройку. Изначально я использовал стандартный CheckBox, но это просто галочка и она уже настолько приелась, что увидев в Интернете на каком-то сайте подобный ползунок я тоже решил реализовать его в своей программе. Да и пользователи реально обрадовались когда вместо привычных галочек увидели эти ползунки.

Немного погуглив, нашел что они называются "ToggleSwitch". Ну и мы тоже также назовем. Определим еще несколько необходимых переменных. 2 области в которых будут находиться непосредственно сам переключатель и ползунок, а также статус и позиция ползунка в координатной сетке. Также нам необходимо знать каким цветом закрашивать включенный ползунок. В нашем примере это будет зеленый цвет.

public class ToggleSwitch : Control
{
Rectangle rect, rectMain;
public bool Checked { get; set; } = false;
int posX_ON;
int posX_OFF;
public Color BackColorOn { get; set; } = Color.Green;
}

При создании компонента "с нуля" абсолютно необходимо определить действия которые будут происходить с нашим элементом и на что он должен реагировать. В нашем случае это делается командой
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.SupportsTransparentBackColor | ControlStyles.UserPaint, true)

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

protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
rect = new Rectangle(4, 4, 25, 11);
rectMain = new Rectangle(0, 0, Width, Height);
posX_OFF = rect.X;
posX_ON = rect.Left + rect.Width - rect.Height;
}

Также переопределим событие OnMouseDown, который срабатывает при нажатии кнопки мыши. В нем мы будем инвертировать состояние переключателя

protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
Checked = !Checked;
Invalidate();
}

Прямоугольные области, описываемые классом Rectangle, имеют прямые углы. Чтобы их немного "скруглить", мной была использована специальная функция, которую мы в дальнейшем будем использовать в процедуре рисования

private GraphicsPath RoundedRectangle(Rectangle rect, int RoundSize)
{
GraphicsPath gp = new GraphicsPath();
gp.AddArc(rect.X, rect.Y, RoundSize, RoundSize, 180, 90);

gp.AddArc(rect.X + rect.Width - RoundSize, rect.Y, RoundSize, RoundSize, 270, 90);
gp.AddArc(rect.X + rect.Width - RoundSize, rect.Y + rect.Height - RoundSize, RoundSize, RoundSize, 0, 90);
gp.AddArc(rect.X, rect.Y + rect.Height - RoundSize, RoundSize, RoundSize, 90, 90);
gp.CloseFigure();
return gp;
}

Ну и наконец, рисуем наш элемент При прорисовке также "сдвигаем" наш ползунок на нужную позицию и закрашиваем в выбранный цвет в случае необходимости

protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics graph = e.Graphics;
graph.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graph.Clear(Parent.BackColor);
Pen TSPen = new Pen(Color.DarkGray, 3);
Pen TSPenToggle = new Pen(Color.DarkGray, 3);
GraphicsPath rectGP = RoundedRectangle(rect, rect.Height);
Rectangle rectToggle = new Rectangle(rect.X, rect.Y, rect.Height, rect.Height);
graph.DrawRectangle(new Pen(BackColor), rectMain);
graph.FillRectangle(new SolidBrush(BackColor), rectMain);
graph.DrawString(Text,Font,new SolidBrush(ForeColor), new Rectangle(rect.Width + 7, 1, rectMain.Right, rectMain.Bottom));
graph.DrawPath(TSPen, rectGP);
if (Checked)
{
rectToggle.Location = new Point(posX_ON, rect.Y);
graph.FillPath(new SolidBrush(BackColorOn), rectGP);
}
else
{
rectToggle.Location = new Point(posX_OFF, rect.Y);
graph.FillPath(new SolidBrush(BackColor), rectGP);
}
graph.DrawEllipse(TSPenToggle, rectToggle);
graph.FillEllipse(new SolidBrush(Color.WhiteSmoke), rectToggle);
}

Вот такой вот у нас получился элемент управления. Красиво и функционально.

На этом пока все.