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

Главная » » .NET - Все статьи »

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

Расширение элемента управления TreeView
в статье рассматривается добавление функциональных возможностей привязки данных к элементу управления TreeView как один из ряда примеров разработки элементов управления Microsoft Windows, которые следует читать вместе с обзорной статьей на смежную тему.

Резюме

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

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

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

  • Разработка пользовательских элементов управления Windows с помощью Visual Basic .NET (обзор)
  • Добавление проверки правильности регулярного выражения
  • Объединение нескольких элементов управления в один
  • Расширение элемента управления TreeView
  • Рисование собственных элементов управления с помощью GDI+

Содержаниее

По возможности работу всегда необходимо начинать с существующего элемента управления. Во все предоставленные элементы управления Microsoft® Windows® Forms были вложены огромные усилия по кодированию и тестированию, и всю эту работу нужно выполнять заново, если построение элемента управления необходимо начинать полностью "с нуля". С учетом этого в данном примере будет выполняться наследование из существующего элемента управления Windows Form - Tree View - и его настройка. Код этого нового элемента управления TreeView можно загрузить одним файлом наряду с прочими примерами разработки элемента управления и типовым приложением, демонстрирующим, как этот расширенный TreeView может использоваться совместно с другими связанными с данными элементами управления.

Проектирование связанного с данными Tree View

Тема добавления привязки данных к элементу управления TreeView периодически затрагивалась разработчиками Windows, однако базовый элемент управления по-прежнему не поддерживает эту возможность из-за ключевого различия между TreeView и другими элементами управления, такими как ListBox или DataGrid: TreeView отображает иерархические данные. Отдельную таблицу данных довольно несложно отобразить в ListBox или DataGrid, однако использовать преимущества иерархического характера TreeView для отображения тех же данных не так просто. Существует много различных способов использования TreeView для отображения данных, но один способ является наиболее общим: группировка данных из таблицы по определенным полям, изображенная на рис. 1.

Рис. 1. Отображение данных в TreeView

Для этого примера предполагалось создать элемент управления TreeView, в который можно передать плоский набор данных (см. рис. 2) и легко достигнуть результата, изображенного на рис. 1.

Рис. 2. Плоский набор результатов, содержащий всю информацию, необходимую для создания дерева, изображенного на рис. 1.

Перед началом кодирования был разработан дизайн нового элемента управления, при котором можно обрабатывать этот специфический набор данных и, возможно, много других подобных ситуаций. Добавление коллекции "Группы", в которой поле группировки, поле отображения и поле значения (любые из них могут быть тем же полем) определены для каждого уровня иерархии, должно быть достаточно универсальным для создания иерархий из большинства плоских данных. Чтобы преобразовать данные (см. рис. 2) в TreeView (см. рис. 1), для нового элемента управления требуется указать два уровня группировки - Publisher (издатель) и Title (название), определяя pub_id как поле группировки для группы Publisher и title_id - для группы Title. Кроме поля группировки для каждой группы также необходимо указать поле отображения и поле значения, чтобы определить текст, показываемый на соответствующем узле группы, и значение, используемое для однозначной идентификации определенной группы. В случае с этими данными pub_name/pub_id и title/title_id будут использоваться как поля отображения/значения для этих двух групп. Имя автора будет конечным узлом дерева (узлом в конце иерархии группировки), и для этих узлов также необходимо определить поля идентификатора (au_id) и отображения (au_lname).

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

        With DbTreeControl
            .ValueMember = "au_id"
            .DisplayMember = "au_lname"

            .DataSource = myDataTable.DefaultView
            .AddGroup("Publisher", "pub_id", "pub_name", "pub_id")
            .AddGroup("Title", "title_id", "title", "title_id")
        End With

Примечание. Этот код - не совсем то, что требовалось, но он не слишком далек от желаемого результата. При разработке элемента управления стало очевидно, что необходимо связать индекс изображения из связанного ImageList элемента управления TreeView с каждым уровнем группировки. Поэтому был добавлен дополнительный параметр для метода AddGroup.

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

Примечания к рисунку:
Group Nodes - узлы групп
Leaf Nodes - конечные узлы

Рис. 3. Узлы групп и конечные узлы

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

Реализация связывания данных

Первым шагом в написании кода для этого элемента управления нужно создать проект и соответствующий стартовый класс. В данном случае сначала создадим новый проект Windows Control Library и затем, удалив класс UserControl по умолчанию, заменим его новым классом, который наследуется из элемента управления TreeView:

Public Class dbTreeControl
    Inherits System.Windows.Forms.TreeView

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

