В предыдущей статье я писал об использовании шаблона формы «О программе». Такие шаблоны очень удобно использовать для написания программ. Библиотека быстрого доступа dbFacade к базе данных SQLite, также является шаблоном, который можно усовершенствовать и подогнать под конкретную программу. Такой подход существенно экономит время при разработке программы и уменьшает количество ошибок, т.к. код проверяется и «шлифуется» от приложения к приложению.
Сейчас я хочу представить на ваш суд заготовку для окна приветствия. В народе называемой Splash screen. Для пользователя это лицо программы. Это то, что он видит до запуска основного окна программы, ожидая окончательной загрузки. Вопрос: для каких целей и когда следует использовать в программе splashscreen? В принципе, лепить это окно можно во все программы. Почему «лепить»? Потому, что это не всегда уместно. Если приложение загружается за одну секунду, тогда зачем показывать еще одно окно, которое будет висеть и не давать работать сразу же после появления главного окна? Таким образом, я вижу смысл вставлять splashscreen только в те программы, когда можно успеть прочитать все что на нем написано, до появления основного окна. Информация в этом окне может быть различной. Обычно я показываю, что в данный момент происходит с программой. Это может быть создание кэша записей из базы данных или active directory, загрузка картинок в память, шрифтов и т.д. Т.е. нужно показать пользователю, что программа загружается, что ей нужно время, чтобы подготовиться к работе. Иначе, человек может подумать что программа не запускается и будет пытаться запустить ее повторно.
После подготовки splashscreen для каждой программы, я понял что это дело нужно как-то ускорить. Каждый раз писать новое окно приветствия и «О программе» скучно и долго.
Требования, которые я предъявляю к классу окна приветствия:
- запуск до основного окна;
- доступ к объекту splashscreen из любого класса. Для того, чтобы можно было передавать данные в этот объект и управлять им в любой момент;
- возможность вставлять прозрачную картинку с любым уровнем прозрачности;
- иметь самые простые спецэффекты, вроде мягкого возникновения и затухания;
- быстрая настройка при добавлении в новое приложение.
Все перечисленное невозможно реализовать средствами .NET, значит будем использовать WINAPI.
Реализация
Главная функция, которая реализует эффект прозрачности любого уровня называется UpdateLayeredWindow. У нее есть существенные недостатки. Событие OnPaint, при работе этой функции, работает некорректно. Все контролы на форме прорисовываться не будут. Они будут невидимы. Так, что про него него можно забыть. Использовать его не будем. Вся прорисовка на форме должна происходить в методе SetBitmap. В этом методе мы рисуем текст и все остальное. Label использовать нельзя.
Принцип работы метода такой: мы передаем картинку, на этой картинке должны нарисовать все, что нам нужно – контролы, текст, линии и т.д. Потом вызываем функцию UpdateLayeredWindow и она уже прорисовывает нашу форму. Также, нужно помнить, что эта функция применима только для объектов форм, а не конролов. Для отображения контролов в методе SetBitmap используется следующий хак:
1 |
foreach (Control ctrl in this .Controls) |
2 |
ctrl.DrawToBitmap(temp_bmp, ctrl.Bounds); |
Как видите, мы проходит по всем конролам, расположенных на форме и переводим их в картинки. Но, еще раз повторю, контролы лучше не использовать. Теряем внешний вид. Лучше рисовать все вручную.
Т.к. у нас не работает метод OnPaint, мы должны сами прорисовывать форму. Создаем таймер, который будет выполнять метод SetBitmap каждую сотую секунды. А в самом методе, всегда делаем копию основной картинки, и работаем только с копией.
1 |
Bitmap temp_bmp = new Bitmap(bitmap); |
После перевода всех конролов в картинки, пишем текст. Для удобства я создал метод WriteText, который и рисует все элементы на картинке. После всех приготовления вызываем функцию UpdateLayeredWindow, передаем ей нужные параметры и наслаждаемся результатом.
Полный код метода:
1 |
public void SetBitmap(Bitmap bitmap, byte opacity) |
3 |
if (bitmap.PixelFormat != PixelFormat.Format32bppArgb) |
4 |
throw new ApplicationException( "Картинка должна иметь 32-битный альфа-канал" ); |
7 |
_FormOpacity = opacity; |
9 |
IntPtr screenDc = GetDC(IntPtr.Zero); |
10 |
IntPtr memDc = CreateCompatibleDC(screenDc); |
11 |
IntPtr hBitmap = IntPtr.Zero; |
12 |
IntPtr oldBitmap = IntPtr.Zero; |
16 |
Bitmap temp_bmp = new Bitmap(bitmap); |
17 |
foreach (Control ctrl in this .Controls) |
18 |
ctrl.DrawToBitmap(temp_bmp, ctrl.Bounds); |
20 |
if (Bounds.Width > 0 && Bounds.Height > 0 && Visible) |
25 |
g = Graphics.FromImage(temp_bmp); |
26 |
g.SetClip( new Rectangle(0, 0, this .Width, this .Height)); |
30 |
catch (Exception exception) |
32 |
System.Diagnostics.StackFrame stackFrame = new System.Diagnostics.StackFrame( true ); |
33 |
Console.WriteLine( "\nОшибка: {0}, \n\t{1}, \n\t{2}, \n\t{3}\n" , this .GetType().ToString(), stackFrame.GetMethod().ToString(), stackFrame.GetFileLineNumber(), exception.Message); |
36 |
hBitmap = temp_bmp.GetHbitmap(Color.FromArgb(0)); |
38 |
oldBitmap = SelectObject(memDc, hBitmap); |
40 |
Size size = new Size(bitmap.Width, bitmap.Height); |
41 |
Point pointSource = new Point(0, 0); |
43 |
Rectangle screenArea = SystemInformation.WorkingArea; |
44 |
int nX = (screenArea.Width - Width) / 2; |
45 |
int nY = (screenArea.Height - Height) / 2; |
46 |
Point topPos = new Point(nX, nY); |
48 |
BLENDFUNCTION blend = new BLENDFUNCTION(); |
49 |
blend.BlendOp = AC_SRC_OVER; |
51 |
blend.SourceConstantAlpha = opacity; |
52 |
blend.AlphaFormat = AC_SRC_ALPHA; |
53 |
UpdateLayeredWindow(Handle, screenDc, ref topPos, ref size, memDc, ref pointSource, 0, ref blend, ULW_ALPHA); |
57 |
ReleaseDC(IntPtr.Zero, screenDc); |
58 |
if (hBitmap != IntPtr.Zero) |
60 |
SelectObject(memDc, oldBitmap); |
62 |
DeleteObject(hBitmap); |
На картинке текст пишем следующим образом:
2 |
g = Graphics.FromImage(temp_bmp); |
3 |
g.SetClip( new Rectangle(0, 0, this .Width, this .Height)); |
6 |
private void WriteText(Graphics g) |
8 |
g.TextRenderingHint = TextRenderingHint.SystemDefault; |
9 |
SolidBrush brushFont = new SolidBrush(Color.White); |
11 |
g.TextRenderingHint = TextRenderingHint.AntiAlias; |
12 |
DrawText(g, _productname, 25, 30, new Font(Instance.Font.FontFamily, 30, FontStyle.Bold), brushFont); |
13 |
g.TextRenderingHint = TextRenderingHint.SystemDefault; |
14 |
DrawText(g, _productversion, 265, 72, Instance.Font, brushFont); |
15 |
DrawText(g, _title, 25, 135, new Font(Instance.Font.FontFamily, Instance.Font.SizeInPoints, FontStyle.Bold), brushFont); |
16 |
DrawText(g, _currenttitle, 25, 150, Font, brushFont); |
17 |
DrawText(g, _copyrights, 25, 185, Font, brushFont); |
22 |
private static void DrawText(Graphics g, string text, int x, int y, Font font, Brush brush) |
24 |
int _nTextOffsetY = 0; |
25 |
int _nTextOffsetX = 0; |
27 |
SizeF sizeF = g.MeasureString(text, font, Instance.Width, StringFormat.GenericDefault); |
28 |
_nTextOffsetX += Convert.ToInt32(Math.Ceiling(sizeF.Width)); |
29 |
_nTextOffsetY += Convert.ToInt32(Math.Ceiling(sizeF.Height)); |
30 |
RectangleF rectangleF = new RectangleF(x, y, _nTextOffsetX, _nTextOffsetY); |
31 |
g.DrawString(text, font, brush, rectangleF, StringFormat.GenericDefault); |
В методе WriteText ряд вызовов метода DrawText, который непосредственно и рисует текст. Строки
1 |
g.TextRenderingHint = TextRenderingHint.AntiAlias; |
3 |
g.TextRenderingHint = TextRenderingHint.SystemDefault; |
переключают режим рендеринга текста. При маленьком размере шрифта не следует использовать TextRenderingHint.AntiAlias, т.к. текст получается смазанным. При большом, наоборот, рекомендую. В DrawText передаются координаты X и Y. Их придется настраивать методом пробы. В режиме дизайна нельзя посмотреть как будет рисоваться окно. Больше о том как рисовать, ищите сами. Информации полно.
Как пользоваться
Подключаем к проекту файл SplashScreen.cs. В части кода после «#region Настройки» мы прописываем переменные, текст которых будет отображаться в окне. Доступ к переменным описывается в конце этого файла – «#region Свойства». Модификатор доступа должен быть установлен в static. Не забывайте. Сейчас в классе описано следующее:
1 |
public static void SetTitle( string text) |
6 |
public static void SetBackgroundImage(Image image) |
8 |
Instance.BackgroundImage = image; |
9 |
Instance.Width = image.Width; |
10 |
Instance.Height = image.Height; |
13 |
public static void SetCurrentTitle( string text) |
15 |
Instance.CurrentTitle = text; |
18 |
public static void SetFadeIn( bool iffadein) |
20 |
Instance.FadeIn = iffadein; |
Через эти методы и происходит «общение» с объектом.
Подключили файл к проекту. Поменяли в файле namespace на имя которое уже в проекте, чтобы не заморачиваться прописывать в каждом файле using, из которого будет доступ к нашему объекту.
Далее в файле Program.cs пишем следующие строки:
2 |
SplashScreen.Instance.Font = new System.Drawing.Font( "Tahoma" , 12, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Pixel, (( byte )(0))); |
4 |
SplashScreen.SetBackgroundImage(splashscreen.Properties.Resources.logo); |
6 |
SplashScreen.SetFadeIn( true ); |
8 |
SplashScreen.BeginDisplay(); |
SplashScreen.BeginDisplay() и SplashScreen.EndDisplay() соотвественно показывает форму или скрывает. SplashScreen.SetFadeIn(true) задает эффект. Свойство эффекта нужно задавать прямо в коде класса.
Для того, чтобы передавать данные на форму достаточно вызывать
1 |
SplashScreen.SetTitle( "Загрузка" + dotted); |
2 |
SplashScreen.SetCurrentTitle( "Текущая загрузка " + Math.Ceiling(procent) + "%" ); |
Это пример из текстового проекта. В итоге у меня получилось так:
Заключение
На мой взгляд в статье описано только самое важное. Картинку для фона готовьте сразу с тенью, если она вам нужна. Задавать прозрачность картинки можно как угодно – с любым уровнем прозрачности каждого пикселя. Обязательно посмотрите тестовый проект к этой статье. Я думаю, что со временем класс будет улучшаться, и в нем появятся новые эффекты.