Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Windows Forms »

Обсудить на форуме Обсудить на форуме

Рисование собственных элементов управления с помощью GDI+
в этой статье подробно рассматривается разработка связанного с данными элемента управления, нарисованного пользователем с помощью GDI+, как один из ряда примеров разработки элементов управления Microsoft Windows, которые следует читать вместе с обзорной статьей на смежную тему.,

Резюме

В этой статье подробно рассматривается разработка связанного с данными элемента управления, нарисованного пользователем с помощью GDI+, как один из ряда примеров разработки элементов управления Microsoft Windows, которые следует читать вместе с обзорной статьей на смежную тему. (15 печатных страниц)

Исходный код WinFormControls.exe можно загрузить из MSDN Code Center.

Данная статья является последней статьей в серии из пяти статей по разработке элементов управления в Microsoft® .NET:

Содержание

  • Введение
  • Поддержка сложного связывания данных
  • Рисование изображений
  • Переопределение OnPaint
  • Обработка нажатий клавиш и щелчков мыши
  • Пример
  • Резюме

    Введение

    Перед началом работы обратите внимание на небольшое предупреждение: построение собственного элемента управления "с нуля" и рисование собственных изображений рекомендуется выполнять только тогда, когда другие возможности исчерпаны. Этот процесс не очень сложен с технической точки зрения (хотя и не прост), однако при этом необходимо учитывать столько деталей, что, вероятно, в созданном элементе управления не будет хватать, по крайней мере, части возможностей проектирования и доступности элементов управления, поставляемых с Microsoft® Visual Studio® .NET. Начав работу с одним из предоставленных элементов управления, наследуя от него функциональные возможности и надстраивая над ним дополнительную функциональность, разработчик может получить почти все функциональные возможности элемента управления бесплатно. Однако кроме этого предупреждения следует также заметить, что строить собственные элементы управления и особенно рисовать собственный интерфейс с помощью GDI+ очень интересно, поэтому любому разработчику нужно это попробовать.

    На практике никакого специфичного типа элемента управления, который лучше всего строить "с нуля", не существует, за исключением того, что, как правило, обычно это достаточно необычный элемент управления, который невозможно построить на основе существующего элемента. Далее будет рассматриваться пример построения связанного с данными просмотра эскизов, в достаточной степени отличающегося от любого другого элемента управления Microsoft Windows® Form, который нужно создавать "с нуля".

    Примечание Если требуется подобный элемент управления, который отличается от элементов управления Windows Forms, поставляемых с Microsoft.NET, и сложен с точки зрения реализации, то перед началом работы над ним можно также обратиться к сторонним поставщикам элементов управления. В обзорной статье по разработке элементов управления упоминалось, что одной из причин успеха Microsoft Visual Basic® было большое количество доступных сторонних элементов управления. Это относится и к Visual Basic .NET.

    Первоначально этот элемент управления должен был отвечать следующим требованиям:

    1. Отображение многоколоночного представления изображений, поддерживающего несколько страниц и позволяющего индивидуальному элементу получить фокус.
    2. Текст элемента, изображение (URL изображения) и соответствующее значение должны быть свойствами, связанными с данными.
    3. Пользователь элемента управления должен иметь возможность определить размер изображений, а элемент управления должен автоматически обрабатывать изменение размеров и расположение изображений.

    В результате с минимальными усилиями был создан визуальный браузер для домашней библиотеки компакт-дисков (см. рис. 1).

    Рис. 1. Пример использования связанного с данными просмотра эскизов

    Для этого элемента управления необходимо было построить собственный интерфейс с помощью GDI+ на основе ближайшего элемента управления Windows Form ListView. Этот элемент управления и некоторые другие, например, TextBox, ComboBox и ListBox, фактически являются общими элементами управления Windows, которые были просто обернуты кодом .NET для использования в приложениях Windows Forms. Эти элементы управления не позволяют полностью переопределить их процедуры рисования, поэтому возможностей настройки согласно определенным требованиям было недостаточно.

    Поддержка сложного связывания данных

    Даже при том, что новый элемент управления не мог быть основан на существующем классе, он должен быть построен для максимально возможного многократного использования. Поэтому было решено создать базовый класс, инкапсулирующий основную работу для элемента управления со сложным связыванием данных. В процессе работы этот класс был построен и использовался для первой версии этого элемента управления. Собственный элемент управления, построенный этим способом, работал действительно хорошо, и созданный базовый класс мог использоваться многократно при построении окна списка и некоторых других элементов управления на основе GDI+, что привело к значительному сокращению кода, необходимого для каждого элемента. После завершения выяснилось, что можно было сократить работу еще больше: если бы это было такой хорошей идеей, то разработчики Windows Forms догадались бы об этом сами, и было бы установлено, что элементы управления ListBox и ComboBox основаны на общем классе ListControl, который почти в точности совпадает с классом, который был создан. Созданная версия работала прекрасно, однако элементы управления были переписаны для использования этого класса, который будет также использоваться в этой статье.

    С помощью класса ListControl выполняется почти вся работа по связыванию данных, что значительно сокращает объем требуемого кода. В рассматриваемый элемент управления добавим новое свойство, чтобы определить поле, хранящее путь к изображению. Другие свойства DataSource, DataMember, SelectedIndex, DisplayMember и ValueMember уже предоставлены.

    Над данными в этом элементе управления по-прежнему необходимо поработать (например, над циклом прохождения через элементы в процедурах рисования), однако это упрощается с помощью класса ListControl, предоставляющего экземпляр класса CurrencyManager как Me.DataManager. Этот объект обеспечивает доступ к списку элементов (Me.DataManager.List), текущей позиции в списке (Me.DataManager.Position) и классу PropertyDescriptorCollection, позволяющему обращаться к любому полю элемента списка (Me.DataManager.GetItemProperties).

    Рисование изображений

    С помощью переопределения метода OnPaint базового класса можно принять прорисовку этого элемента управления на себя. Именно это делается в коде, который требуется, чтобы нарисовать страницу, заполненную эскизами изображений. Прежде началом рисования необходимо определить позицию каждого изображения (и связанный текст), информацию об используемом цвете и шрифте и количество одновременно видимых строк и столбцов.

    Определение настраиваемого размещения

    Чтобы этот элемент управления был полезным, расположение и установка размеров эскизов должны конфигурироваться. Поэтому при определении процедуры рисования использовались переменные (см. рис. 2).

    Примечания к рисунку:
    Image 1 - изображение 1

    Рис. 2. Во время рисования собственного элемента управления перед началом кодирования определите размещение.

    Каждая из перечисленных ниже переменных может быть сконфигурирована с помощью открытого свойства элемента управления:

    • HorizontalSpacing (x)
    • VerticalSpacing (y)
    • ImageHeight (h)
    • ImageWidth (w)

    Помимо этого вид элемента управления может быть настроен с помощью свойств по умолчанию ForeColor, BackColor и Font, на каждое из которых код графики ссылается соответствующим образом. Это выполняется вручную, поэтому во время работы с графикой необходимо ссылаться на эти стандартные свойства, чтобы элемент управления вел себя должным образом.

    Вычисление числа строк и столбцов на странице

    Поскольку все изображения могут не поместиться на одной странице, необходимо следить, какой элемент находится в настоящий момент в левом верхнем углу элемента управления, и рисовать элементы относительно данного элемента списка. Определение количества строк и столбцов на странице выполняется в событии изменения размеров элемента управления, поскольку эти значения необходимо рассчитывать каждый раз заново при увеличении или уменьшении размера элемента управления:

        Private Sub imageList_Resize(ByVal sender As Object, _
                ByVal e As System.EventArgs) Handles MyBase.Resize
            Dim new_rowsPerPage As Integer = (Me.Height - y) \ ((2 * y) + h)
            Dim new_colsPerPage As Integer = Me.Width \ ((2 * x) + w)
            If (new_rowsPerPage <> rowsPerPage) _
                OrElse (new_colsPerPage <> colsPerPage) Then
                rowsPerPage = new_rowsPerPage
                colsPerPage = new_colsPerPage
            End If
        End Sub
    

    Чтобы избежать ненужной работы и нежелательного мерцания, необходим минимальный объем перерисовки элемента управления, но в этом случае каждое изменение размера потребует перерисовки. Иногда, как и в рассматриваемом случае, невозможно избежать перерисовки из-за дизайна пользовательского интерфейса. Нарисуйте две стрелки (одну наверху и одну внизу элемента управления), которые указывают на элементы, находящиеся вне экрана. Чтобы после каждого изменения размеров выполнялась перерисовка, можно было бы добавить вызов Me.Invalidate в рамках этой процедуры изменения размеров, но Windows Forms предоставляет другой метод, использующий стили элемента управления. Добавляя вызовы метода SetStyle в конструкторе (метод New) данного элемента управления, можно управлять его рисованием и обновлением с помощью Windows Forms. В этом случае установка стиля ResizeRedraw вызовет обновление при любом изменении элемента управления. Тем не менее, установим также стиль DoubleBuffer, превосходно позволяющий устранять нежелательное мерцание из элемента управления, нарисованного пользователем:

    Public Sub New()
        Me.SetStyle(ControlStyles.DoubleBuffer, True)
        Me.SetStyle(ControlStyles.ResizeRedraw, True)
        Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
        Me.SetStyle(ControlStyles.UserPaint, True)
    End Sub
    

    Примечание Двойная буферизация - это метод работы с графикой, при котором все изображение пользовательского интерфейса рисуется сначала в буфер (например, объект Image в памяти), а затем - в окне как цельное изображение. Она позволяет значительно уменьшить нежелательное мерцание по сравнению с последовательным выполнением всех отдельных графических команд непосредственно в окне. Согласно документации по .NET Framework для опций ControlStyles необходимо установить UserPaint и AllPaintingInWmPaint, чтобы воспользоваться всеми преимуществами двойной буферизации.

    Переопределение OnPaint

    На практике работа с графикой выполняется путем переопределения OnPaint, которому в качестве параметра передается объект, включающий объект Graphics, который может использоваться для рисования на подложке элемента управления. В данной процедуре OnPaint выполняется цикл прохождения через элементы источника данных. При этом необходимо следить за позициями строк и столбцов и рисовать каждый элемент:

    Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
        Dim myList As IList
        Dim gr As Graphics = e.Graphics
        gr.FillRectangle(New SolidBrush(Me.BackColor), e.ClipRectangle)
        gr.InterpolationMode = scalingMode
        gr.SmoothingMode = SmoothingMode.Default
    
        ControlPaint.DrawBorder3D(gr, Me.DisplayRectangle, m_borderStyle)
    
        If Me.DataManager Is Nothing Then
            myList = Nothing
        Else
            myList = Me.DataManager.List
        End If
    
        If Not myList Is Nothing Then 'if there is any data
            Dim itemCount As Integer
            itemCount = myList.Count
    
            Dim itemsDisplayed As Integer 'current position in the list
            itemsDisplayed = currentTopLeftItem
    
            Dim i, j As Integer 'loop indexes
            Dim height, width As Integer
    
            For i = 0 To rowsPerPage - 1
                For j = 0 To colsPerPage - 1
                    If itemsDisplayed < itemCount Then
                        DrawOneItem(itemsDisplayed, i, j, gr)
                        itemsDisplayed += 1
                    End If
                Next
            Next
    
            'Прорисовка указателей "страница вниз/страница вверх"
            Dim webdingsFont As New Font("Webdings", 20, _
                FontStyle.Regular, GraphicsUnit.Pixel)
            Dim textBrush As New SolidBrush(Me.ForeColor)
    
            If itemsDisplayed < itemCount - 1 Then
                'Прорисовка стрелки вниз
                gr.DrawString("6", _
                webdingsFont, textBrush, 0, Me.Height - 24)
            End If
    
            If currentTopLeftItem > 0 Then
                'Прорисовка стрелки вверх
                gr.DrawString("5", webdingsFont, textBrush, 0, 0)
            End If
    
        End If
    End Sub
    

    Большая часть кода в этой процедуре рисования служит для определения позиции (строки и столбца) каждого элемента списка, и лишь незначительная часть кода фактически обрабатывает рисунок. Эту процедуру можно значительно очистить, переместив код рисования отдельного элемента в его собственную процедуру:

    Private Sub DrawOneItem(ByVal index As Integer, _
       ByVal row As Integer, _
       ByVal col As Integer, _
       ByVal gr As Graphics)
        Dim textFont As Font = Me.Font
        Dim textBrush As New SolidBrush(Me.ForeColor)
    
        Dim myStringFormat As StringFormat = New StringFormat()
        myStringFormat.Alignment = StringAlignment.Center
        myStringFormat.FormatFlags = StringFormatFlags.LineLimit
    
        Dim imageURL As String = GetListItemImage(index)
        If imageURL = "" Then imageURL = m_GenericImage
    
        If imageURL <> "" Then
            If IO.File.Exists(imageURL) Then
                Dim myNewImage As New Bitmap(imageURL)
                'Масштабирование изображения до необходимого определенного        
                'размера
                With myNewImage
                    If .Height > h Then
                        Height = h
                        Width = CInt((h / .Height) * .Width)
                    Else
                        Height = .Height
                        Width = .Width
                    End If
                    If Width > w Then
                        Height = CInt((w / Width) * Height)
                        Width = w
                    End If
                End With
    
                Dim imageRect _
                    As New Rectangle((2 * x) + _
                      (col * ((2 * x) + w)) + ((w - Width) \ 2), _
                       (1 * y) + (row * ((2 * y) + h)) _ 
                       + ((h - Height) \ 2), _
                       Width, Height)
                gr.DrawImage(myNewImage, imageRect)
                Dim myNewPen As Pen
                If index = Me.DataManager.Position Then  'selected
                    myNewPen = New Pen(Color.Yellow)
                    myNewPen.Width = 4
                Else
                    myNewPen = New Pen(Color.Black)
                    myNewPen.Width = 1
                End If
                gr.DrawRectangle(myNewPen, imageRect)
            End If
        End If
    
        Dim textHeight As Integer = y * 2
        gr.DrawString(Me.GetItemText(Me.DataManager.List.Item(index)), _
            textFont, textBrush, _
            New RectangleF((x) + (col * ((2 * x) + w)), _
                2 + (1 * y) + h + (row * ((2 * y) + h)), _
                w + (2 * x), textHeight), myStringFormat)
    End Sub
    

    Удалив эту процедуру из основного кода OnPaint, легче рассматривать фактически выполненную работу GDI+. Если путь для изображения ссылается на реальный файл, сначала создается новый объект Bitmap с помощью этого пути как конструктора, и затем само изображение рисуется на элемент управления при помощи метода DrawImage объекта Graphics. Перед фактическим рисованием изображения выполняются небольшие математические расчеты для пропорционального масштабирования изображения в его целевое пространство.

    Примечание Объект Graphics представляет поверхность рисунка, поэтому эти те же методы могут использоваться в некоторых других ситуациях, например, при создании собственных файлов изображения, таких как bitmap или jpeg.

    Следующим шагом в рисовании элемента будет рисование прямоугольника заданного цвета и с заданной толщиной линий вокруг изображения для указания фокуса. Граница рисуется после рисования изображения так, чтобы она была сверху и не закрывалась изображением. Затем под изображением рисуется текстовая строка с помощью метода DrawString. С помощью перегрузки метода DrawString, который принимает прямоугольник размещения текста, текст может быть автоматически перенесен должным образом в рамках указанной области. Методу DrawString передается также объект StringFormat, позволяющий сконфигурировать детали рисования текста, такие как использование переноса на новую строку. В этом примере объект StringFormat сконфигурирован с флажком LineLimit, запрещающим рисование частично отсеченного текста, поэтому появится только текст, полностью находящийся в прямоугольнике размещения текста.

    Обработка нажатий клавиш и щелчков мыши

    Разработчик должен самостоятельно обработать всю навигацию в рамках этого элемента управления, поскольку он не базируется на существующем элементе управления как ListView. Поэтому было решено поддерживать следующее навигационное поведение:

    • Клавиши с изображением стрелок служат для перемещения между изображениями. Перемещение за пределы нижней или верхней границы элемента управления будет аналогично нажатию клавиш PageUp или PageDown.
    • Клавиши PageUp или PageDown служат для перемещения на один экран информации.
    • Отдельный элемент выбирается щелчком мыши. Множественный выбор недоступен.
    • Двойной щелчок на изображении или выбор изображения нажатием клавиш Return или Enter генерирует специальное событие ItemPicked.
    • Перемещение за границы элемента управления (влево или вправо в любом случае; вверх или вниз при отсутствии элементов, доступных в этом направлении) генерирует другое пользовательское событие LeaveControl. Разработчики, использующие этот элемент управления в своих формах, смогут управлять навигацией из этого элемента управления к другим элементам управления в той же форме.

    Обработка нажатий клавиш

    Код, поддерживающий перемещение с помощью клавиатуры, довольно прост; в нем используютс, я небольшие математические расчеты для определения строки и столбца, выбранных в настоящий момент. Весь код инкапсулирован в процедуру KeyPressed (вызываемую из события KeyDown элемента управления):

    Private Sub imageList_KeyDown(ByVal sender As Object, _
            ByVal e As System.Windows.Forms.KeyEventArgs) _
            Handles MyBase.KeyDown
        KeyPressed(e.KeyCode)
    End Sub
    
    Private Sub KeyPressed(ByVal Key As System.Windows.Forms.Keys)
        Try
            Dim m_oldTopItem As Integer = currentTopLeftItem
            Dim m_oldSelectedItem As Integer = Me.DataManager.Position
    
            Dim newPosition As Integer = m_oldSelectedItem
            Dim selectedRow As Integer
            Dim selectedColumn As Integer
    
            selectedRow = System.Math.Floor( _
             (m_oldSelectedItem - currentTopLeftItem) / colsPerPage)
            selectedColumn = (m_oldSelectedItem - currentTopLeftItem) _
             Mod colsPerPage
    
            If Not Me.DataManager.List Is Nothing Then
                Select Case Key
                    Case Keys.Up
                        If newPosition >= colsPerPage Then
                         newPosition -= colsPerPage
                        End If
                    Case Keys.Down
                        If Me.DataManager.Count - colsPerPage > _
                            newPosition Then
                            newPosition += colsPerPage
                        End If
                    Case Keys.Left
                        If selectedColumn = 0 Then
                            RaiseEvent LeaveControl(Direction.Left)
                        Else
                            newPosition -= 1
                        End If
                    Case Keys.Right
                        If selectedColumn = (colsPerPage - 1) Then
                            RaiseEvent LeaveControl(Direction.Right)
                        Else
                            newPosition += 1
                        End If
                    Case Keys.PageDown
                        If newPosition < Me.DataManager.Count Then
                            newPosition += (rowsPerPage * colsPerPage)
                            If newPosition >= Me.DataManager.Count Then
                                newPosition = Me.DataManager.Count - 1
                            End If
                        Else
                            RaiseEvent LeaveControl(Direction.Down)
                        End If
                    Case Keys.PageUp
                        If newPosition > 0 Then
                            newPosition -= (rowsPerPage * colsPerPage)
                            If newPosition < 0 Then
                                newPosition = 0
                            End If
                        Else
                            RaiseEvent LeaveControl(Direction.Down)
                        End If
    
                    Case Keys.Enter, Keys.Return
                        RaiseEvent ItemChosen(newPosition)
                End Select
    
                If newPosition < 0 Then newPosition = 0
                If newPosition >= Me.DataManager.Count Then
                    newPosition = Me.DataManager.Count - 1
                End If
    
                If newPosition <> m_oldSelectedItem Then
                    Me.DataManager.Position = newPosition
                End If
            End If
        Catch except As Exception
            Debug.WriteLine(except)
        End Try
    End Sub
    

    Поддержка мыши

    Чтобы элемент управления "просмотр эскизов" хорошо работал с мышью, были созданы два обработчика событий: открытое событие (ItemChosen, также вызываемое из KeyPressed, когда пользователь нажимает Enter или Return), которое может быть сгенерировано, и сервисная функция, обрабатывающая нажатия:

    Private Function HitTest(ByVal loc As Point) As Integer
        Dim i As Integer
        Dim found As Boolean = False
        i = 0
        Do While i < Me.DataManager.Count And Not found
            If GetItemRect(i).Contains(loc) Then
                found = True
            Else
                i += 1
            End If
        Loop
        If found Then
            Return i
        Else
            Return -1
        End If
    End Function
    
    Private Sub dbThumbnailView_Click(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles MyBase.Click
        Dim mouseLoc As Point = Me.PointToClient(Me.MousePosition())
        Dim itemHit As Integer = HitTest(mouseLoc)
        If itemHit <> -1 Then
            Me.DataManager.Position = itemHit
        End If
    End Sub
    
    Private Sub dbThumbnailView_DoubleClick(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles MyBase.DoubleClick
        Dim mouseLoc As Point = Me.PointToClient(Me.MousePosition())
        Dim itemHit As Integer = HitTest(mouseLoc)
        If itemHit <> -1 Then
            RaiseEvent ItemChosen(itemHit)
        End If
    End Sub
    

    Обратите внимание, что событие Click не должно вызываться явно изнутри обработчика событий dbThumbnailView_Click. Элемент управления автоматически вызовет стандартное событие Click, которое может быть обработано пользователем элемента управления. Кроме того, событие Double-click будет сгенерировано для каждого двойного щелчка, однако событие ItemChosen произойдет, только если дважды щелкнуть на выбранном элементе.

    Определение события по умолчанию

    Рассматривая события, необходимо упомянуть один из тех "последних штрихов", облегчающих использование элемента управления. Если дважды щелкнуть по элементу управления, находясь в режиме его проектирования (в интегрированной среде разработки Visual Studio .NET), появится обработчик событий для одного из событий объекта. Эта возможность интегрированной среды разработки очень полезна, однако она хорошо работает только тогда, когда появляется наиболее общий обработчик событий. Добавив атрибут DefaultEvent к классу элемента управления, можно определить событие, выбираемое интегрированной средой разработки, если в режиме проектирования дважды щелкнуть на элементе управления:

    <DefaultEvent("ItemChosen")> _
    Public Class dbThumbnailView
        Inherits ListControl
    

    Без атрибута DefaultEvent интегрированная среда разработки будет использовать любой атрибут DefaultEvent, который определен базовым классом или другими классами далее по цепочке наследования. В случае с данным элементом управления "просмотр эскизов" событие Click выбирается по умолчанию как событие по умолчанию класса Control, и данная база (ListControl) наследуется из Control.

    Некоторые проблемы и примечания

    Первоначальный элемент управления не проектировался для использования клавиатуры или мыши, поэтому полоса прокрутки отсутствует. Там было достаточно только визуальных индикаторов (стрелки в левых нижнем и верхнем углах), однако в среде с использованием мыши можно добавить полосу прокрутки. Также в первой версии этого элемента управления не использовалась граница, и не поддерживалась мышь, но обе эти возможности были добавлены к версии, рассматриваемой в этой статье.

    Пример

    Этот элемент управления демонстрируется как часть того же примера (связанный с данными TreeView) и используется для отображения всех книг определенного автора или издателя (см. рис. 3). Код для этого типового приложения включен в загрузку для этой статьи.

    Рис. 3. Это типовое приложение демонстрирует элемент управления "просмотр эскизов" и связанный с данными элемент управления Tree из примера 3.

    Резюме

    Иногда необходимо строить довольно специфичные элементы управления. Разработку собственного элемента управления нужно начинать "с нуля", включая написание кода для собственной графики. Если создается сложный связанный с данными элемент управления, такой как Grid, или некая форма ListBox, можно значительно ускорить разработку, базируя элемент управления на классе ListControl, как описано в этой статье.


  • Может пригодится:


    Автор: Дункан Маккензи
    Прочитано: 6165
    Рейтинг:
    Оценить: 1 2 3 4 5

    Комментарии: (0)

    Добавить комментарий
    Ваше имя*:
    Ваш email:
    URL Вашего сайта:
    Ваш комментарий*:
    Код безопастности*:

    Рассылка новостей
    Рейтинги
    © 2007, Программирование Исходники.Ру