Главная » Статьи » Excel » Макросы и программы VBA

Структура типа "дерево" - готовый к использованию набор VBA классов

Введение

В рамках практического изучения ООП написал набор классов, реализующих построение древовидной структуры любого уровня сложности для VBA. Как вы знаете, сам VBA кроме массивов и коллекций (объект Collection) в готовом виде ничего более не имеет. Пределом мечтаний на данный момент является внешний компонент Dictionary из библиотеки Microsoft Scripting Runtime. В виду этакой скудности приходится городить конструкты типа Dictionary с элементами в виде других Dictionary, либо изобретать свои классы. Чем я и занялся. Теперь, если вам потребуется выстроить дерево, то вы можете воспользоваться моим готовым решением.

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

PerfectTree: свойства класса

Моя структура базируется на классе PerfectTree. Данный класс с точки зрения прикладного программиста содержит лишь корень нашего дерева (свойство Root) и несколько методов, о которых позже. Root – это экземпляр класса Node (узел). Узел Root создаётся одновременно с созданием экземпляра класса PerfectTree. Дальнейший рост дерева идёт от этого корня.

Свойство Описание
Root
Тип: Node
Доступ: Read only
Корневой узел древовидной структуры
UnicityLevel
Тип: Byte
Доступ: Read / Write
Управление режимом уникальности имен узлов. По умолчанию принимает значение cUnicityNone=0 - одинаковые имена разрешены. Можно установить режим cUnicityChildren=1 - нельзя иметь одинаковые имена только детям одного родителя, а также cUnicityGlobal=2 - у всех узлов дерева должны быть уникальные имена. Данный свойство контролируется только во время создания нового узла. Смена режима НИКАК не влияет на уже созданные узлы.

Node: свойства класса

Наше дерево представляет собой некое множество узлов, объединенных друг с другом отношениями родитель – потомки. То есть каждый узел имеет ОДНОГО родителя и может иметь потомков (одного или больше), а может и не иметь. Любой узел имеет следующие свойства:

Свойство Описание
Name
Тип: String
Доступ: Read / Write
Текстовое имя узла. Нет никаких требований уникальности имени по структуре. Корень имеет стандартное имя «_ROOT_». Любые узлы могут быть переименованы (поле доступно на запись), если есть такая необходимость.
ID
Тип: Long
Доступ: Read / Write
Числовой уникальный идентификатор. При создании нового узла он автоматически получает уникальный номер по принципу счётчика. Корневой узел имеет номер 0. Его первый потомок имеет номер 1 и так далее. Однако, вы можете при создании указать конкретный, желаемый вами номер, в этом случае создаваемый узел получит его в качестве ID. В любой момент вы можете присвоить ID узла другой номер. Если номер, который вы присваиваете уже используется другим узлом, то они обменяются номерами. Нельзя присвоить узлу, отличному от корня, номер 0. Однако корню можно присвоить любой другой номер и, если тот занят, то ноль перейдёт к тому узлу. То есть и тут фактически полная свобода.
Level
Тип: Long
Доступ: Read only
Поколение узла, считая от корня. Корень всегда имеет Level=0. Его прямые дети имеют Level=1, их дети 2 и так далее.
Children
Тип: Nodes
Доступ: Read only
Данное свойство представляет собой коллекцию узлов – потомков данного узла – в виде класса Nodes (подробнее о нём – ниже)
Parent
Тип: Node
Доступ: Read / Write
Ссылка на родителя узла. У корня содержит Nothing. Путем изменения данного свойства можно перемещать узлы (разумеется со всеми их потомками). Однако, есть 2 условия: 1) перемещаемый узел и приёмный родитель должны принадлежать к одному экземпляру класса PerfectTree; 2) приёмный родитель не может быть потомком перемещаемого.
Root
Тип: Node
Доступ: Read only
Ссылка на корень. У корня содержит Nothing.
Tree
Тип: PerfectTree
Доступ: Read only
Ссылка на экземпляр родительского класса
ThisIsRoot
Тип: Boolean
Доступ: Read only
Для корня - true, у остальных - false.
DeepCount
Тип: Long
Доступ: Read only
Число потомков во всех последующих поколениях.

Чтобы лучше уяснить себе назначение основных свойств узла, поизучайте эту иллюстрацию:

