22 August 2011

Окна «неправильной» формы

.NET
From Sandbox

Начало


Иногда возникает необходимость в окне «неправильной» (не прямоугольной) формы, будь то заставка(splash screen), или виджет рабочего стола.

image


В Windows API есть несколько функций при помощи, которых можно создавать «неправильные» окна, такие как: CreateEllipticRgn, CreateRectRgn, CreatePolygonRgn, CreateRoundRectRgn, CombineRgn и др., но при работе с этими функциями выявляется ряд недостатков, окна получаются угловатые, косые края имеют неприятные зубчики, невозможно сделать полноценную тень, да и написание кода создающего окно сложной формы порой требует немало усилий.

Начиная с Windows 2000, у окон появился расширенный стиль WS_EX_LAYERED, делающий окно многослойным, а так же было добавлено несколько API функций по работе с такими окнами, одна из которых UpdateLayeredWindow отвечающая за обновление положения, размера, формы, содержимого и прозрачности многослойного окна. Данная функция позволяет создать окно на основе изображения, в том числе PNG, с учетом альфа канала. Создание формы окна на основе заранее подготовленного изображения гораздо удобнее и легче чем работать с регионами, но и у этого метода есть свой недостаток. На многослойном окне невозможно, простым путем, отобразить какие либо компоненты, такие как кнопки, текстовые поля и др., это является следствием того, что операционная система берет на себя весь процесс перерисовки окна, и стандартное сообщение WM_PAINT окну более не отсылается. В большинстве своем, всякого рода заставки, виджеты и прочие украшательства не требуют наличие на себе каких либо дополнительных компонентов, или требуют их в минимальном кол-ве, и потому на недостаток можно закрыть глаза.

Пример


Далее я хотел бы привести небольшой наглядный пример использования многослойных окон. Так как всё сводится к вызову API функций, язык программирования может быть любой, но у меня на работе установлена Visual Studio, потому я буду писать на VB.NET. Описывать используемые API функции я не буду, я лучше дам ниже ссылки на описание с сайта MSDN, так как цель статьи показать их практическое применение.

  1. Для начала нужно нарисовать наше будущее окно в любимом графическом редакторе, я рисовал в Photoshop, обязательно неправильной формы и обязательно с прозрачностью, чтобы почувствовать все прелести многослойных окон, и сохранить его в формат PNG. У меня получился вот такой стикер:
    image
  2. Далее создаем новый проект в Visual Studio и добавляем наше изображение в ресурс под именем «Стикер», у единственной формы в проекте убираем все ненужные заголовки и границы.
  3. Необходимо определить 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
    

  4. Этот и весь последующий код пишем в класс нашей единственной формы в проекте. Для начала нужно описать несколько локальных переменных, они нам понадобятся в процессе.
        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
    

  5. Делаем окно многослойным. В .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
    

  6. Создание функции обновления многослойного окна на основе изображения и степени общей прозрачности (от 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
    

  7. При загрузке экземпляра формы, формируем изображение (пишем текст на стикере, просто статика) и далее передаем в функцию обновления.
        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
    


Ну, вот вроде бы и все, запускаем и смотрим что, у нас получилось (результат на первом изображении).

Ссылки

  1. Описания используемых структур и функций
    Point, Size , GetDC, ReleaseDC, DeleteDC, SelectObject, DeleteObject, UpdateLayeredWindow
  2. Исходник примера

Продолжение следует...

Tags:.NETVisual StudioVB.NETAPIWS_EX_LAYEREDUpdateLayeredWindowмногослойные окна
Hubs: .NET
+22
10.8k 63
Comments 35
Ads