Модный ProgressBar своими руками
Иногда у меня появляется небольшое количество свободного времени, которое, однако, трудно потратить на нечто грандиозное. Вообще, я люблю сочинять свои собственные элементы управления, аналогичные стандартным, но с какой-нибудь "изюминкой". Как только я поставил себе антивирус AVG, я обратил внимание на его довольно симпатичный прогресс-бар (см. рис.). Первоначально его изображение черно-белое, но по мере заполнения становится цветным. И мне захотелось написать что-то подобное. Нет, я вовсе не был намерен использовать такой прогресс-бар в одной из своих программ. Я просто хотел проверить: а смогу ли я это повторить? У меня было около трех часов свободного времени, немного знаний по преобразованию изображений с помощью матриц и компилятор C#. С волей к победе я принялся за дело.
Возьмем в руки кисть и краски
Перед началом работы не забудьте описать часто используемые пространства имен.
using System;
using System.Drawing;using System.Drawing.Imaging;
using System.Windows.Forms;
Первое, что я сделал, — это написал служебный класс Convert, в задачи которого входит преобразовывать изображение из цветного в черно-белое.
public class Convert
{
}
Вообще впредь мы будем использовать одну из перегрузок метода Graphics.DrawImage, которой для вывода нового изображения требуется экземпляр класса System. Drawing.Imaging.ImageAttributes. Соответственно, необходимо как-то эти самые атрибуты получать. Для этого заведем защищенный (protected) метод GetMatrix, который будет работать непосредственно с матрицами. На всякий случай я добавил сюда матрицу не только для черно-белого изображения, но еще и для сепии. Для того чтобы в процессе работы различать, какие атрибуты я сейчас хочу получить, я дополнительно описал перечисление MatrixType, которое на данный момент содержит два элемента: GreyScale и Sepia. Вот как я реализовал вышесказанное в коде:
protected ImageAttributes GetMatrix (MatrixType matrixType)
{
ColorMatrix matrix = null;
switch (matrixType) {
case MatrixType.GreyScale:
matrix = new ColorMatrix(new float[][] {
new float[]{0.3086f, 0.3086f, 0.3086f, 0.0f, 0.0f},
new float[]{0.6094f, 0.6094f, 0.6094f, 0.0f, 0.0f},
new float[]{0.0820f, 0.0820f, 0.0820f, 0.0f, 0.0f},
new float[]{0.0f, 0.0f, 0.0f, 1.0f, 0.0f},
new float[]{0.0f, 0.0f, 0.0f, 0.0f, 1.0f}});
break;
case MatrixType.Sepia:
matrix = new ColorMatrix(new float[][] {
new float[] {0.393f, 0.349f, 0.272f, 0.0f, 0.0f},
new float[] {0.769f, 0.686f, 0.534f, 0.0f, 0.0f}
new float[] {0.189f, 0.168f, 0.131f, 0.0f, 0.0f},
new float[] {0.0f, 0.0f, 0.0f, 1.0f, 0.0f},
new float[] {0.0f, 0.0f, 0.0f, 0.0f, 1.0f}});
break;
}
ImageAttributes attributes = new ImageAttributes();
attributes.SetColorMatrix(matrix);
return attributes;
}
protected enum MatrixType {
GreyScale,
Sepia,
}
Внешний интерфейс реализован посредством публичного метода ToGreyScale. Для удобства я снабдил его двумя перегрузками. Первая из них принимает ссылку только на само изображение, вторая же еще и просит предоставить ей параметры области, в которую будет выводиться преобразованное изображение. Подумав еще немного, я написал своего рода "прокладку". Как я уже говорил, возможно, в будущем мне захочется преобразовать изображение не только в черно-белое, но и в любое другое. Тогда при таком раскладе мне не придется переписывать еще раз уже имеющийся код.
public virtual void ToGreyScale (Image image)
{
ToGreyScale(image, new Rectan-gle(0, 0, image.Width, image.Height));
}
public virtual void ToGreyScale (Image image, Rectangle bounds)
{
PerformChanges(image, bounds, GetMatrix(MatrixType.GreyScale));
}
protected virtual void Perform Changes(Image image, Rectangle bounds, ImageAttributes attributes)
{
Graphics g = Graphics.From Image(image);
g.DrawImage(image, bounds, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, attributes);
}
Отлично. Следующим этапом было написание собственно прогресс-бара. Для начала опишем сам класс.
public class GSProgressBar: Panel
{
}
Возиться с перерисовкой стандартного прогресс-бара бесполезно. Гораздо проще унаследоваться от Panel и рисовать все, что душе заблагорассудится. Так каким же образом будет происходить заполнение нашего прогресс-бара, ведь у нас есть логика только для приведения изображения к черно-белому? Для этого при инициализации экземпляра класса сохраняем первоначальное изображение, а затем преобразовываем его в черно-белое. Таким образом, у нас остается два изображения: одно цветное, другое — черно-белое. Из цветного мы в процессе заполнения прогресс-бара будем выбирать необходимую область и перерисовывать прогресс-бар. При этом изображение мелькает, поэтому придется включить двойную буферизацию. В данном случае defaultImage — это черно-белое изображение, подложка, а overlayImage — цветное, которое мы будем накладывать поверх черно-белого. Посмотрите, каким образом я инициализирую переменную overlayImage. Причина тому довольно проста. Экземпляр класса передается по ссылке, поэтому при обычном присвоении как в переменной defaultImage, так и в переменной overlayImage содержалась бы ссылка на один и тот же объект. Получается, если мы приведем defaultImage к черно-белому, то "испортится" и overlayImage, что неприемлемо.
private Image defaultImage = null;
private Image overlayImage = null;
public GSProgressBar(Image image)
{
this.SetStyle(ControlStyles.User Paint, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles. DoubleBuffer, true);
this.defaultImage = image;
this.overlayImage = (Image) image.Clone();
imageService.Convert convert = new imageService.Convert();
convert.ToGreyScale(this.default Image);
}
Чтобы разработчик при использовании нашего контрола ничего не заподозрил, сымитируем работу оригинального прогресс-бара, т.е. добавим те же свойства и методы.
Думаю, их назначение вам хорошо знакомо. Обратите внимание на вызов метода Refresh в свойстве Value. Его следует осуществлять в обязательном порядке, иначе Panel просто не будет перерисовываться.
private ushort internalMaximum = 100;
private ushort internalMinimum = 0;
private ushort internalStep = 1;
private ushort internalValue = 0;
public virtual ushort Maximum {
get { return this.internalMaximum; }
set { this.internalMaximum = value; }
}
public virtual ushort Minimum {
get { return this.internalMinimum; }
set { this.internalMinimum = value; }
}
public virtual ushort Step {
get { return this.internalStep; }
set { this.internalStep = value; }
}
public virtual ushort Value
{
get { return this.internalValue; }
set {
if (this.internalValue > = this. internalMaximum) {
this.internalValue = this.internal Maximum;
} else {
this.internalValue = value;
}
this.Refresh();
}
}
public virtual void Increment(ushort step)
{
this.Value += step;
}
public virtual void PerformStep()
{
this.Value += internalStep;
}
Отображать изменения сообразно значениям свойств будем в методе OnPaint. Но прежде чем окунуться в логику его работы, напишем два сервисных метода, которые будут преобразовывать значения свойства Value в ширину самого прогресс-бара, а также используемой картинки. Т.е. при изменении Value должна изменяться и степень заполнения прогресс-бара. Для этого выбираем необходимую область из цветного изображения и заполняем соответствующую область прогресс-бара. Параметры этих областей могут быть различны (ширина и/или высота), поэтому будем рассчитывать процентное соотношение для каждого случая.
protected virtual float ValueToImage Width(ushort value)
{
double percent = (double) this.overlayImage.Width / this.internal Maximum;
return (float)(percent * value);
}
protected virtual float ValueToWidth (ushort value)
{
return (float)((double)this.Width * value / this.internalMaximum);
}
Ну, а теперь, как и обещал, разберем работу OnPaint. В общем-то, принцип работы GSProgressBar я описывал пару абзацев назад. Поэтому остановимся лишь на некоторых интересных моментах. Стоит учитывать, что по величине исходный размер может отличаться от прогресс-бара. Но мощный GDI+ не даст нам пропасть. Эта проблема хорошо описана в руководстве для разработчиков Programming Microsoft Windows with C#, где его автор Ч. Петцольд экспериментировал с изображением астронавта. Следуя примеру Петцольда, возьмем два экземпляра класса Rectangle. Один из них описывает область исходного изображения, второй — выходного. Для перекрывающего изображения я, правда, использовал RectangleF, который, в отличие от Rectangle, работает с типом float, а не int (что и отражено в названии). При использовании Rectangle правая граница изображения движется неравномерно из-за округления то в большую, то в меньшую сторону, т.е. плавает шаг. RectangleF не дает такого эффекта. Здесь нам как раз и пригодятся методы ValutToImageWidth и ValueToWidth. Далее вызываем метод DrawImage и рисуем последовательно черно-белое изображение, а поверх него — цветное. Все довольно просто, когда знаешь, как:
protected override void OnPaint (PaintEventArgs e)
{
Graphics g = e.Graphics;
// Default image.
Rectangle defaultSrcRect = new Rectangle(0, 0, this.defaultImage. Width, this.defaultImage.Height);
Rectangle defaultDestRect = new Rectangle(0, 0, this.Width, this.Height);
g.DrawImage(this.defaultImage, defaultDestRect, defaultSrcRect, GraphicsUnit.Pixel);
// Overlay image.
RectangleF overlaySrcRect = new RectangleF(0, 0, this.ValueToImage Width(internalValue), this.overlay Image.Height);
RectangleF overlayDestRect = new RectangleF(0, 0, this.ValueToWidth (internalValue), this.Height);
g.DrawImage(this.overlayImage, overlayDestRect, overlaySrcRect, GraphicsUnit.Pixel);
}
Для пущей важности я поместил прогресс-бар в рамку. Если свет у нас падает слева, то левая и верхняя границы должны быть светлее, чем нижняя и правая. Получаем четыре опорные точки — углы прогресс-бара — и по ним проводим соответствующие линии. В метод OnPaint дописываем:
// 3D Frame.
Pen light = new Pen(new SolidBrush (Color.FromKnownColor(KnownColor.HighlightText)), 2);
Pen shadow = new Pen(new SolidBrush(Color.FromKnownColor(KnownColor.ControlText)), 2);
Point upperLeft = new Point(this. ClientRectangle.X, this.ClientRectangle.Y);
Point upperRight = new Point (this.ClientRectangle.Width — this. ClientRectangle.X * 2, this.Client Rectangle.Y);
Point lowerLeft = new Point (this.ClientRectangle.X, this.Client Rectangle.Height — this.ClientRectan-gle.Y * 2);
Point lowerRight = new Point (this.ClientRectangle.Width — this. ClientRectangle.Y * 2, this.ClientRec-tangle.Height — this.ClientRectangle.Y * 2);
g.DrawLine(light, upperLeft, upper Right);
g.DrawLine(light, upperLeft, lower Left);
g.DrawLine(shadow, upperRight, lowerRight);
g.DrawLine(shadow, lowerLeft, lowerRight);
Компилируем библиотечку.
Тестирование
Замечательно. У нас есть еще около 20 минут, чтобы полюбоваться на наше творение. Добавляем в решение еще один проект и ссылаемся на библиотеку GSProgressBar.dll. Теперь в классе главной формы объявляем экземпляр GSProgressBar:
private GSProgressBar progressBar = new GSProgressBar(Image. FromFile ("images/CodeProject.jpg"));
Использовать новоиспеченный контрол так же элементарно просто, как и стандартный. Например, в событии Tick таймера пишем что-то вроде этого:
this.progressBar.PerformStep();
Любуемся результатом.
Заключение
Вот так меньше чем за три часа я доказал себе, что такие "побрякушки" мне по зубам. Я вовсе не настаиваю на том, что в реализации GSProgressBar есть что-то сверхъестественное. Но, возможно, кому-то мой прогресс-бар покажется интересным, и он захочет заюзать его в своем приложении. А кто-то может пойти дальше в своих изысканиях графических возможностей GDI+. Например, можно пройти по пути создателей широкоизвестной игры Moorhuhn, в частности, последней версии, где при загрузке прогресс-баром служит большая буква X (см. рис.). Можно поиграться с прозрачностью накладываемого изображения. В общем, поле для творчества ограничивается только вашей фантазией. Дерзайте!
2005, Алексей Нестеров, eisernWolf@tut.by
Возьмем в руки кисть и краски
Перед началом работы не забудьте описать часто используемые пространства имен.
using System;
using System.Drawing;using System.Drawing.Imaging;
using System.Windows.Forms;
Первое, что я сделал, — это написал служебный класс Convert, в задачи которого входит преобразовывать изображение из цветного в черно-белое.
public class Convert
{
}
Вообще впредь мы будем использовать одну из перегрузок метода Graphics.DrawImage, которой для вывода нового изображения требуется экземпляр класса System. Drawing.Imaging.ImageAttributes. Соответственно, необходимо как-то эти самые атрибуты получать. Для этого заведем защищенный (protected) метод GetMatrix, который будет работать непосредственно с матрицами. На всякий случай я добавил сюда матрицу не только для черно-белого изображения, но еще и для сепии. Для того чтобы в процессе работы различать, какие атрибуты я сейчас хочу получить, я дополнительно описал перечисление MatrixType, которое на данный момент содержит два элемента: GreyScale и Sepia. Вот как я реализовал вышесказанное в коде:
protected ImageAttributes GetMatrix (MatrixType matrixType)
{
ColorMatrix matrix = null;
switch (matrixType) {
case MatrixType.GreyScale:
matrix = new ColorMatrix(new float[][] {
new float[]{0.3086f, 0.3086f, 0.3086f, 0.0f, 0.0f},
new float[]{0.6094f, 0.6094f, 0.6094f, 0.0f, 0.0f},
new float[]{0.0820f, 0.0820f, 0.0820f, 0.0f, 0.0f},
new float[]{0.0f, 0.0f, 0.0f, 1.0f, 0.0f},
new float[]{0.0f, 0.0f, 0.0f, 0.0f, 1.0f}});
break;
case MatrixType.Sepia:
matrix = new ColorMatrix(new float[][] {
new float[] {0.393f, 0.349f, 0.272f, 0.0f, 0.0f},
new float[] {0.769f, 0.686f, 0.534f, 0.0f, 0.0f}
new float[] {0.189f, 0.168f, 0.131f, 0.0f, 0.0f},
new float[] {0.0f, 0.0f, 0.0f, 1.0f, 0.0f},
new float[] {0.0f, 0.0f, 0.0f, 0.0f, 1.0f}});
break;
}
ImageAttributes attributes = new ImageAttributes();
attributes.SetColorMatrix(matrix);
return attributes;
}
protected enum MatrixType {
GreyScale,
Sepia,
}
Внешний интерфейс реализован посредством публичного метода ToGreyScale. Для удобства я снабдил его двумя перегрузками. Первая из них принимает ссылку только на само изображение, вторая же еще и просит предоставить ей параметры области, в которую будет выводиться преобразованное изображение. Подумав еще немного, я написал своего рода "прокладку". Как я уже говорил, возможно, в будущем мне захочется преобразовать изображение не только в черно-белое, но и в любое другое. Тогда при таком раскладе мне не придется переписывать еще раз уже имеющийся код.
public virtual void ToGreyScale (Image image)
{
ToGreyScale(image, new Rectan-gle(0, 0, image.Width, image.Height));
}
public virtual void ToGreyScale (Image image, Rectangle bounds)
{
PerformChanges(image, bounds, GetMatrix(MatrixType.GreyScale));
}
protected virtual void Perform Changes(Image image, Rectangle bounds, ImageAttributes attributes)
{
Graphics g = Graphics.From Image(image);
g.DrawImage(image, bounds, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, attributes);
}
Отлично. Следующим этапом было написание собственно прогресс-бара. Для начала опишем сам класс.
public class GSProgressBar: Panel
{
}
Возиться с перерисовкой стандартного прогресс-бара бесполезно. Гораздо проще унаследоваться от Panel и рисовать все, что душе заблагорассудится. Так каким же образом будет происходить заполнение нашего прогресс-бара, ведь у нас есть логика только для приведения изображения к черно-белому? Для этого при инициализации экземпляра класса сохраняем первоначальное изображение, а затем преобразовываем его в черно-белое. Таким образом, у нас остается два изображения: одно цветное, другое — черно-белое. Из цветного мы в процессе заполнения прогресс-бара будем выбирать необходимую область и перерисовывать прогресс-бар. При этом изображение мелькает, поэтому придется включить двойную буферизацию. В данном случае defaultImage — это черно-белое изображение, подложка, а overlayImage — цветное, которое мы будем накладывать поверх черно-белого. Посмотрите, каким образом я инициализирую переменную overlayImage. Причина тому довольно проста. Экземпляр класса передается по ссылке, поэтому при обычном присвоении как в переменной defaultImage, так и в переменной overlayImage содержалась бы ссылка на один и тот же объект. Получается, если мы приведем defaultImage к черно-белому, то "испортится" и overlayImage, что неприемлемо.
private Image defaultImage = null;
private Image overlayImage = null;
public GSProgressBar(Image image)
{
this.SetStyle(ControlStyles.User Paint, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles. DoubleBuffer, true);
this.defaultImage = image;
this.overlayImage = (Image) image.Clone();
imageService.Convert convert = new imageService.Convert();
convert.ToGreyScale(this.default Image);
}
Чтобы разработчик при использовании нашего контрола ничего не заподозрил, сымитируем работу оригинального прогресс-бара, т.е. добавим те же свойства и методы.
Думаю, их назначение вам хорошо знакомо. Обратите внимание на вызов метода Refresh в свойстве Value. Его следует осуществлять в обязательном порядке, иначе Panel просто не будет перерисовываться.
private ushort internalMaximum = 100;
private ushort internalMinimum = 0;
private ushort internalStep = 1;
private ushort internalValue = 0;
public virtual ushort Maximum {
get { return this.internalMaximum; }
set { this.internalMaximum = value; }
}
public virtual ushort Minimum {
get { return this.internalMinimum; }
set { this.internalMinimum = value; }
}
public virtual ushort Step {
get { return this.internalStep; }
set { this.internalStep = value; }
}
public virtual ushort Value
{
get { return this.internalValue; }
set {
if (this.internalValue > = this. internalMaximum) {
this.internalValue = this.internal Maximum;
} else {
this.internalValue = value;
}
this.Refresh();
}
}
public virtual void Increment(ushort step)
{
this.Value += step;
}
public virtual void PerformStep()
{
this.Value += internalStep;
}
Отображать изменения сообразно значениям свойств будем в методе OnPaint. Но прежде чем окунуться в логику его работы, напишем два сервисных метода, которые будут преобразовывать значения свойства Value в ширину самого прогресс-бара, а также используемой картинки. Т.е. при изменении Value должна изменяться и степень заполнения прогресс-бара. Для этого выбираем необходимую область из цветного изображения и заполняем соответствующую область прогресс-бара. Параметры этих областей могут быть различны (ширина и/или высота), поэтому будем рассчитывать процентное соотношение для каждого случая.
protected virtual float ValueToImage Width(ushort value)
{
double percent = (double) this.overlayImage.Width / this.internal Maximum;
return (float)(percent * value);
}
protected virtual float ValueToWidth (ushort value)
{
return (float)((double)this.Width * value / this.internalMaximum);
}
Ну, а теперь, как и обещал, разберем работу OnPaint. В общем-то, принцип работы GSProgressBar я описывал пару абзацев назад. Поэтому остановимся лишь на некоторых интересных моментах. Стоит учитывать, что по величине исходный размер может отличаться от прогресс-бара. Но мощный GDI+ не даст нам пропасть. Эта проблема хорошо описана в руководстве для разработчиков Programming Microsoft Windows with C#, где его автор Ч. Петцольд экспериментировал с изображением астронавта. Следуя примеру Петцольда, возьмем два экземпляра класса Rectangle. Один из них описывает область исходного изображения, второй — выходного. Для перекрывающего изображения я, правда, использовал RectangleF, который, в отличие от Rectangle, работает с типом float, а не int (что и отражено в названии). При использовании Rectangle правая граница изображения движется неравномерно из-за округления то в большую, то в меньшую сторону, т.е. плавает шаг. RectangleF не дает такого эффекта. Здесь нам как раз и пригодятся методы ValutToImageWidth и ValueToWidth. Далее вызываем метод DrawImage и рисуем последовательно черно-белое изображение, а поверх него — цветное. Все довольно просто, когда знаешь, как:
protected override void OnPaint (PaintEventArgs e)
{
Graphics g = e.Graphics;
// Default image.
Rectangle defaultSrcRect = new Rectangle(0, 0, this.defaultImage. Width, this.defaultImage.Height);
Rectangle defaultDestRect = new Rectangle(0, 0, this.Width, this.Height);
g.DrawImage(this.defaultImage, defaultDestRect, defaultSrcRect, GraphicsUnit.Pixel);
// Overlay image.
RectangleF overlaySrcRect = new RectangleF(0, 0, this.ValueToImage Width(internalValue), this.overlay Image.Height);
RectangleF overlayDestRect = new RectangleF(0, 0, this.ValueToWidth (internalValue), this.Height);
g.DrawImage(this.overlayImage, overlayDestRect, overlaySrcRect, GraphicsUnit.Pixel);
}
Для пущей важности я поместил прогресс-бар в рамку. Если свет у нас падает слева, то левая и верхняя границы должны быть светлее, чем нижняя и правая. Получаем четыре опорные точки — углы прогресс-бара — и по ним проводим соответствующие линии. В метод OnPaint дописываем:
// 3D Frame.
Pen light = new Pen(new SolidBrush (Color.FromKnownColor(KnownColor.HighlightText)), 2);
Pen shadow = new Pen(new SolidBrush(Color.FromKnownColor(KnownColor.ControlText)), 2);
Point upperLeft = new Point(this. ClientRectangle.X, this.ClientRectangle.Y);
Point upperRight = new Point (this.ClientRectangle.Width — this. ClientRectangle.X * 2, this.Client Rectangle.Y);
Point lowerLeft = new Point (this.ClientRectangle.X, this.Client Rectangle.Height — this.ClientRectan-gle.Y * 2);
Point lowerRight = new Point (this.ClientRectangle.Width — this. ClientRectangle.Y * 2, this.ClientRec-tangle.Height — this.ClientRectangle.Y * 2);
g.DrawLine(light, upperLeft, upper Right);
g.DrawLine(light, upperLeft, lower Left);
g.DrawLine(shadow, upperRight, lowerRight);
g.DrawLine(shadow, lowerLeft, lowerRight);
Компилируем библиотечку.
Тестирование
Замечательно. У нас есть еще около 20 минут, чтобы полюбоваться на наше творение. Добавляем в решение еще один проект и ссылаемся на библиотеку GSProgressBar.dll. Теперь в классе главной формы объявляем экземпляр GSProgressBar:
private GSProgressBar progressBar = new GSProgressBar(Image. FromFile ("images/CodeProject.jpg"));
Использовать новоиспеченный контрол так же элементарно просто, как и стандартный. Например, в событии Tick таймера пишем что-то вроде этого:
this.progressBar.PerformStep();
Любуемся результатом.
Заключение
Вот так меньше чем за три часа я доказал себе, что такие "побрякушки" мне по зубам. Я вовсе не настаиваю на том, что в реализации GSProgressBar есть что-то сверхъестественное. Но, возможно, кому-то мой прогресс-бар покажется интересным, и он захочет заюзать его в своем приложении. А кто-то может пойти дальше в своих изысканиях графических возможностей GDI+. Например, можно пройти по пути создателей широкоизвестной игры Moorhuhn, в частности, последней версии, где при загрузке прогресс-баром служит большая буква X (см. рис.). Можно поиграться с прозрачностью накладываемого изображения. В общем, поле для творчества ограничивается только вашей фантазией. Дерзайте!
2005, Алексей Нестеров, eisernWolf@tut.by
Компьютерная газета. Статья была опубликована в номере 06 за 2005 год в рубрике программирование :: разное