Nodes: свойства класса

Последний имеющийся класс – класс Nodes – представляет из себя просто коллекцию узлов. Они могут быть объединены по какому-то признаку, как, например, коллекция в свойстве Children класса Node объединена по принципу одного родителя, либо вы можете создать коллекцию с произвольными узлами.

Свойство Описание
Count
Тип: Long
Доступ: Read only
Количество узлов в коллекции. В частности, узнать количество детей у узла можно через Children.Count.
Item(index)
Тип: Node
Доступ: Read only
Узел-элемент коллекции с указанным индексом. Индекс начинается с 1. Коллекция умеет перечислять себя с использованием оператора for each.

PerfectTree: методы класса

Метод Описание
ExistsID
Параметры: ID
Возвращает: Boolean
Проверяет есть ли в дереве узел с указанным ID.
GetNodeByID
Параметры: ID
Возвращает: Node
Возвращает узел с указанным ID.
ExistsName
Параметры: Name
Возвращает: Boolean
Проверяет есть ли в дереве узел с указанным Name.
GetNodeByName
Параметры: Name
Возвращает: Node
Возвращает узел с указанным Name.

Node: методы класса

Метод Описание
CreateChild
Параметры: Name [,ID]
Возвращает: Node
Создаёт дочерний узел для того экземпляра класса, у которого вызывается. Параметр Name обязательный, ID опциональный. Если установлен режим уникальности имён узла (смотри свойство UnicityLevel объекта PerfectTree) и такое имя уже есть, то вместо создания узла метод просто вернёт ссылку на уже имеющейся узел (указанный ID при этом на процесс не влияет).
Remove
Параметры: нет
Возвращает: ничего
Удаляет узел, чей экземпляр вызвал данный метод. Если у узла есть потомки, то они также рекурсивно все удаляются, начиная от самых дальних и заканчивая текущим.
HasChildren
Параметры: нет
Возвращает: Boolean
Отвечает на вопрос: есть ли дети у текущего узла.
ChildExistsByName
Параметры: Name
Возвращает: Boolean
Отвечает на вопрос: есть ли у проверяемого узла дети с указанным именем. Именно дети, а не потомки вообще.
GetChildByName
Параметры: Name
Возвращает: Node
Возвращает дочерний узел по имени, если таковой, конечно, есть. В противном случае возвращается Nothing.
LinkPayload
Параметры: Variant
Возвращает: ничего
Прикрепляет к узлу некую полезную нагрузку, которую вы определяете сами. Это может быть какой-то простой тип или объект. В последнем случае вы, разумеется, должны использовать оператор Set.
HasPayload
Параметры: нет
Возвращает: Boolean
Отвечает на вопрос: есть ли у узла полезная нагрузка.
Payload
Параметры: нет
Возвращает: Variant
Возвращает полезную нагрузку узла, если она есть. Вы сами должны анализировать, что вам возвращается и как это принять.
GoBack
Параметры: Count
Возвращает: Node
Возвращает предка относительно вызвавшего экземпляра. С count=1 вы получите своего родителя, с count=2 получите родителя своего родителя и так далее. Если вы проскочите корень, указав слишком большой count, то в любом случае будет возвращен корневой узел.
Descendants
Параметры: NodesSet
Возвращает: Nodes
Возвращает коллекцию узлов в виде нового экземпляра Nodes, которая содержит потомков текущего узла. NodesSet принимает значения: PlusMe и MinusMe. Как нетрудно догадаться, параметр регулирует вопрос включения в конечную выборку текущего узла.
Ancestors
Параметры: NodesSet
Возвращает: Nodes
Возвращает коллекцию узлов в виде нового экземпляра Nodes, которая содержит всех предков текущего узла. NodesSet принимает значения: PlusRoot_PlusMe, PlusRoot_MinusMe, MinusRoot_PlusMe или MinusRoot_MinusMe. Параметр регулирует вопрос включения в конечную выборку корневого узла и текущего узла. Есть любые комбинации.
Adopt
Параметры: OldParent
Возвращает: нет
Текущий узел усыновляет всех детей указанного узла. Если нарушаются условия, о которых уже говорилось при обсуждении свойства Parent, то генерируется исключение.
DebugPrint
Параметры: Section
Возвращает: нет
Отладочная печать на лист Debug в указанную секцию листа. Полезный метод, чтобы понимать, что происходит. По идее его надо бы убрать, но я решил оставить для удобства.