Добавление свойства DataSource

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

Создание процедуры свойства

Для начала работы необходимо, чтобы любой элемент управления, реализующий сложное связывание данных, реализовал процедуру DataSource и поддерживал соответствующие переменные члены:

Private WithEvents cm As CurrencyManager
Private m_DataSource As Object

<Category("Data")> _
Public Property DataSource() As Object
    Get
        Return m_DataSource
    End Get
    Set(ByVal Value As Object)
        If Value Is Nothing Then
            cm = Nothing
            GroupingChanged()
        Else
            If Not (TypeOf Value Is IList Or _
                      TypeOf Value Is IListSource) Then
                'используемый источник данных недопустим.
                Throw New System.Exception("Invalid DataSource")
            Else
                If TypeOf Value Is IListSource Then
                    Dim myListSource As IListSource
                    myListSource = CType(Value, IListSource)
                    If myListSource.ContainsListCollection = True Then
                        Throw New System.Exception("Invalid DataSource")
                    Else
                        'а теперь источник данных допустим.
                        m_DataSource = Value
                        cm = CType(Me.BindingContext(Value), _
                            CurrencyManager)
                        GroupingChanged()
                    End If
                Else
                    m_DataSource = Value
                    cm = CType(Me.BindingContext(Value), _
                        CurrencyManager)
                    GroupingChanged()
                End If
            End If
        End If
    End Set
End Property

Интерфейс IList

Объекты, которые могут использоваться в качестве источника данных для сложного связывания данных, как правило, поддерживают интерфейс IList, предоставляющий данные в виде коллекции объектов, а также несколько полезных свойств, например, Count. Новому элементу управления TreeView требуется объект, который поддерживает IList для его связывания, однако другой интерфейс IListSource также вполне подходит, поскольку он предоставляет простой метод (GetList) для получения объекта IList. Когда свойство DataSource установлено, сначала рекомендуется определить, был ли предоставлен допустимый объект, т.е. который поддерживает IList или IListSource. Рекомендуется выбрать IList, поскольку если предоставленный объект поддерживает только IListSource (например, DataTable), то для получения допустимого объекта используется метод GetList() этого интерфейса.

Некоторые объекты, реализующие IListSource (например, DataSet), фактически содержат несколько списков, что обозначено свойством ContainsListCollection. Если это свойство имеет значение True, то GetList возвратит объект IList, представляющий список нескольких подсписков. В рассматриваемом примере было решено поддерживать связи непосредственно с объектами IList или IListSource, хранящими только один объект IList и игнорирующими такие объекты, как DataSet, для которых необходимо дополнительно определить источник данных.

Примечание. Если требуется поддержка данного типа объектов (DataSet или ему подобных), можно добавить второе свойство (например, DataMember), определяющее используемый для связывания конкретный подсписок.

Если предоставлен допустимый источник данных, то в конечном результате будет создан экземпляр класса CurrencyManager (cm = Me.BindingContext(Value)). Этот экземпляр сохраняется в локальную переменную, так как он будет использоваться для обращения к базовому источнику данных, свойствам объекта и информации о позиции.

Добавление свойств Display и Value

Наличие DataSource - это первый шаг в сложном связывании данных, однако элементу управления необходимо знать, какие конкретные поля или свойства данных должны использоваться для значения и отображения. Член Display (отображение) будет использоваться как заголовок узлов дерева, член Value (значение) будет доступен через свойство Value узла. Эти свойства являются лишь строками, представляющими названия поля или свойства, и легко добавляются к элементу управления:

    Private m_ValueMember As String
    Private m_DisplayMember As String

    <Category("Data")> _
    Public Property ValueMember() As String
        Get
            Return m_ValueMember
        End Get
        Set(ByVal Value As String)
            m_ValueMember = Value
        End Set
    End Property

    <Category("Data")> _
    Public Property DisplayMember() As String
        Get
            Return m_DisplayMember
        End Get
        Set(ByVal Value As String)
            m_DisplayMember = Value
        End Set
    End Property

В данном TreeView эти свойства представляют членов Display и Value только для конечных узлов, соответствующая информация для каждого уровня группировки определяется в методе AddGroup.

Использование объекта CurrencyManager

В рассмотренном ранее свойстве DataSource экземпляр класса CurrencyManager был создан и сохранен в переменную уровня класса. Класс CurrencyManager, к которому обращаются через этот объект, является ключевым в реализации связывания данных, поскольку он обладает свойствами, методами и событиями, позволяющими:

  • обращаться к базовому объекту IList источника данных;
  • получать и устанавливать поля или свойства на объекте в источнике данных;
  • синхронизировать элемент управления с другими связанными с данными элементами управления в той же форме.

