Начало
Иногда возникает необходимость в окне «неправильной» (не прямоугольной) формы, будь то заставка(splash screen), или виджет рабочего стола.
В Windows API есть несколько функций при помощи, которых можно создавать «неправильные» окна, такие как: CreateEllipticRgn, CreateRectRgn, CreatePolygonRgn, CreateRoundRectRgn, CombineRgn и др., но при работе с этими функциями выявляется ряд недостатков, окна получаются угловатые, косые края имеют неприятные зубчики, невозможно сделать полноценную тень, да и написание кода создающего окно сложной формы порой требует немало усилий.
Начиная с Windows 2000, у окон появился расширенный стиль WS_EX_LAYERED, делающий окно многослойным, а так же было добавлено несколько API функций по работе с такими окнами, одна из которых UpdateLayeredWindow отвечающая за обновление положения, размера, формы, содержимого и прозрачности многослойного окна. Данная функция позволяет создать окно на основе изображения, в том числе PNG, с учетом альфа канала. Создание формы окна на основе заранее подготовленного изображения гораздо удобнее и легче чем работать с регионами, но и у этого метода есть свой недостаток. На многослойном окне невозможно, простым путем, отобразить какие либо компоненты, такие как кнопки, текстовые поля и др., это является следствием того, что операционная система берет на себя весь процесс перерисовки окна, и стандартное сообщение WM_PAINT окну более не отсылается. В большинстве своем, всякого рода заставки, виджеты и прочие украшательства не требуют наличие на себе каких либо дополнительных компонентов, или требуют их в минимальном кол-ве, и потому на недостаток можно закрыть глаза.
Пример
Далее я хотел бы привести небольшой наглядный пример использования многослойных окон. Так как всё сводится к вызову API функций, язык программирования может быть любой, но у меня на работе установлена Visual Studio, потому я буду писать на VB.NET. Описывать используемые API функции я не буду, я лучше дам ниже ссылки на описание с сайта MSDN, так как цель статьи показать их практическое применение.
- Для начала нужно нарисовать наше будущее окно в любимом графическом редакторе, я рисовал в Photoshop, обязательно неправильной формы и обязательно с прозрачностью, чтобы почувствовать все прелести многослойных окон, и сохранить его в формат PNG. У меня получился вот такой стикер:
- Далее создаем новый проект в Visual Studio и добавляем наше изображение в ресурс под именем «Стикер», у единственной формы в проекте убираем все ненужные заголовки и границы.
- Необходимо определить API функции и структуры, которые понадобятся в процессе. Я обычно это делаю в отдельном классе.
Namespace System Public Class Win32API Public Const WS_EX_LAYERED = &H80000 Public Const ULW_ALPHA As Int32 = &H2 Public Const AC_SRC_OVER As Byte = &H0 Public Const AC_SRC_ALPHA As Byte = &H1 'Точка (координата)' <StructLayout(LayoutKind.Sequential)> _ Public Structure Point Public x As Int32 Public y As Int32 Public Sub New(ByVal x As Int32, ByVal y As Int32) Me.x = x Me.y = y End Sub End Structure 'Размер' <StructLayout(LayoutKind.Sequential)> _ Public Structure Size Public cx As Int32 Public cy As Int32 Public Sub New(ByVal cx As Int32, ByVal cy As Int32) Me.cx = cx Me.cy = cy End Sub End Structure ' Определяет режим вывода полупрозрачных изображений' <StructLayout(LayoutKind.Sequential, Pack:=1)> _ Public Structure BLENDFUNCTION Public BlendOp As Byte Public BlendFlags As Byte Public SourceConstantAlpha As Byte Public AlphaFormat As Byte Public Sub New(ByVal BledOp As Byte, ByVal BlendFlags As Byte, ByVal SourceContrastAlpha As Byte, ByVal AlphaFormat As Byte) Me.BlendOp = BledOp Me.BlendFlags = BlendFlags Me.SourceConstantAlpha = SourceContrastAlpha Me.AlphaFormat = AlphaFormat End Sub End Structure ' Получает дескриптор контекста дисплея для клиентской области указанного окна' <DllImport("user32.dll")> _ Public Shared Function GetDC(ByVal hWnd As IntPtr) As IntPtr End Function 'Создает совместимый контекст с заданным устройством' <DllImport("gdi32.dll")> _ Public Shared Function CreateCompatibleDC(ByVal hDC As IntPtr) As IntPtr End Function 'Освобождает контекст' <DllImport("user32.dll", ExactSpelling:=True)> _ Public Shared Function ReleaseDC(ByVal hWnd As IntPtr, ByVal hDC As IntPtr) As Integer End Function 'Удаляет контекст' <DllImport("gdi32.dll")> _ Public Shared Function DeleteDC(ByVal hdc As IntPtr) As Boolean End Function 'Выберает объект в заданный контекст' <DllImport("gdi32.dll", ExactSpelling:=True)> _ Public Shared Function SelectObject(ByVal hDC As IntPtr, ByVal hObject As IntPtr) As IntPtr End Function 'Удаляет объект' <DllImport("gdi32.dll")> _ Public Shared Function DeleteObject(ByVal hObject As IntPtr) As Boolean End Function 'Обновляет многослойное окно' <DllImport("user32.dll")> _ Public Shared Function UpdateLayeredWindow(ByVal hwnd As IntPtr, ByVal hdcDst As IntPtr, ByRef pptDst As Win32API.Point, ByRef psize As Win32API.Size, ByVal hdcSrc As IntPtr, ByRef pprSrc As Win32API.Point, ByVal crKey As Int32, ByRef pblend As Win32API.BLENDFUNCTION, ByVal dwFlags As Int32) As Boolean End Function End Class End Namespace
- Этот и весь последующий код пишем в класс нашей единственной формы в проекте. Для начала нужно описать несколько локальных переменных, они нам понадобятся в процессе.
Private _ScreenDC As IntPtr = IntPtr.Zero Private _MemDC As IntPtr = IntPtr.Zero Private _BitmapHandle As IntPtr = IntPtr.Zero Private _OldBitmapHandle As IntPtr = IntPtr.Zero Private _Size As Win32API.Size = Nothing Private _PoinSource As Win32API.Point = Nothing Private _TopPos As Win32API.Point = Nothing Private _Blend As Win32API.BLENDFUNCTION = Nothing Private _Opacity As Byte = 255 Private bmpDest As Bitmap = Nothing Private bmpSrc As Bitmap = Nothing
- Делаем окно многослойным. В .NET можно назначить некоторые свойства окна непосредственно перед его созданием (вызов API функции CreateWindowEx), переопределив свойство класса CreateParams.
Protected Overrides ReadOnly Property CreateParams() As System.Windows.Forms.CreateParams Get Dim CP As CreateParams = MyBase.CreateParams CP.ExStyle = CP.ExStyle Or Win32API.WS_EX_LAYERED Return CP End Get End Property
- Создание функции обновления многослойного окна на основе изображения и степени общей прозрачности (от 0 до 255).
Public Sub SetImage(ByRef Bitmap As Bitmap, ByVal Opacity As Byte) 'Получим дескриптор на контекст дисплея' _ScreenDC = Win32API.GetDC(0) 'Создадим контекст совместимый с дисплеем' _MemDC = Win32API.CreateCompatibleDC(_ScreenDC) 'Хендл изображения' _BitmapHandle = IntPtr.Zero 'Хендл старого изображения' _OldBitmapHandle = IntPtr.Zero Try 'Получим хендл изображения' _BitmapHandle = Bitmap.GetHbitmap(Color.FromArgb(0)) 'Сохраним хендл изображения на случай ошибки' _OldBitmapHandle = Win32API.SelectObject(_MemDC, _BitmapHandle) 'Укажем размеры окна, в нашем случае равны размеру изображения' _Size = New Win32API.Size(Bitmap.Width, Bitmap.Height) _PoinSource = New Win32API.Point(0, 0) _TopPos = New Win32API.Point(Me.Left, Me.Top) 'Заполним структуру BLENDFUNCTION' _Blend = New Win32API.BLENDFUNCTION(Win32API.AC_SRC_OVER, 0, Opacity, Win32API.AC_SRC_ALPHA) 'Обновляем многослойное окно' Win32API.UpdateLayeredWindow(Me.Handle, _ScreenDC, _TopPos, _Size, _MemDC, _PoinSource, 0, _Blend, Win32API.ULW_ALPHA) Finally Win32API.ReleaseDC(IntPtr.Zero, _ScreenDC) If _BitmapHandle <> IntPtr.Zero Then Win32API.SelectObject(_MemDC, _OldBitmapHandle) Win32API.DeleteObject(_BitmapHandle) End If Win32API.DeleteDC(_MemDC) _Size = Nothing _PoinSource = Nothing _TopPos = Nothing _Blend = Nothing End Try End Sub
- При загрузке экземпляра формы, формируем изображение (пишем текст на стикере, просто статика) и далее передаем в функцию обновления.
Private Sub FormSticker_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load bmpSrc = My.Resources.Стикер bmpDest = New Bitmap(bmpSrc.Width, bmpSrc.Height) Using g As Graphics = Graphics.FromImage(bmpDest) With g .InterpolationMode = Drawing2D.InterpolationMode.NearestNeighbor .SmoothingMode = Drawing2D.SmoothingMode.AntiAlias .TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias 'Рисуем стикер' .DrawImage(bmpSrc, 0, 0, bmpSrc.Width, bmpSrc.Height) 'Пишем текст' Dim sf As New StringFormat(StringFormatFlags.LineLimit) sf.Alignment = StringAlignment.Center sf.LineAlignment = StringAlignment.Center .DrawString("Сходить за молочком и забрать кошку от ветеринара", Me.Font, New SolidBrush(Me.ForeColor), New Rectangle(10, 10, bmpDest.Width - 20, bmpDest.Height - 20), sf) End With End Using Me.SetImage(bmpDest, Me._Opacity) bmpDest.Dispose() End Sub
Ну, вот вроде бы и все, запускаем и смотрим что, у нас получилось (результат на первом изображении).
Ссылки
- Описания используемых структур и функций
Point, Size , GetDC, ReleaseDC, DeleteDC, SelectObject, DeleteObject, UpdateLayeredWindow - Исходник примера