Техника программирования сложных окон в Visual Basic

Техника программирования сложных окон в Visual Basic 

Введение

Mногие из Вас наверняка видели в Windows программах окна нестандартной формы (круглые, треугольные и т.д.) и задавали себе вопрос: как мне сделать такое окно? Если прочитать документацию по Visual Basic, то можно сделать вывод, что стандартные средства языка не предоставляют такой возможности. А что же делать, если очень хочется? Тогда следует вспомнить, что в распоряжении программиста на VB есть еще и Windows API, который должен нам в этом помочь. 

Теоретические основы

Для начала давайте разберемся, как это можно сделать теоретически. Из документации Windows видно, что каждое окно в системе описывается множеством параметров, из которых нас с Вами интересует <видимая область окна>. Видимая область окна в системе, создаваемое Visual Basic <по умолчанию> имеет вид прямоугольника, но, в принципе, ничто не мешает изменить форму этой области. Данная область окна описывается с помощью специального объекта, который называется Region. Регион можно представить в виде поверхности, ограниченной координатами, описываемыми угловые точки этой области. Проще говоря, можно описать область любой формы, затем создать из неё, с помощью специальных функций, регион и <прикрепить> его к нужому нам окну. 

Существует несколько функций Windows API для создания регионов, основными из которых являются следующие: 

CombineRgn - Комбинирует два региона между собой 

CreateEllipticRgn - Создает регион в виде эллипса или окружности 

CreatePolygonRgn - Создает регион в виде многоугольника 

CreateRectRgn - Создает прямоугольный регион 

CreateRoundRectRgn - Создает регион со скругленными краями из прямоугольной области 

SetWindowRgn - Прикрепляет регион к указанному окну 

Я не буду приводить подробное описание этих функций, так как его можно найти в описании Win32 API. Кроме этих функций существуют ещё несколько функций для работы с регионами, но нам они не потребуются. 

Создание простых нестандартных окон

Теперь, когда нам известны основные функции, для создания регионов, мы можем применить полученные знания на практике. Загрузите проект pTestRgn и внимательно изучите его код. В этом проете, для изменения формы окна на овальную, используется всего три строки кода и три функции Win32 API. Вначале с помощью CreateEllipticRgn создается регион, затем он прикрепляется к окну и, наконец, завершающая фаза удаление, ставшего ненужным, созданного нами региона. Если же Вы не удалите ненужный Вам больше объект, то Windows, создав регион для Вас будет хранить его в своих <недрах> и ждать дальнейших указаний по его использованию. В общем, нехорошо <захламлять> выделенную память, и настигнет Вас кара небесная, и затянется небо тучами синими, и будет страшный суд над всеми неверующими: Короче код выглядит так: 

Private Sub cmbCreateOval_Click()

   Dim lRgn As Long

   lRgn = CreateEllipticRgn(0, 0, Me.ScaleWidth / Screen.TwipsPerPixelX, _

   Me.ScaleHeight / Screen.TwipsPerPixelY)

   SetWindowRgn Me.hwnd, lRgn, True

   DeleteObject lRgn

End Sub

Так же всё просто, скажете Вы? Да, на первый взгляд всё очень просто, но это только кажется. Тот пример, который Вы только что видели, почти не имеет практического применения в настоящих приложениях Windows. Кому же нужно просто овальное окно, которое к тому же жестко задается на этапе программирования? А вот окно, которое свободно могло бы менять свою форму вполне может потребоваться. Примеры? Пожалуйста, WinAmp, Помощник в Microsoft Office и другие программы. Как же там всё это реализовано? Давайте разберемся с таким применением регионов. 

Создание сложных нестандартных окон

Допустим, что у нас есть рисунок в BMP формате, из которого нужно сделать форму, а белый цвет (например) на нём означает <пустоту>. Как же сделать форму? Очень просто, нужно взять все <не белые> пиксели на рисунке, создать из их координат регион и прикрепить его к нужному нам окну. Анализировать пиксели можно GetPixel, эта функция по координатам возвращает его цвет. Давайте теперь напишем такой алгоритм для анализа BMP матрицы. Я думаю, что такой алгоритм Вам известен, и мы не будем его подробно разбирать, отмечу только, что анализ производится построчно и Pixel-и добавляются в регион не по одному, а группами построчно. Такой подход сильно экономит ресурсы процессора, выигрыш в производительности достигает 100%. 