Получение значений Property/Field

Объект CurrencyManager позволяет получить значения свойства или поля отдельных элементов в источнике данных, например, значения полей DisplayMember или ValueMember, через свой метод GetItemProperties. Затем используются объекты PropertyDescriptor, чтобы получить значение конкретного поля или свойства определенного элемента списка. Приведенный ниже фрагмент кода показывает, как создаются эти объекты PropertyDescriptor, и как в последствии может использоваться функция GetValue, чтобы получить значение свойства одного из элементов в базовом источнике данных. Обратите внимание на свойство List объекта CurrencyManager: он предоставляет доступ к экземпляру IList, с которым был связан элемент управления:

Dim myNewLeafNode As TreeLeafNode
Dim currObject As Object
currObject = cm.List(currentListIndex)
If Me.DisplayMember <> "" AndAlso Me.ValueMember <> "" Then
    'добавим вершину?
    Dim pdValue As System.ComponentModel.PropertyDescriptor
    Dim pdDisplay As System.ComponentModel.PropertyDescriptor
    pdValue = cm.GetItemProperties()(Me.ValueMember)
    pdDisplay = cm.GetItemProperties()(Me.DisplayMember)
    myNewLeafNode = _
    New TreeLeafNode(CStr(pdDisplay.GetValue(currObject)), _
          currObject, _
          pdValue.GetValue(currObject), _
          currentListIndex)

GetValue возвращает объект независимо от типа базовых данных свойства, поэтому перед использованием возвращаемое значение необходимо конвертировать.

Поддержка синхронизации связанных с данными элементов управления

CurrencyManager имеет еще одну важную особенность: кроме предоставления доступа к связанному источнику данных и к свойствам элемента, он осуществляет координацию связывания данных между этим элементом управления и любыми другими элементами управления, использующими тот же DataSource. Эта поддержка обеспечивает то, что несколько элементов управления, связанных с одним источником данных, остаются на том же самом элементе в источнике данных. Для рассматриваемого элемента управления необходимо сделать так, чтобы при выборе элемента в дереве любые другие элементы управления, связанные с тем же источником данных, указывали на один и тот же элемент (запись, строку или кортеж, используя терминологию баз данных). Для этого необходимо переопределить метод OnAfterSelect базового элемента управления TreeView. В этом методе, вызываемом после выбора узла дерева, установим свойство Position объекта CurrencyManager на индекс текущего выбранного элемента. Типовое приложение, предоставляемое наряду с элементом управления TreeView, иллюстрирует, как явление синхронизированных элементов управления облегчает построение связанных с данными пользовательских интерфейсов. Чтобы облегчить определение позиции текущего выбранного элемента в списке, используйте созданные пользовательские классы TreeNode (TreeLeafNode или TreeGroupNode) и сохраните индекс списка каждого узла в свойство Position:

Protected Overrides Sub OnAfterSelect _ 
(ByVal e As System.Windows.Forms.TreeViewEventArgs)
    Dim tln As TreeLeafNode
    If TypeOf e.Node Is TreeGroupNode Then
        tln = FindFirstLeafNode(e.Node)
        Dim groupArgs As New groupTreeViewEventArgs(e)
        RaiseEvent AfterGroupSelect(groupArgs)
    ElseIf TypeOf e.Node Is TreeLeafNode Then
        Dim leafArgs As New leafTreeViewEventArgs(e)
        RaiseEvent AfterLeafSelect(leafArgs)
        tln = CType(e.Node, TreeLeafNode)
    End If

    If Not tln Is Nothing Then
        If cm.Position <> tln.Position Then
            cm.Position = tln.Position
        End If
    End If
    MyBase.OnAfterSelect(e)
End Sub

В предыдущем фрагменте кода используется функция FindFirstLeafNode, которую необходимо немного разъяснить. В рассматриваемом TreeView только конечные узлы иерархии соответствуют элементам в DataSource, остальные узлы служат только для создания структуры группы. Чтобы построить хороший связанный с данными элемент управления, в нем всегда должен быть выбранный элемент, соответствующий DataSource, поскольку при выборе узла группы в ней всегда будет находиться первый конечный узел, который рассматривается как текущий выбор. Как это работает, можно увидеть на этом примере, однако пока проверить невозможно, что это действительно работает, поэтому будем доверять автору:

