C#. Пишем свой ListBox. Учимся использовать градиент и наследование
На сегодняшний день библиотека .NET предлагает разработчику достаточно много компонентов, которые можно легко настраивать под себя. Но, бывают моменты, когда хочется видеть чуть больше функций и возможностей. Поэтому, разработчик из Microsoft предоставляют нам возможность улучшать стандартные компоненты. И вот нам, вдруг захотелось сделать что-то свое, чтобы выглядело так, как мы хотим и функционал был нужный. Давайте вместе попробуем разработать свой ListBox (список). Немного забегая вперед я хочу показать, что у нас в итоге должно получиться:

Теперь переходим к постановке задачи. Что мы хотим реализовать:
- Каждый элемент списка должен содержать картинку
- Каждый активный элемент выделяется прямоугольником с закругленными углами и заполняется градиентом
- При изменении размеров списка — автоматически перерисовывать содержимое этого списка
- Каждый элемент будет содержать объект с определенными свойствами. В последних будет храниться некая информация и при выборе элемента отображаться на форме
Откройте Visual Studio и создайте проект с формой. После этого, в дерево проектов добавьте файл с классом ExListBox:
sealed class exList : ListBox
{
//...
}
Как видно из кода, мы унаследовали свой класс от стандартного ListBox и установили модификатор sealed, т.к. этот класс мы не будем проектировать как родителя для других классов.
Далее, в теле класса, записываем
const int cornerRadius = 4; //радиус закругления
int x, y, rectWidth, rectHeight; //вспомогательные переменные для отрисовки
//конструктор класса
public ExListBox()
{
DrawMode = DrawMode.OwnerDrawVariable;
}
Радиус закругления задается для углов прямоугольника, который будет указывать на активный элемент списка. В теле конструктора свойство компонента DrawMode следует задать как DrawMode.OwnerDrawVariable, для того, чтобы можно было самим рисовать на компоненте.
После наследование нашего класса от стандартного ListBox, мы фактически получили холст для рисования. Вот чем мы сейчас и займемся. Нам нужно перегрузить три метода класса ListBox:
- OnDrawItem — вызывается при прорисовке каждого элемента списка
- OnSizeChanged — при изменении размера компонента
- OnMeasureItem — при измерении высоты и ширины элемента
Первый метод самый сложный и самый важный:
protected override void OnDrawItem(DrawItemEventArgs e)
{
//e - элемент, с которым мы дальше и работаем
//если текущего элемента нет или в списке нет вообще элементов,
//значит выходить из метода
if (e.Index <= -1 || this.Items.Count == 0) return;
//получаем текст элемента
string s = Items[e.Index].ToString();
//формат строки для рисования текста
StringFormat sf = new StringFormat();
//формат выставляем по центру
sf.Alignment = StringAlignment.Center;
//создаем обычную кисть с заданным цветом
Brush solidBrush = new SolidBrush(Color.FromArgb(45, 131, 218));
//создаем кисть с градиентом по вертикали
Brush gradientBrush = new LinearGradientBrush(e.Bounds, Color.FromArgb(121, 187, 255), Color.FromArgb(65, 151, 238), LinearGradientMode.Vertical);
//теперь определяем какой элемент сейчас нужно отрисовать
if ((e.State & DrawItemState.Focus) == 0) //если не активный
{
//заполняем прямоугольник выбранным цветом
e.Graphics.FillRectangle(new SolidBrush(SystemColors.Window), e.Bounds.X, e.Bounds.Y, e.Bounds.Width, e.Bounds.Height + 1);
//рисуем картинку
e.Graphics.DrawImage(Properties.Resources.document, e.Bounds.Width / 2 - Properties.Resources.document.Width / 2, e.Bounds.Top);
//пишем текст
e.Graphics.DrawString(s, Font, new SolidBrush(SystemColors.WindowText), new RectangleF(0, e.Bounds.Y + 56, e.Bounds.Width, 16), sf);
}
else //если активный
{
//создаем путь который повторит контур с закругленными углами
GraphicsPath gfxPath = new GraphicsPath();
//определяем координаты элемента в списке
//т.к. для каждого элемента они разные
x = e.Bounds.X;
y = e.Bounds.Y;
//также определяем его ширину и высоту
rectWidth = e.Bounds.Width - 2;
rectHeight = e.Bounds.Height;
#region Рисуем прямоугольник с закругленными углами
gfxPath.AddLine(x + cornerRadius, y, x + rectWidth - (cornerRadius * 2), y);
gfxPath.AddArc(x + rectWidth - (cornerRadius * 2), y, cornerRadius * 2, cornerRadius * 2, 270, 90);
gfxPath.AddLine(x + rectWidth, y + cornerRadius, x + rectWidth, y + rectHeight - (cornerRadius * 2));
gfxPath.AddArc(x + rectWidth - (cornerRadius * 2), y + rectHeight - (cornerRadius * 2), cornerRadius * 2, cornerRadius * 2, 0, 90);
gfxPath.AddLine(x + rectWidth - (cornerRadius * 2), y + rectHeight, x + cornerRadius, y + rectHeight);
gfxPath.AddArc(x, y + rectHeight - (cornerRadius * 2), cornerRadius * 2, cornerRadius * 2, 90, 90);
gfxPath.AddLine(x, y + rectHeight - (cornerRadius * 2), x, y + cornerRadius);
gfxPath.AddArc(x, y, cornerRadius * 2, cornerRadius * 2, 180, 90);
gfxPath.CloseFigure();
e.Graphics.DrawPath(new Pen(solidBrush, 1), gfxPath);
//закрашиваем область
e.Graphics.FillPath(gradientBrush, gfxPath);
gfxPath.Dispose();
#endregion
//картинка и текст будут над областью заливки
//рисуем картинку
e.Graphics.DrawImage(Properties.Resources.document, e.Bounds.Width / 2 - Properties.Resources.document.Width / 2, e.Bounds.Top);
//и пишем текст
e.Graphics.DrawString(s, Font, new SolidBrush(SystemColors.WindowText), new RectangleF(0, e.Bounds.Y + 56, e.Bounds.Width, 16), sf);
}
}
Комментарии написал почти к каждой строке, поэтому, думаю, что все будет понятно.
Два оставшихся метода короткие:
//после изменения размера
protected override void OnSizeChanged(EventArgs e)
{
//вызываем обновление компонента
Refresh();
base.OnSizeChanged(e);
}
//во время задания размеров элемента
protected override void OnMeasureItem(MeasureItemEventArgs e)
{
//если это элемент
if (e.Index > -1)
{
//задаем высоту
e.ItemHeight = 70;
//и ширину
e.ItemWidth = Width;
}
}
После этого вам нужно загрузить в проект картинку, которая будет отображаться на элементах списка. В моем случае это Properties.Resources.document.
В итоге у нас получился такой класс. В принципе, на этом написание своего компонента закончено. Теперь в появившемся списке компонентов, берем наш и перетаскиваем на форму. Задаем ему читаемое имя.