Nodes: методы класса

Метод Описание
Add
Параметры: Node
Возвращает: ничего
Добавляет узел в данный экземпляр коллекции
Remove
Параметры: Node
Возвращает: ничего
Исключает узел из коллекции
Exists
Параметры: Node
Возвращает: Boolean
Проверяет входит ли указанный узел в текущую коллекцию
WithChildren
Параметры: нет
Возвращает: Nodes
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, у которых есть дети.
WithNoChildren
Параметры: нет
Возвращает: Nodes
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, у которых нет детей.
WithPayload
Параметры: нет
Возвращает: Nodes
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, у которых есть полезная нагрузка.
WithNoPayload
Параметры: нет
Возвращает: Nodes
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, у которых нет полезной нагрузки.
WithLevel
Параметры: Level
Возвращает: Nodes
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, принадлежащие к указанному level.
FilterByName
Параметры: SubName, Include
Возвращает: Nodes
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, у которых в названии есть (Include = True) или нет (Include = False) указанной подстроки.
SortByName
Параметры: Direction
Возвращает: Nodes
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, отсортированные по Name в указанном порядке.
SortByID
Параметры: Direction
Возвращает: Nodes
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, отсортированные по ID в указанном порядке.
GetArrayOfIDs
Параметры: нет
Возвращает: Variant
Возвращает массив номеров ID коллекции. Нумерация элементов с 1. Если коллекция пуста, возвращается Null.
GetArrayOfNames
Параметры: нет
Возвращает: Variant
Возвращает массив имен коллекции. Нумерация элементов с 1. Если коллекция пуста, возвращается Null.
GetArrayOfPayload
Параметры: нет
Возвращает: Variant
Возвращает массив полезной нагрузки узлов. Нумерация элементов с 1. Если коллекция пуста, возвращается Null.
GetArrayDistinctName
Параметры: нет
Возвращает: Variant
Возвращает массив уникальных имен узлов. Нумерация элементов с 1. Если коллекция пуста, возвращается Null.
DebugPrint
Параметры: Section
Возвращает: ничего
Отладочная печать членов коллекции на лист Debug в указанную секцию листа.

Некоторые замечания


Как грамотно прятать внутреннюю кухню ваших объектов?

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

Например, возьмём такое свойство класса Node, как Level. Если вы сделаете так:

То через некоторое время поймёте, что:

  1. У вас нет защиты от дурака. Кто угодно сможет присвоить неправильное значение свойству Level и сломает всю гармонию вашей структуры, которую, вы вообще-то говоря, обязаны защищать, если пишите компоненты для других. Если же вы будете в обработчике Let городить проверку присваиваемого значения на корректность, то получится слишком сложно и, в конце концов, медленно.

  2. Осознав вышесказанное, вы захотите закрыть свойство Level на запись из пользовательских модулей. Ведь действительно, при создании нового узла его Level вычисляется из родительского +1 и у пользователя нет никаких практических причин писать в это свойство. Но как это сделать? Вы в начале наивно используете директиву Friend, но она вам никак не поможет с этой проблемой.

  3. Тогда вы уберёте обработчик Property Let вообще. И через 5 минут столкнётесь с проблемой, что непонятно, как инициировать поле pLevel при создании нового экземпляра объекта Node. "C хрена ли?" - скажете вы. Объясняю: у вас есть родительский Node. Находясь в нём, вы вызываете метод CreateChild, в котором создаёте новый экземпляр класса Node. Но вы не можете передать туда информацию. Вы можете менять только Public или Friend свойства, открытые на запись! А мы как раз хотим от них избавиться. WTF?

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

Интерфейс класса

первое, что необходимо сделать, это объявить класс-предок или класс-интерфейс. В нём мы объявляем пустое публичное свойство Let - то есть именно то свойство, которое мы хотели бы скрыть в реализации класса Node.

Далее при объявлении наших основных классов, в частности Node, мы должны сослаться на наш интерфейс:

Ну и сам механизм, ради которого всё это затевалось:

