В этой статье основное внимание уделяется использованию элемента управления TreeView. Элемент управления TreeView имеет богатые функции и может использоваться во многих ситуациях. Сводка: Описывает, как добавить функциональность привязки данных к элементу управления TreeView. Это один из серии примеров разработки элементов управления Microsoft Windows. Вы можете прочитать эту статью вместе с соответствующей обзорной статьей.
Введение
По возможности следует начинать с готовых элементов управления, поскольку предоставляемые элементы управления Microsoft® Windows® Forms включают в себя так много кода и тестирования, что было бы расточительством отказываться от них и начинать с нуля; Исходя из этого, в этом примере я унаследую существующий элемент управления Windows Forms TreeView, а затем настрою его. Загрузив код элемента управления TreeView , вы также получите дополнительные примеры разработки элементов управления, а также пример приложения, демонстрирующий, как использовать расширенный элемент управления TreeView с другими элементами управления, привязанными к данным.
Проектирование древовидного представления привязки данных
Для разработчиков Windows добавление привязки данных к элементу управления TreeView является распространенной проблемой, но поскольку существует одно существенное различие между TreeView и другими элементами управления (такими как ListBox или DataGrid ) (т. е. TreeView отображает иерархические данные), базовый элемент управления. Эта функция пока не поддерживается (то есть нам еще придется его использовать). Имея таблицу данных, понятно, как отобразить эту информацию в ListBox или DataGrid , но использовать многоуровневую природу TreeView для отображения тех же данных не так просто. Лично я применял много разных методов при отображении данных с помощью TreeView , но есть один метод, который используется чаще всего: группировка данных в таблице по определенным полям, как показано на рисунке 1.
Рисунок 1. Отображение данных в TreeView.
В этом примере я создам элемент управления TreeView , в который смогу передать простой набор данных (показанный на рисунке 2) и легко получить результаты, показанные на рисунке 1.
Рисунок 2. Плоский набор результатов, содержащий всю информацию, необходимую для создания дерева, показанного на рисунке 1.
Прежде чем приступить к программированию, я придумал дизайн нового элемента управления, который будет обрабатывать этот конкретный набор данных, и, надеюсь, он будет работать во многих других подобных ситуациях. Добавьте коллекцию групп, достаточно большую, чтобы создать иерархию с использованием большинства плоских данных, в которой вы указываете поле группировки, поле отображения и поле значения для каждого уровня иерархии (любые или все поля должны быть одинаковыми). Чтобы преобразовать данные, показанные на рисунке 2, в TreeView , показанный на рисунке 1, мой новый элемент управления требует, чтобы вы определили два уровня группировки: «Издатель» и «Заголовок», а также определили pub_id в качестве поля группировки группы «Издатель» и title_id в качестве поля группировки. группы «Заголовок». Помимо полей группировки, вам также необходимо указать поля отображения и значения для каждой группы, чтобы определить текст, который отображается в соответствующем узле группы, и значение, которое однозначно идентифицирует конкретную группу. При обнаружении такого рода данных используйте pub_name/pub_id и title/title_id в качестве полей отображения/значения для этих двух групп. Информация об авторе становится конечными узлами дерева (узлами в конце иерархии группировки), и вам также необходимо указать поля идентификатора ( au_id ) и отображения ( au_lname ) для этих узлов.
При создании пользовательского элемента управления определение того, как программист будет использовать этот элемент управления, прежде чем приступить к написанию кода, поможет сделать элемент управления более эффективным. В этом случае мне бы хотелось, чтобы программист (с учетом показанных ранее данных и желаемого результата) смог выполнить группировку с помощью нескольких строк кода, например:
С помощью DbTreeControl .ValueMember = au_id .DisplayMember = au_lname .DataSource = myDataTable.DefaultView .AddGroup(Издатель, pub_id, pub_name, pub_id) .AddGroup(Заголовок, title_id, заголовок, title_id) Конец с |
Примечание. Это не последняя строка кода, которую я написал, но она похожа. При разработке элемента управления я понял, что мне нужно связать индекс изображения в ImageList, связанном с TreeView , с каждым уровнем группировки, поэтому мне пришлось добавить дополнительный параметр в метод AddGroup .
Чтобы фактически построить дерево, я просматриваю данные и ищу изменения в полях (указанных как значения группировки для каждой группировки), при этом создавая новые узлы группировки, если это необходимо, и листовой узел для каждого элемента данных. Из-за группировки узлов общее количество узлов будет больше, чем количество элементов в источнике данных, но для каждого элемента базовых данных будет ровно один листовой узел.
Рисунок 3: Групповые узлы и конечные узлы
Различие между листовыми узлами и групповыми узлами (показанное на рисунке 3) будет важно в оставшейся части этой статьи. Я решил по-разному относиться к этим двум типам узлов, создавать собственные узлы для каждого типа узлов и вызывать разные события в зависимости от выбранного типа узла.
Реализация привязки данных
Первым шагом в написании кода для этого элемента управления является создание проекта и соответствующего начального класса. В этом примере я сначала создаю новую библиотеку элементов управления Windows, затем удаляю класс UserControl по умолчанию и заменяю его новым классом, который наследуется от элемента управления TreeView :
Открытый класс dbTreeControl
Наследует System.Windows.Forms.TreeView
С этого момента я буду разрабатывать элемент управления, который можно будет разместить в форме и который будет иметь внешний вид и функциональность обычного TreeView . Следующим шагом будет добавление кода, необходимого для обработки новых функций, добавляемых в TreeView , а именно привязки и группировки данных.
Добавить свойство источника данных
Вся функциональность моего нового элемента управления важна, но две ключевые проблемы при создании сложных элементов управления с привязкой к данным — это обработка свойств DataSource и получение отдельных элементов из каждого объекта источника данных.
Создать процедуру атрибутов
Во-первых, любой элемент управления, используемый для реализации сложной привязки данных, должен реализовать подпрограмму свойства DataSource и поддерживать соответствующие переменные-члены:
Частный m_DataSource как объект _ Открытое свойство DataSource() как объект Получать Возврат m_DataSource Конец Получить Установить (значение ByVal как объект) Если ценность — это ничто, тогда см = Ничего ГруппированиеИзменено() Еще Если нет (значение TypeOf — IList или _ Значение TypeOf — IListSource), тогда ' не является допустимым источником данных для этой цели Выдать новое исключение System.Exception (неверный источник данных) Еще Если значение TypeOf равно IListSource, тогда Затемнить myListSource как IListSource myListSource = CType (Значение, IListSource) Если myListSource.ContainsListCollection = True Тогда Выдать новое исключение System.Exception (неверный источник данных) Еще — Да, да. это действительный источник данных m_DataSource = Значение см = CType(Me.BindingContext(Значение), _ Валютный менеджер) ГруппированиеИзменено() Конец, если Еще m_DataSource = Значение см = CType(Me.BindingContext(Значение), _ Валютный менеджер) ГруппированиеИзменено() Конец, если Конец, если Конец, если Конечный набор Конечная собственность |
Обычно поддерживаются объекты, которые можно использовать в качестве источников данных для сложных привязок данных. Этот интерфейс представляет данные как коллекцию объектов и предоставляет несколько полезных свойств, таких как Count . Моему новому элементу управления TreeView в привязке требуется объект, поддерживаемый IList , но использование другого интерфейса работает нормально, поскольку обеспечивает удобный способ получения объекта IList ( GetList ). При установке свойства DataSource я сначала определяю, предоставлен ли допустимый объект, то есть объект, поддерживающий IList или IListSource . Что мне действительно нужно, так это IList , поэтому, если объект поддерживает только IListSource (например, DataTable ), тогда я буду использовать метод GetList() этого интерфейса, чтобы получить правильный объект.
Некоторые объекты, реализующие IListSource (например, DataSet ), на самом деле содержат несколько списков, представленных свойством containsListCollection . Если это свойство имеет значение True , GetList вернет объект IList , представляющий список (содержащий несколько списков). В моем примере я решил поддерживать прямые подключения к объектам IList или объектам IListSource , которые содержат только один объект IList , и игнорировать объекты, требующие дополнительной работы для указания источника данных, например DataSet .
Примечание. Если вы хотите поддерживать такие объекты ( DataSet или аналогичные), вы можете добавить дополнительное свойство (например, DataMember ), чтобы указать конкретный подсписок для привязки.
Если предоставленный источник данных действителен, конечным результатом является созданный экземпляр ( cm = Me.BindingContext(Value) ). Поскольку этот экземпляр будет использоваться для доступа к базовому источнику данных, свойствам объекта и информации о местоположении, он хранится в локальных переменных.
Добавление свойств элементов отображения и значения
Наличие источника данных — это первый шаг в реализации сложной привязки данных, но элементу управления необходимо знать, какие конкретные поля или свойства данных будут использоваться в качестве элементов отображения и значений. Элемент Display будет использоваться в качестве заголовка узла дерева, а элемент Value доступен через свойство Value узла. Все эти свойства представляют собой строки, представляющие имена полей или свойств, и их можно легко добавить в элемент управления:
Частный m_ValueMember как строка Частный m_DisplayMember как строка _ Открытое свойство ValueMember() как строка Получать Возврат m_ValueMember Конец Получить Установить (значение ByVal в виде строки) m_ValueMember = Значение Конечный набор Конечная собственность _ Открытое свойство DisplayMember() как строка Получать Возврат m_DisplayMember Конец Получить Установить (значение ByVal в виде строки) m_DisplayMember = Значение Конечный набор Конечная собственность |
В этом TreeView эти свойства будут представлять только элементы Display и Value конечных узлов, а соответствующая информация для каждого уровня группировки будет указана в методе AddGroup .
Использование объекта CurrencyManager
В свойстве DataSource , обсуждавшемся ранее, создается экземпляр класса CurrencyManager , который сохраняется в переменной уровня класса. Класс CurrencyManager , доступ к которому осуществляется через этот объект, является ключевой частью реализации привязки данных, поскольку он имеет свойства, методы и события, которые позволяют выполнять следующие функции:
Получить значение атрибута/поля
Объект CurrencyManager позволяет получать значения свойств или полей, например значение поля DisplayMember или ValueMember , из отдельного элемента в источнике данных с помощью метода GetItemProperties . Затем используйте объект PropertyDescriptor , чтобы получить значение определенного поля или свойства в определенном элементе списка. В следующем фрагменте кода показано, как создать эти объекты PropertyDescriptor и как использовать функцию GetValue для получения значения свойства элемента в базовом источнике данных. Обратите внимание на свойство List объекта CurrencyManager : оно обеспечивает доступ к экземпляру IList , к которому привязан элемент управления:
Затемнить myNewLeafNode как TreeLeafNode Dim currObject как объект currObject = cm.List(currentListIndex) Если Me.DisplayMember <> AndAlso Me.ValueMember <> Тогда 'Добавить листовой узел? Dim pdValue As System.ComponentModel.PropertyDescriptor Dim pdDisplay As System.ComponentModel.PropertyDescriptor pdValue = cm.GetItemProperties()(Me.ValueMember) pdDisplay = cm.GetItemProperties()(Me.DisplayMember) мойНовыйЛифНоде = _ Новый TreeLeafNode(CStr(pdDisplay.GetValue(currObject)), _ куррОбъект, _ pdValue.GetValue(currObject), _ текущийлистиндекс) |
GetValue игнорирует базовый тип данных свойства при возврате объекта, поэтому возвращаемое значение необходимо преобразовать перед его использованием.
Синхронизируйте элементы управления, связанные с данными
CurrencyManager имеет еще одну важную функцию: помимо предоставления доступа к привязанным источникам данных и свойствам элементов, он позволяет использовать один и тот же источник данных для координации привязки данных между этим элементом управления и любым другим элементом управления. Эту поддержку можно использовать, чтобы гарантировать, что несколько элементов управления, привязанных к одному и тому же источнику данных, одновременно остаются в одном и том же элементе источника данных. Что касается моего элемента управления, я хочу убедиться, что при выборе элемента в дереве все остальные элементы управления, привязанные к тому же источнику данных, указывают на один и тот же элемент (одну и ту же запись, строку или даже массив, если хотите). думать в терминах базы данных). Для этого я переопределяю метод OnAfterSelect в базовом TreeView . В этом методе (который вызывается после выбора узла дерева) я присваиваю свойству Position объекта CurrencyManager индекс текущего выбранного элемента. Пример приложения, поставляемый с элементом управления TreeView , иллюстрирует, как элементы управления синхронизацией упрощают создание пользовательских интерфейсов с привязкой к данным. Чтобы упростить определение положения текущего выбранного элемента в списке, я использовал собственный класс TreeNode ( TreeLeafNode или TreeGroupNode ) и сохранил индекс списка каждого узла в созданном мной свойстве Position :
Защищенные переопределения Sub OnAfterSelect _ (По значению и как System.Windows.Forms.TreeViewEventArgs) Dim tln как TreeLeafNode Если TypeOf e.Node равен TreeGroupNode, тогда tln = FindFirstLeafNode(e.Node) Dim groupArgs как новый groupTreeViewEventArgs(e) RaiseEvent AfterGroupSelect (groupArgs) ИначеЕсли TypeOf e.Node имеет значение TreeLeafNode, тогда Dim LeafArgs как новый LeafTreeViewEventArgs(e) RaiseEvent AfterLeafSelect (leafArgs) tln = CType(e.Node, TreeLeafNode) Конец, если Если нет, то ничего, тогда Если cm.Position <> tln.Position Тогда cm.Position = tln.Position Конец, если Конец, если MyBase.OnAfterSelect(e) Конец субтитра |
В предыдущем фрагменте кода вы, возможно, заметили функцию FindFirstLeafNode , о которой я хочу здесь кратко рассказать. В моем TreeView только конечные узлы (последние узлы в иерархии) соответствуют элементам DataSource , все остальные узлы используются только для создания структуры группировки. Если я хочу создать хорошо работающий элемент управления с привязкой к данным, мне всегда нужно выбирать элемент, соответствующий DataSource , поэтому всякий раз, когда я выбираю узел группы, я нахожу первый листовой узел в группе, как если бы этот узел был текущий выбор. Вы можете проверить пример в действии, а пока можете смело его использовать.
Частная функция FindFirstLeafNode (ByVal currNode As TreeNode) _ Как TreeLeafNode Если TypeOf currNode равен TreeLeafNode, тогда Возврат CType(currNode, TreeLeafNode) Еще Если currNode.Nodes.Count > 0 Тогда Возврат FindFirstLeafNode(currNode.Nodes(0)) Еще Ничего не возвращать Конец, если Конец, если Конечная функция |
Установка свойства Position объекта CurrencyManager синхронизирует другие элементы управления с текущим выбором, но когда положение других элементов управления изменяется, CurrencyManager также генерирует события, чтобы выбор изменялся соответствующим образом. Чтобы быть хорошим компонентом, привязанным к данным, выделение должно перемещаться при изменении местоположения источника данных, а отображение должно обновляться при изменении данных для элемента. CurrencyManager вызывает три события: CurrentChanged , ItemChanged и PositionChanged . Последнее событие довольно простое; одна из целей CurrencyManager — поддерживать индикатор текущей позиции для источника данных, чтобы несколько связанных элементов управления могли отображать одну и ту же запись или элемент списка, и это событие вызывается при каждом изменении позиции. Два других события иногда совпадают, и различие менее четкое. Ниже описано, как использовать эти события в пользовательских элементах управления: PositionChanged — это относительно простое событие, которое не будет здесь описываться. Если вы хотите настроить выбранный в данный момент элемент в сложном элементе управления с привязкой к данным (например, в дереве), используйте событие. Событие ItemChanged возникает при каждом изменении элемента в источнике данных, а событие CurrentChanged возникает только при изменении текущего элемента.
В моем TreeView я обнаружил, что всякий раз, когда я выбираю новый элемент, возникают все три события, поэтому я решил обработать событие PositionChanged , изменив текущий выбранный элемент и ничего не делая с двумя другими. Рекомендуется привести источник данных к IBindingList (если источник данных поддерживает IBindingList ) и вместо этого использовать событие ListChanged , но я не реализовал эту функцию.
Private Sub cm_PositionChanged (отправитель ByVal As Object, _ ByVal e As System.EventArgs) Обрабатывает cm.PositionChanged Dim tln как TreeLeafNode Если TypeOf Me.SelectedNode — это TreeLeafNode, тогда tln = CType(Me.SelectedNode, TreeLeafNode) Еще tln = FindFirstLeafNode(Me.SelectedNode) Конец, если Если tln.Position <> cm.Position Тогда Me.SelectedNode = FindNodeByPosition(cm.Position) Конец, если Конец субтитра Функция частных перегрузок FindNodeByPosition (индекс ByVal как целое число) _ Как TreeNode Вернуть FindNodeByPosition(index, Me.Nodes) Конечная функция Функция частных перегрузок FindNodeByPosition (индекс ByVal As Integer, _ ByVal NodesToSearch As TreeNodeCollection) As TreeNode Тусклый я как целое число = 0 Dim currNode как TreeNode Dim tln как TreeLeafNode Делайте, пока я < NodesToSearch.Count currNode = NodesToSearch(i) я += 1 Если TypeOf currNode равен TreeLeafNode, тогда tln = CType(currNode, TreeLeafNode) Если tln.Position = индекс Тогда Возврат текущего узла Конец, если Еще currNode = FindNodeByPosition(индекс, currNode.Nodes) Если не currNode — это ничего, тогда Возврат текущего узла Конец, если Конец, если Петля Ничего не возвращать Конечная функция |
Преобразование источника данных в дерево
После написания кода привязки данных я могу добавить код для управления уровнями группировки, соответствующим образом построить дерево, а затем добавить некоторые пользовательские события, методы и свойства.
Группа управления
Чтобы настроить набор групп, программист должен создать функции AddGroup , RemoveGroup и ClearGroups . Всякий раз, когда коллекция групп изменяется, дерево должно быть перерисовано (чтобы отразить новую конфигурацию), поэтому я создал общую процедуру GroupingChanged , которую можно вызывать из различного кода в элементе управления, когда обстоятельства меняются и дерево необходимо принудительно изменить. быть перестроен:
Частные древовидные группы как новый ArrayList() Публичная подгруппа RemoveGroup (группа ByVal как группа) Если не TreeGroups.Contains(group) Тогда TreeGroups.Remove(группа) ГруппированиеИзменено() Конец, если Конец субтитра Публичные перегрузки Sub AddGroup (группа ByVal As Group) Пытаться TreeGroups.Add(группа) ГруппированиеИзменено() Ловить Конец попытки Конец субтитра Public Overloads Sub AddGroup (имя ByVal As String, _ Группа ByValПо As String, _ ByVal displayMember As String, _ ByVal valueMember Как строка, _ ByVal imageIndex Как целое число, _ ByVal selectedImageIndex как целое число) Dim myNewGroup как новая группа (имя, groupBy, _ displayMember, valueMember, _ imageIndex, selectedImageIndex) Me.AddGroup(мояНоваяГруппа) Конец субтитра Открытая функция GetGroups() как Group() Возврат CType(treeGroups.ToArray(GetType(Group))), Group()) Конечная функция |
связующее дерево
Фактическая реконструкция дерева выполняется парой процессов: BuildTree и AddNodes . Поскольку код этих двух процессов слишком длинный, в этой статье они не перечисляются, а делается попытка обобщить их поведение (конечно, при желании вы можете скачать полный код). Как упоминалось ранее, программисты могут взаимодействовать с этим элементом управления, создавая ряд групп, а затем используя эти группы в BuildTree, чтобы определить, как настроить узлы дерева. BuildTree очищает текущую коллекцию узлов, а затем выполняет итерацию по всему источнику данных для обработки группировки первого уровня (издатель, упомянутый в примере и на диаграмме ранее в этой статье), добавляя узел для каждого отдельного значения группировки (используя данные в примере, для каждого добавьте узел со значением pub_id ), а затем вызовите AddNodes, чтобы заполнить все узлы в группе первого уровня. AddNodes вызывает себя рекурсивно для обработки необходимого количества уровней, добавляя при необходимости групповые и конечные узлы. Используйте два пользовательских класса на основе TreeNode, чтобы различать групповые и конечные узлы, а также предоставлять соответствующие атрибуты для двух типов узлов.
Пользовательские события TreeView
Всякий раз, когда выбирается узел, TreeView вызывает два события: BeforeSelect и AfterSelect . Но в моем контроле мне хотелось сделать события узлов группы и конечных узлов разными, поэтому я добавил свои собственные события BeforeGroupSelect/AfterGroupSelect и BeforeLeafSelect/AfterLeafSelect . В дополнение к базовым событиям я также запускал собственный класс параметров событий:
Публичное событие BeforeGroupSelect _ (Отправитель ByVal как объект, ByVal e As groupTreeViewCancelEventArgs) Публичное событие AfterGroupSelect _ (Отправитель ByVal как объект, ByVal e As groupTreeViewEventArgs) Публичное событиеBeforeLeafSelect _ (Отправитель ByVal как объект, ByVal e как LeafTreeViewCancelEventArgs) Публичное событие AfterLeafSelect _ (Отправитель ByVal как объект, ByVal e как LeafTreeViewEventArgs) Защищенные переопределения Sub OnBeforeSelect _ (По значению и как System.Windows.Forms.TreeViewCancelEventArgs) Если TypeOf e.Node равен TreeGroupNode, тогда Dim groupArgs как новый groupTreeViewCancelEventArgs(e) RaiseEvent BeforeGroupSelect(CObj(Me), groupArgs) ИначеЕсли TypeOf e.Node имеет значение TreeLeafNode, тогда Dim LeafArgs как новый LeafTreeViewCancelEventArgs(e) RaiseEvent BeforeLeafSelect(CObj(Me), LeafArgs) Конец, если MyBase.OnBeforeSelect(e) Конец субтитра Защищенные переопределения Sub OnAfterSelect _ (По значению и как System.Windows.Forms.TreeViewEventArgs) Dim tln как TreeLeafNode Если TypeOf e.Node равен TreeGroupNode, тогда tln = FindFirstLeafNode(e.Node) Dim groupArgs как новый groupTreeViewEventArgs(e) RaiseEvent AfterGroupSelect (CObj (Me), groupArgs) ИначеЕсли TypeOf e.Node имеет значение TreeLeafNode, тогда Dim LeafArgs как новый LeafTreeViewEventArgs(e) RaiseEvent AfterLeafSelect(CObj(Me), LeafArgs) tln = CType(e.Node, TreeLeafNode) Конец, если Если нет, то ничего, тогда Если cm.Position <> tln.Position Тогда cm.Position = tln.Position Конец, если Конец, если MyBase.OnAfterSelect(e) Конец субтитра |
Пользовательские классы узлов ( TreeLeafNode и TreeGroupNode ) и пользовательские классы параметров событий включены в загружаемый код.
Образец заявления
Чтобы полностью понять весь код в этом образце элемента управления, вы должны понимать, как он работает в вашем приложении. Включенный пример приложения использует базу данных Access pubs.mdb и иллюстрирует, как элемент управления Tree можно использовать с другими элементами управления с привязкой к данным для создания приложений Windows. Ключевые особенности, на которые следует обратить особое внимание, в этом случае включают синхронизацию дерева с другими связанными элементами управления и автоматический выбор узлов дерева при выполнении поиска в источнике данных.
Примечание. Этот пример приложения (с названием TheSample) включен в загрузку для этой статьи.
Рисунок 4. Демонстрационное приложение для TreeView с привязкой к данным.
краткое содержание
Элемент управления Tree с привязкой к данным, описанный в этой статье, подходит не для каждого проекта, которому требуется элемент управления Tree для отображения информации базы данных, но он предоставляет метод настройки элемента управления для личных целей. Помните, что любой сложный элемент управления с привязкой к данным, который вы хотите создать, будет иметь большую часть того же кода, что и элемент управления «Дерево», и вы можете упростить будущую разработку элемента управления, изменив существующий код.