Следующим шагом будет заполнение нашего списка элементами. Предлагаю создавать объекты и добавлять их в список на основе следующего класса:
public class Book
{
#region Переменные
private string title, author, pagesCount, year;
public string Author
{
get { return author; }
}
public string PagesCount
{
get { return pagesCount; }
}
public string Year
{
get { return year; }
}
#endregion
public Book(string title, string author, string pagesCount, string year)
{
this.title = title;
this.author = author;
this.pagesCount = pagesCount;
this.year = year;
}
public override string ToString()
{
return title + " [" + year + "]";
}
}
Объекты этого класса будут содержать в себе информацию о книге. Эта же информация будет отображаться на форме.
При загрузке формы загружаем данные в наш список:
private void Form1_Load(object sender, EventArgs e)
{
lstMain.Items.Add(new Book("Идеальное отражение", "Дмитрий Казаков", "500", "2010"));
lstMain.Items.Add(new Book("Пепельный свет", "Андрей Ливадный", "300", "2010"));
//...
lstMain.SelectedIndex = 0;
}
При изменении элемента списка, загружаем из него данные:
private void lstMain_SelectedIndexChanged(object sender, EventArgs e)
{
getInfo();
}
/// <summary>
/// Получить информацию о книге
/// </summary>
private void getInfo()
{
//распаковываем объект
Book book = (Book)lstMain.SelectedItem;
if (book != null)
{
//и получаем из него данные
lblAuthor.Text = book.Author;
lblPagesCount.Text = book.PagesCount;
lblYear.Text = book.Year;
}
}
В итоге при выделении элемента, его информация будет отображаться на форме.
К статье прилагается исходный код для более глубокого изучения.
На этом все. Удачного кодинга!
Популярность: 25%
Если у вас возникли вопросы, вы можете оставить их в комментариях