Private Function FindFirstLeafNode(ByVal currNode As TreeNode) _
        As TreeLeafNode
    If TypeOf currNode Is TreeLeafNode Then
        Return CType(currNode, TreeLeafNode)
    Else
        If currNode.Nodes.Count > 0 Then
            Return FindFirstLeafNode(currNode.Nodes(0))
        Else
            Return Nothing
        End If
    End If
End Function

Установка свойства Position объекта CurrencyManager позволяет сохранить синхронизацию других элементов управления с текущим выбранным элементом, но CurrencyManager также генерирует события, когда другие элементы управления изменяют позицию так, чтобы можно было изменить выбранный элемент соответствующим образом. Чтобы построить небольшой хороший связанный с данными компонент, выбор должен перемещаться при изменении позиции источника данных, и если данные элемента изменились, отображение должно обновиться. CurrencyManager генерирует три события: CurrentChanged, ItemChanged и PositionChanged. Последнее событие довольно прямое; одной из целей CurrencyManager является управление индикатором текущей позиции для источника данных так, чтобы несколько связанных элементов управления отображали ту же запись или элемент списка, и это событие будет генерироваться при любом изменении этой позиции. В некоторых случаях другие события накладываются друг на друга, поэтому они не очень ясны. Здесь представлена развертка их использования в своем пользовательском элементе управления: PositionChanged - это простое событие, поэтому рассмотрим его сразу; используйте его, когда необходимо скорректировать текущий выбранный элемент в сложном связанном с данными элементе управления, таком как данное дерево. Событие ItemChanged генерируется при изменении любого элемента в источнике данных, а CurrentChanged - только при изменении текущего элемента.

В рассматриваемом TreeView было обнаружено, что все три события генерируются при выборе нового элемента, поэтому было решено обрабатывать событие PositionChanged при изменении текущего выбранного элемента, а два других события не обрабатывать вообще. В Документации по .Net Framework рекомендуется преобразовать источник данных в IBindingList (если он поддерживает IBindingList) и вместо этого использовать его событие ListChanged, однако эти функциональные возможности не были реализованы:

