Пользователь
0,0
рейтинг
22 августа 2011 в 14:58

Разработка → Окна «неправильной» формы из песочницы

.NET*

Начало


Иногда возникает необходимость в окне «неправильной» (не прямоугольной) формы, будь то заставка(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. Исходник примера

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

Илья Наумов @Joo
карма
3,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (35)

  • 0
    • +1
      Ой, этот комментарий был адресован вам.
  • +1
    Это не имеет какого либо отношения к многослойным окнам, и к тому же не позволит достичь того же результата, как в примере.
  • +2
    С кошкой хоть всё в порядке?
    • +1
      Да, с кошкой всё в порядке, спасибо )
  • +14
    Боже, VB .NET, неужели его кто-то использует? Неудобно же писать код, просто борода!
    Последний раз вживую работал с VB .NET года 3 назад при работе с DotNetNuke.
    Так себе опыт :)
    • +5
      Неудобно — это когда соседские дети на тебя похожи… А если серьезно, то писать вполне себе можно, можно было пример и на C# или даже С++ написать, но на работе только экспресс с бэйсиком.
  • +9
    А можно взять WPF и выставить там пару параметров.
    • +2
      Ну, WPF пока все же довольно тормозной, хотя на нем, конечно, такие вещи реализуются в разы проще…
      • +1
        Ну не скажите, WPF очень шустро отрисовывает даже сложные GUI.
        • 0
          Да ни разу не шустро. Запустил вот недавно чисто ради любопытства небольшую программку, к которой был применен порт темы Cosmopolitan c сервелата. Ничего сверхъестественного — таб контрол, тексбоксы, несколько кнопок — все контролы стилизованы, присутствует легкая анимация кое-где. Запустил на нетбуке с атомом N450. И были явные тормоза. Возможно, если ограничить FPS руками, будет быстрее, но все же, тормоза заметны. На среднем компьютере, возможно, будет менее заметно, но все же будет заметно.
          WPF приложения требуют всяческих оптимизаций, чтобы работать сколь-либо пристойно. Работы тут для МС — непочатый край.
          Я уж не говорю о времени старта приложения. Холодный старт просто очень долог, неприлично долог. Горячий тоже не особо, чтобы уж быстр.
          WPF, как ни крути, в текущей редакции не годится для повседневных приложений, только если LOB…
          • +4
            У нас есть проект — приложение для финансового трейдинга, заказчик крупный международный банк. Требования к скорости работы приложения и отзывчивости интерфейса очень серьезные. Интерфейс приложения полностью сделан на WPF (.NET 4.0). Возможно ваши доводы основаны на опыте использования WPF более ранних версий, либо любительские приложения. В 4й версии майкрософт достаточно серьезно поработала над оптимизацией WPF.
            • 0
              Должно быть вы не используете анимацию. Без нее приложения работают относительно сносно. Но опять же, старт медленный и при таких обстоятельствах.
              • 0
                Делайте предкомпиляцию при установке, помогает для первого запуска.
              • 0
                Анимации используются но, сугубо стандартного характера (подсветить кнопку при наведении, или, например, анимированный прогресс бар в заголовке таба)
                • 0
                  Вот попробуйте открыть демо темы Cosmopolitan. Обычная тема для Silverlight. Только у меня браузер виснуть начинает и все тормозит через минуту после загрузки этого приложения. И это на Core i5 460, видео интел. На WPF ситуация лучше, но далеко не идеально.
              • 0
                Анимация, выходящая за рамки стандартного поведения стандартных элементов управления или идентичных им, в приложениях для корпоративного сектора как минимум неуместна. А избыточная анимация, которой так пестрят всевозможные демо WPF, в обычной жизни и вовсе будет дико раздражать.
                • 0
                  Стандартные элементы управления — понятие достаточно расплывчатое. Если корпоративное приложение строго выдержано в едином стиле — стандартность элементов управления не имеет особого значения. Наличие анимаций — тоже не показатель низкого качества интерфейса. Все зависит от качества и уместности анимации в данном конкретном месте. Короче говоря, ваше суждение несколько шаблонно.
                  • 0
                    Да. Согласен. Главное — идентичность интерфейса во всем приложении. Но иногда нужно чтобы этот интерфейс не отличался от системного интерфейса самой ОС. Например когда компания разрабатывает собственную утилиту конфигурирования наподобие MMC. Я сам придерживаюсь следующего правила — если приложение можно отнести к «системным», то я стараюсь разрабатывать его, не применяя нестандартных элементов управления и схем оформления. А если какая-то задача не может быть решена при помощи стандартного элемента управления и мне нужно разработать какой-то специфический элемент управления — стараюсь комбинировать стандартные элементы управления для решения задачи. Например TreeListView (дерево с колонками) я делал через переопределение шаблона TreeView с использованием GridViewRowHeaderPresenter c переопределением схемы оформления для GridViewColumnHeader.
          • +2
            WPF использует DirectX для аппаратного ускорения отрисовки GUI. Если видеокарта — встроенная от Intel, то отзывчивости от интерфейса ожидать не стоит :(
            • 0
              В насыщенном UI, да еще и спроектированном неоптимально (лишние полупрозрачные градиенты и прочие ненадобности), очень плохо ускоряются. Особенно это относится к аппликейшенам типа упомянутого выше. Большое количество геометрии в темплейтах, анимации в пять десять слоев + куча биндингов, и в результате неимоверные тормоза. А если еще юзается либа типа Visifire, в которой для каждого вшивого элемента, для каждого датапоинта отдельный канвас используется, так вообще жесть. Трехмерку WPF пускает через DX, а вот двумерка не особо-то и ускорена (на личном опыте убедился — все печально).
          • +2
            Visual Studio 2010 на WPF написана, и ничего, хорошо работает.
            • 0
              Написан только UI. В фоне все тот же шелл. И работает через одно место, данное нам природой как раз для таких случаев. Ну мало-мальски сложных солюшенах и файловая система захлебывается от ее бэкэнда и UI тормозит неимоверно, когда на XAML не приведи Господь случайно наткнешься. Не дай Бог оставить включенным сплит-режим (разметка + дизайнер). 30 сек. (и более) открытие не слишком громоздкой разметки на Core i3 — это жесть. Почти минуту захлопывать сразу все вкладки (Close All через контекстное меню)… И тэдэ и тэпэ.
              Я обожаю эту платформу, но тормоза выносят мозг…
              • 0
                Вы ReSharper не юзаете?

                VS2010 стоит. Работал как с сервелатом так и с WPF. Никаких проблем с производительностью не замечал.
          • +3
            В WPF очень просто сделать ошибку по неосторожности, он гораздо сложнее, чем кажется. Писать под WPF надо крайне аккуратно. Нужно понимать, как будет обрабатываться лейаут, каков будет размер визуального дерева, как будут роутиться события, как будет работать виртуализация GUI, каков будет объём динамических и статических ресурсов.
            По неосторожности можно запросто в разы увеличить объём потребляемой приложением памяти, например, если случайно разместить какие-нибудь тяжелые объекты в словаре ресурсов часто используемого элемента GUI (FrameworkElement.Resources), вместо словаря ресурсов сборки.
            Виртуализация GUI отдельная сложная тема, позволяющая круто повысить производительность, за счет создания визуальных элементов на лету, по мере попадания их предполагаемого региона размещения в видимую область окна.
            А еще есть кеш композиции, а еще… ну в общем вы поняли.
            Иными словами — WPF в сегодняшнем виде — платформа для профессиональных разработчиков. Без большого объема знаний получить хорошую производительность при сложном GUI — сложная задача. Однако, если его освоить, то преимущества становятся очевидными. При желании, по производительности можно запросто переплюнуть Windows Forms. Но и недостатков, конечно, хватает.
            • 0
              Да, с этим совершенно согласен. Microsoft надо оптимизировать это так, чтобы высокая производительность была доступна без танцев с бубном. Большинство начинающих программистов таких тонкостей не знают, потому большинство приложений выйдет медленными. Даже разработчики Evernote, далеко наверно не бездари, не смогли добиться достаточной производительности. Разработчики Lenovo с их набором софта для Think Pad. Intel с панелью управления графическим адаптером и т.д. и т.п. Куда уж тогда менее опытным программистам.
            • +1
              > Виртуализация GUI отдельная сложная тема, позволяющая круто повысить производительность
              … или огрести багов по скроллингу. WPF Toolkit + Virtualization + ScrollBar (особенно навешенный в темплейте + участие биндингов) == Куча Трудноустранимых багов.

              > Microsoft надо оптимизировать это…
              Причем тут M$? Учим матчасть… все есть — просто надо знать лучшие практики, а не видики с индусами зырить и не говнокод копипастить.
              • 0
                >… или огрести багов по скроллингу.
                А не надо лезть напрямую в визуальное дерево из кода модели. Опасность виртуализации в том, что нельзя быть уверенным в том, что для элемента модели на какой-то момент времени существует визуальный контейнер. Этот факт по неопытности может принести много неприятностей. Самый простой пример — есть иерархическая объектная модель, которая визуально представляется в виде дерева. Задача — развернуть дерево так, что бы был виден определенный узел. Если попытаться влезть в визуальное дерево контрола дерева, то получим жесткий облом, т.к. ItemsControl.GetContainerForItem может ничего не вернуть! Выход из подобных ситуаций — спуск нужной логики во ViewModel. Для дерева в объект модели, соответствующий узлу добавляем свойство IsExpanded, делаем к нему Binding из шаблона узла, и всё! Развернутостью узлов можно управлять при обращении к модели. На то в общем то ViewModel и отделен от Model. Речь, само собой, про паттерн MVVM, самый популярный для WPF, в той или иной форме.
                На счет скроллинга — ScrollBar должен корректно работать если его целевая панель корректно реализует IScrollInfo. Если что-то работает не так, то это вина разработчика класса панели.
                WPF Toolkit вообще штука сырая, полно багов. Долго плевался от тамошнего DataGrid'а. После доработки напильником и пары хаков через рефлексию пашет более-менее сносно.
                К слову, лично засабмитил несколько багов по WPF на Microsoft Connect. Году эдак в 2009м. Так они в активе и висят… Так что проще по ходу делать хаки, благо, что исходники есть.
                • 0
                  Это все понятно… для виртуализованного элемента вне вьюпорта нет контейнера. Я о другом писал. Возникают проблемы с контекстным меню на скроллбаре, который определен в шаблоне контрола. Вот навешен в шаблоне на скроллвьюер внешний скроллер и получает он (скроллер) неправильный размер ползунка (например, меньше, чем должно быть), а в результате команда «Bottom» в контекстном меню двигает нутро скроллвьюера слишком далеко вверх. Пол дерева в TreeView как не бывало.
                  • 0
                    Ничего не понял, что у вас, ScrollBar, ScrollViewer, или какой-то свой скроллер. И при чем тут размер ползунка? У ScrollViewer'а есть вьюпорт, его размеры и положение. Работать с прокруткой через размер ползунка у ScrollBar'а относительно его общего размера, это мягко говоря, не корректно. Но как-то неправильно у вас команда реализована по моему. Она что ли явно устанавливает свойство ScrollViewer.VerticalOffset? Если нужно что-то прокрутить, то надо либо юзать методы ScrollViewer'а, а у него есть метод ScrollToBottom, либо нужно обращаться к IScrollInfo панели, которая хостит элементы.
                    • 0
                      Есть шаблон для WPF Toolkit DataGrid, в котором определены вьюпорт и скроллбар. Положение содержимого скроллвьюера завязано на скроллбар, у которого имеется свое собственное (нативное) контекстное меню. В этом контекстном меню есть команды, перемещающие ползунок.
                      У DataGrid включена виртуализация, т.е. контейнеры созданы только для видимых итемов. Теперь вопрос: как скроллвьюер определяет суммарную высоту своего контента, если не все элементы DataGrid реально отрисованы и как определить точные параметры для скроллбара?
                      Надо будет на досуге покопаться, когда время на это будет.
        • 0
          У WPF все еще холодный старт порядка 4 секунд, и текст все еще не отрисовывается так же, как на WPF (непонятно зачем Direct2D/DirectWrite делали).
    • +1
      Да, на WPF это реализуется проще, согласен, но не все любят и хотят работать с WPF. Да, и описаный способ можно использовать в нативных приложениях, а WPF это только .NET.
      • 0
        Вот я тоже начал читать и сразу мысль возникла: а причем тут, собственно, .Net? Статья об использовании WinAPI. Хотел даже минуснуть статью, т.к. описание не очень хорошей практики скрытия оконно обвязки и рисование «фердипердозных» окошек в WPF — тема избитая, или даже нубская, вобщем на мойвзгляд не для хабра.
  • +5
    Судя по фото топика, жена автора уже задолбала

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.