Public Function lGetRegion(pic As PictureBox, lBackColor As Long) As Long

   Dim lRgn As Long

   Dim lSkinRgn As Long

   Dim lStart As Long

   Dim lX As Long

   Dim lY As Long

   Dim lHeight As Long

   Dim lWidth As Long

   'создаем пустой регион, с которого начнем работу

   lSkinRgn = CreateRectRgn(0, 0, 0, 0)

   With pic

      'подсчитаем размеры рисунка в Pixel

      lHeight = .Height / Screen.TwipsPerPixelY

      lWidth = .Width / Screen.TwipsPerPixelX

      For lX = 0 To lHeight - 1

         lY = 0

         Do While lY < lWidth

            'ищем нужный Pixel

            Do While lY < lWidth And GetPixel(.hDC, lY, lX) = lBackColor

               lY = lY + 1

            Loop

            If lY < lWidth Then

               lStart = lY

            Do While lY < lWidth And GetPixel(.hDC, lY, lX) <> lBackColor

               lY = lY + 1

            Loop

            If lY > lWidth Then lY = lWidth

               'нужный Pixel найден, добавим его в регион

               lRgn = CreateRectRgn(lStart, lX, lY, lX + 1)

               CombineRgn lSkinRgn, lSkinRgn, lRgn, RGN_OR

               DeleteObject lRgn

            End If

         Loop

      Next

   End With

   lGetRegion = lSkinRgn

End Function

Итак, для проверки на практике этого алгоритма загрузите пример pTestRgnSkin и внимательно изучите его код. В этом проекте нужный нам рисунок, для удобства, <зашит> в файле ресурсов, кроме того проект запускается процедурой Main, в которой и происходят все преобразования. Вначале загружается форма, затем в PictureBox из ресурсов загружается нужный нам рисунок, далее вызывается функция, которая создает регион и, наконец, завершающий этап - прикрепление региона к нужному нам окну. Для удобства здесь же вызывается функция, помещающая окно <поверх всех>, чтобы оно <не потерялось> у Вас на рабочем столе Windows. Кроме того, для нормальной работы программы необходимо, чтобы для PictureBox свойство AutoRedraw было установленно в True, иначе ничего не получится. 

Sub Main()

   Dim lRgn As Long

   Load frmTestRgnSkin

   frmTestRgnSkin.pic.Picture = LoadResPicture(101, vbResBitmap)

   lRgn = lGetRegion(frmTestRgnSkin.pic, vbWhite)

   SetWindowRgn frmTestRgnSkin.hWnd, lRgn, True

   DeleteObject lRgn

   frmTestRgnSkin.Show

   SetFormPosition frmTestRgnSkin.hWnd, True

End Sub

Теперь можно запускать проект... О, знакомое лицо, скажите Вы, это же <Скрепыш> из Microsoft Office. Да, похож, но не совсем, <Скрепыш> двигается, а этот нет. Что же нужно сделать, чтобы это окно динамически изменяло свою форму по рисунку, отображаемому в данный момент времени в PictureBox? 

Динамическое изменение формы окна

Существуют программы в которых необходимо динамически во время работы изменять форму окна (например анимированный персонаж из Microsoft Office). Все это не очень сложно реализовать, нужно в событие PictureBox.Change добавить следующий код: 



lRgn = lGetRegion(frmTestRgnSkin.pic, vbWhite)

SetWindowRgn frmTestRgnSkin.hWnd, lRgn, True

DeleteObject lRgn

SetFormPosition frmTestRgnSkin.hWnd, True

В принципе всё готово, осталось только добавить код для изменения картинки на форме, и <Скрепыш> оживёт. В нашем примере изменять рисунок будем в Timer циклически, т.е. анимация будет непрерывна, так проще. Итак, добавим на форму Timer и поместим <в него> небольшой код, отвечающий за изменения рисунка в PictureBox. Рисунков в файле ресурсов десять штук, поэтому I должно изменяться от 101 до 110. Код изменения выглядит так: 

Static i As Long

If i < 101 Then i = 101

If i > 110 Then i = 101

frmAnimateForm.pic.Picture = LoadResPicture(i, vbResBitmap)

i = i + 1

Готово, можно запускать проект, и если Вы счастливый обладатель Pentium III или Athlon, то Вам улыбнется удача, так как <Скрепыш> будет двигаться. Но если Ваш процессор Pentium II и ниже, то компьютер не сможет выполнять необходимые расчеты за нужное нам время, так как для плавной анимации необходимо (для нашего случая) показывать порядка 15 кадров в секунду, а точнее каждые 80 милисекунд по кадру и ещё оставлять время для других задач компьютера. Как мы видим наши алгоритмы явно не тянут для таких задач и предназначены для <работ> не требующих таких быстрых изменений формы окна, так как, например на Celeron 333 один кадр формируется около 100 милисекунд. Что же делать? 

Оптимизация алгоритма для быстрой анимации

Анализ работы алгоритма показывает, что наибольшие затраты времени приходятся на функцию GetPixel. Это происходит потому, что анализ картинки идет непосредственно на экране. Единственный путь увеличения быстродействия алгоритма, это перенос анализа в память компьютера и использование при этом Win 32 API. Такие алгоритмы существуют, но это тема отдельного разговора, скажу только, что для оптимизации работы алгоритм пишется отдельно для каждой глубины цвета и при применении такой схемы быстродействие увеличивается почти в четыре раза и позволяет делать практически любую анимацию.