Private Sub cm_PositionChanged(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles cm.PositionChanged
    Dim tln As TreeLeafNode
    If TypeOf Me.SelectedNode Is TreeLeafNode Then
        tln = CType(Me.SelectedNode, TreeLeafNode)
    Else
        tln = FindFirstLeafNode(Me.SelectedNode)
    End If

    If tln.Position <> cm.Position Then
        Me.SelectedNode = FindNodeByPosition(cm.Position)
    End If
End Sub

Private Overloads Function FindNodeByPosition(ByVal index As Integer) _
        As TreeNode
    Return FindNodeByPosition(index, Me.Nodes)
End Function

Private Overloads Function FindNodeByPosition(ByVal index As Integer, _
    ByVal NodesToSearch As TreeNodeCollection) As TreeNode
    Dim i As Integer = 0
    Dim currNode As TreeNode
    Dim tln As TreeLeafNode

    Do While i < NodesToSearch.Count
        currNode = NodesToSearch(i)
        i += 1
        If TypeOf currNode Is TreeLeafNode Then
            tln = CType(currNode, TreeLeafNode)
            If tln.Position = index Then
                Return currNode
            End If
        Else
            currNode = FindNodeByPosition(index, currNode.Nodes)
            If Not currNode Is Nothing Then
                Return currNode
            End If
        End If
    Loop
    Return Nothing
End Function

Преобразование DataSource в дерево

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

Управление группами

Необходимо создать функции AddGroup, RemoveGroup и ClearGroups, чтобы сконфигурировать коллекцию групп. При каждом изменении коллекции групп дерево должно перерисовываться (чтобы отразить новую конфигурацию), поэтому была создана общая процедура GroupingChanged, вызываемая различным кодом повсюду в элементе управления при любом изменении и обновляющая дерево: P

rivate treeGroups As New ArrayList()

Public Sub RemoveGroup(ByVal group As Group)
    If Not treeGroups.Contains(group) Then
        treeGroups.Remove(group)
        GroupingChanged()
    End If
End Sub

Public Overloads Sub AddGroup(ByVal group As Group)
    Try
        treeGroups.Add(group)
        GroupingChanged()
    Catch
    End Try
End Sub

Public Overloads Sub AddGroup(ByVal name As String, _
        ByVal groupBy As String, _
        ByVal displayMember As String, _
        ByVal valueMember As String, _
        ByVal imageIndex As Integer, _
        ByVal selectedImageIndex As Integer)
    Dim myNewGroup As New Group(name, groupBy, _
        displayMember, valueMember, _
        imageIndex, selectedImageIndex)
    Me.AddGroup(myNewGroup)
End Sub


Public Function GetGroups() As Group()
    Return CType(treeGroups.ToArray(GetType(Group)), Group())
End Function

Построение дерева

Фактическое обновление дерева обрабатывается двумя процедурами: BuildTree и AddNodes. Их код достаточно длинный, поэтому попытаемся сделать обзор их поведения и не включать весь код в статью (конечно, его можно загрузить). Как упоминалось ранее, разработчик взаимодействует с этим элементом управления путем установки ряда групп, которые затем используются в BuildTree, чтобы установить узлы дерева. BuildTree очищает текущую коллекцию узла, проходит цикл через весь источник данных к процессам первого уровня группировки (см. Publisher в примерах и рисунки в этой статье выше), добавляя по одному узлу для каждого различного значения группировки (один узел для каждого значения pub_id в данном примере), и затем вызывает AddNodes, чтобы заполнить все узлы ниже первого уровня группировки. AddNodes вызывает себя рекурсивно, чтобы обработать любое число уровней, добавляемых в узлы группы и конечные узлы соответственно. Два пользовательских класса, основанные на TreeNode, используются для различения узлов группы и конечных узлов и предоставления каждому типу узла своего набора релевантных свойств.

Настройка событий TreeView

При выборе узла элемент управления TreeView генерирует два события: BeforeSelect и AfterSelect. Однако для рассматриваемого элемента управления лучше иметь различные события для узлов группы и конечных узлов, поэтому добавим собственные события BeforeGroupSelect/AfterGroupSelect и BeforeLeafSelect/AfterLeafSelect с пользовательскими классами параметров событий, которые генерируются в дополнение к основным событиям:

Public Event BeforeGroupSelect _
    (ByVal sender As Object, ByVal e As groupTreeViewCancelEventArgs)
Public Event AfterGroupSelect _
    (ByVal sender As Object, ByVal e As groupTreeViewEventArgs)
Public Event BeforeLeafSelect _
    (ByVal sender As Object, ByVal e As leafTreeViewCancelEventArgs)
Public Event AfterLeafSelect _
    (ByVal sender As Object, ByVal e As leafTreeViewEventArgs)

Protected Overrides Sub OnBeforeSelect _
    (ByVal e As System.Windows.Forms.TreeViewCancelEventArgs)
    If TypeOf e.Node Is TreeGroupNode Then
        Dim groupArgs As New groupTreeViewCancelEventArgs(e)
        RaiseEvent BeforeGroupSelect(CObj(Me), groupArgs)
    ElseIf TypeOf e.Node Is TreeLeafNode Then
        Dim leafArgs As New leafTreeViewCancelEventArgs(e)
        RaiseEvent BeforeLeafSelect(CObj(Me), leafArgs)
    End If
    MyBase.OnBeforeSelect(e)
End Sub

Protected Overrides Sub OnAfterSelect _
    (ByVal e As System.Windows.Forms.TreeViewEventArgs)
    Dim tln As TreeLeafNode
    If TypeOf e.Node Is TreeGroupNode Then
        tln = FindFirstLeafNode(e.Node)
        Dim groupArgs As New groupTreeViewEventArgs(e)
        RaiseEvent AfterGroupSelect(CObj(Me), groupArgs)
    ElseIf TypeOf e.Node Is TreeLeafNode Then
        Dim leafArgs As New leafTreeViewEventArgs(e)
        RaiseEvent AfterLeafSelect(CObj(Me), leafArgs)
        tln = CType(e.Node, TreeLeafNode)
    End If

    If Not tln Is Nothing Then
        If cm.Position <> tln.Position Then
            cm.Position = tln.Position
        End If
    End If
    MyBase.OnAfterSelect(e)
End Sub

Пользовательские классы узлов (TreeLeafNode и TreeGroupNode) и пользовательские классы параметров событий доступны в коде для загрузки.

Типовое приложение

Чтобы полностью понять весь код в этом типовом элементе управления, проанализируйте его работу в приложении. Предоставленное типовое приложение работает с базой данных Microsoft Access pubs.mdb и иллюстрирует, как элемент управления Tree взаимодействует с другими связанными с данными элементами управления, чтобы создать приложения Windows. Главной особенностью этого примера является то, что необходимо уделить особое внимание включению синхронизации Tree с другими связанными элементами управления и автоматическому выбору узла дерева, когда в источнике данных выполняется поиск.

Примечание. Это типовое приложение (называющееся "TheSample") можно загрузить для этой статьи.

Рис. 4. Демонстрационное приложение для связанного с данными TreeView

Резюме

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

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


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


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

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

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

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