Спасибо больше! Статья очень помогла. Неделю мучался с рисованием элементов, никак не мог понять как что делается.
Теперь и сделал красиво и разобрался!
Будут идеи по улучшению компонента, пишите.
Как осуществить рисование картинки не по центру, а, например, прижатой к левому краю. и сразу после этой картинки идет текст
вот эта строка e.Graphics.DrawImage(Properties.Resources.document, e.Bounds.Width / 2 — Properties.Resources.document.Width / 2, e.Bounds.Top); рисует картинку по центру, при этом определяется где центр. В вашем случае можно попробовать написать e.Graphics.DrawImage(Properties.Resources.document, 0, e.Bounds.Top);
За текст отвечает строка e.Graphics.DrawString в которой вторым парамертром вам нужно поставить отступ от левого края. Если отступ будем меньше чем ширина картинки, то текст залезет на картинку. Удачного кодинга.
Предо мной встала задача: создать ячейки такого формата: сначала по центру идет какой-либо текст опр. размера, потом ниже идет картинка и после нее тоже текст, но уже меньшего размера. Не поможете ли в реализации?
Чего ж не помочь =) Помогу. На основе этого компонента, что в статье:
[csharp]
Font drawFont = new Font("Arial", 16);
e.Graphics.DrawString(s, drawFont, new SolidBrush(SystemColors.WindowText), new RectangleF(0, e.Bounds.Y, e.Bounds.Width, 16), sf);
e.Graphics.DrawImage(Properties.Resources.document, e.Bounds.Width / 2 — Properties.Resources.document.Width / 2, 20);
drawFont = new Font("Arial", 12);
Font drawFont = new Font("Arial", 16);
e.Graphics.DrawString(s, drawFont, new SolidBrush(SystemColors.WindowText), new RectangleF(0, e.Bounds.Y + 50, e.Bounds.Width, 16), sf);
[/csharp]
Вам нужно только выставить отступ от верхнего края в зависимости от высоты картинки.
До конца разобрался в вашем компоненте… Руки у вас золотые
Благодарю за помощь!
Спасибо за компонент. Подскажите, пожалуйста:
1) Как сделать чтобы он работал в WEB приложении
2) Как сделать чтобы в каждом пункте отображалась своя картинка (обложка книжки)
1) В WEB приложении работать не будет. Это исключительно десктопный компонент.
2) Картинка отрисовывается в методе Paint самого компонента. DrawImage рисует нужную вам картинку. Картинка хранится в ресурсах программы, например, Properties.Resources.document
Пытался сделать свой контрол такой, чтобы элементы в списке были «стеклянные». Только при обработке OnDrawItem почему то Bound размеры были по высоте не 40, как установлен ItemHeight, а был более 250px.
Может были такие проблемы и у Вас?
а точно Bound больше 250? Может вы где-то ошиблись с заданием координат при отрисовке? Например gfxPath.AddLine