|
Исходники
Статьи
Языки программирования
.NET Delphi Visual C++ Borland C++ Builder C/С++ и C# Базы Данных MySQL MSSQL Oracle PostgreSQL Interbase VisualFoxPro Веб-Мастеру PHP HTML Perl Java JavaScript Протоколы AJAX Технология Ajax Освоение Ajax Сети Беспроводные сети Локальные сети Сети хранения данных TCP/IP xDSL ATM Операционные системы Windows Linux Wap Книги и учебники
Скрипты
Магазин программиста
|
ВведениеПеред началом работы обратите внимание на небольшое предупреждение: построение собственного элемента управления "с нуля" и рисование собственных изображений рекомендуется выполнять только тогда, когда другие возможности исчерпаны. Этот процесс не очень сложен с технической точки зрения (хотя и не прост), однако при этом необходимо учитывать столько деталей, что, вероятно, в созданном элементе управления не будет хватать, по крайней мере, части возможностей проектирования и доступности элементов управления, поставляемых с Microsoft® Visual Studio® .NET. Начав работу с одним из предоставленных элементов управления, наследуя от него функциональные возможности и надстраивая над ним дополнительную функциональность, разработчик может получить почти все функциональные возможности элемента управления бесплатно. Однако кроме этого предупреждения следует также заметить, что строить собственные элементы управления и особенно рисовать собственный интерфейс с помощью GDI+ очень интересно, поэтому любому разработчику нужно это попробовать. На практике никакого специфичного типа элемента управления, который лучше всего строить "с нуля", не существует, за исключением того, что, как правило, обычно это достаточно необычный элемент управления, который невозможно построить на основе существующего элемента. Далее будет рассматриваться пример построения связанного с данными просмотра эскизов, в достаточной степени отличающегося от любого другого элемента управления Microsoft Windows® Form, который нужно создавать "с нуля". Примечание Если требуется подобный элемент управления, который отличается от элементов управления Windows Forms, поставляемых с Microsoft.NET, и сложен с точки зрения реализации, то перед началом работы над ним можно также обратиться к сторонним поставщикам элементов управления. В обзорной статье по разработке элементов управления упоминалось, что одной из причин успеха Microsoft Visual Basic® было большое количество доступных сторонних элементов управления. Это относится и к Visual Basic .NET. Первоначально этот элемент управления должен был отвечать следующим требованиям:
В результате с минимальными усилиями был создан визуальный браузер для домашней библиотеки компакт-дисков (см. рис. 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).
Примечания к рисунку: Рис. 2. Во время рисования собственного элемента управления перед началом кодирования определите размещение. Каждая из перечисленных ниже переменных может быть сконфигурирована с помощью открытого свойства элемента управления:
Помимо этого вид элемента управления может быть настроен с помощью свойств по умолчанию 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. Поэтому было решено поддерживать следующее навигационное поведение:
Обработка нажатий клавишКод, поддерживающий перемещение с помощью клавиатуры, довольно прост; в нем используютс, я небольшие математические расчеты для определения строки и столбца, выбранных в настоящий момент. Весь код инкапсулирован в процедуру 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, как описано в этой статье.
|
Форум Программиста
Новости
Обзоры
Магазин Программиста
Каталог ссылок
Поиск
Добавить файл
Обратная связь |