Почему мы имеем право присвоить переменной TempINode значение CreateChild? Не смотря на то, что это переменные разных классов, класс Node - потомок класса INode, поэтому мы можем переменной родительского класса присвоить ссылку на экземпляр класса-потомка. Наоборот нельзя.

Далее мы помним, что свойство Level в классе Inode объявлено публичным, поэтому мы легко меняем его, но поскульку в TempINode у нас экземпляр класса Node, то фактически мы меняем свойство pLevel переменной CreateChild. Вот и всё.

Если вы полезете в код моих классов, то именно этого примера вы не найдёте, но обнаружите массу свойств и методов, которые объявлены в INode, и, используя описанный выше механизм,

Резюмирую: мы смогли избавиться от метода Let Level на уровне класса Node. Теперь никто не сможет нарушить целостность вашей структуры за счёт присвоения Level-у неправильного значения, а вы избавлены от утомительных проверок. Таким образом всё, что мы хотим спрятать пробрасывается через класс-предок INode. Пользователи не будут создавать экземпляры INode, поэтому нам наплевать, что там "намусорено", нам важно, что всё, что мы хотели скрыть или закрыть на запись, скрыто и закрыто.

Конвееры

Обратите внимание, как удобно использовать коллекции Nodes. Дело в том, что многие методы класса Nodes возвращают тоже экземпляры класса Nodes, поэтому вы можете формировать конвеер из методов и получить в конце то, что вам нужно:

Скачать PerfectTree


Файл XLSM (версия 0.20 от 22.05.2017)

ZIP архив с CLS файлами (для импорта классов в ваш файл)

Принимаются заявки на исправление ошибок и реализацию новой функциональности.

История изменений


  • Версия 0.19 от 19.05.2017. Первая опубликованная версия.

  • Версия 0.20 от 22.05.2017. Исправлены ошибки в Node.CreateChild, в Nodes.FilterByName. На отладочную печать теперь выводится Payload. Добавлены методы Nodes.GetArrayOfPayload, Nodes.GetArrayDistinctName.

  • Версия 0.21 от 23.05.2017. Пересобрал проект из-за непонятных багов в 0.20.

Приехали...

К моему глубокому огорчению, данное решение работает нестабильно. При запуске файла, если нажать кнопку "Создать дерево" на листе Data, то вы скорее всего получите ошибку Type mismatch или произойдёт крах Excel. При этом я почти на 99% уверен, что ошибок в коде нет. Если войти в IDE и начать делать ничего не значащие изменения, перекомпиляции, то код начинает работать. Всё это происходит скорее всего из-за ошибок в реализации ООП в MS Office. Если кто-то сможет мне указать на работающие способы заставить классы работать без существенной переделки архитектуры классов (например, я категорически не хочу отказываться от интерфейсных классов), то буду очень признателен, но что-то мне подсказывает, что этого не случится.

Категория: Макросы и программы VBA | Добавил: dsb75 (07.05.2017) | Автор: Батьянов Денис E W
Просмотров: 11352 | Комментарии: 2 | Рейтинг: 5.0/4
Всего комментариев: 2
1 InExSu   (05.08.2017 15:15) [Материал]
Привет!
Хоть пока это не для меня, но спасибо  wink
у самого было: код работал-работал и вдруг перестаёт работать без IDE - ошибки сыпятся в разных местах. Причём на более слабом компе работал без ошибок.
Влупил в пару нагруженных циклов for next
Код
DoEvents
 

2 dzja112   (12.01.2021 22:34) [Материал]
Я уже встречался с такими проблемами, и как в посте выше они решались исключительно добавлением DoEvents в каждый цикл подпрограмм.
Сам по себе DoEvents сильно может добавлять тормозов, поэтому
я как-то пытался заменить его на API вызов, но вышло не очень.
поэтому DoEvents мастхэв в циклах у меня.
В некоторых случаях пробовал вкладывать DoEvents внуть IF что бы делать прерывания не на каждый цикл, а четные или кратные 10..100.

предполагаю что проблема не в реализации ООП, а в обмене данными между программой и подключенными внешними объектами и классами.
Уходя в цикл VBA без DoEvents плохо реагирует на "пинги" и ОС видя зависшее по её мнению приложение обрывает его (кажется можно увидеть сообщения об этом в ивентах ОС). поправьте если не прав.

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

Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Яндекс.Метрика