|
В лекции №2 вставляю модуль данных. При попытке заменить name на fDM выдает ошибку: "The project already contains a form or module named fDM!". Что делать? |
Сохранение древовидных структур в базе данных
Древовидные структуры не относятся напрямую к программированию баз данных, тем не менее, программисту нередко приходится "изобретать велосипед", придумывая различные решения сохранения таких структур в таблице, и обратное их считывание в дерево.
Типичный пример дерева - всем знакомое дерево каталогов. Примеров таких структур множество - это могут быть отделы в каком-либо учреждении или разделы библиотеки. Посмотрим на рисунок с фрагментом дерева разделов библиотеки:
Основная сложность хранения деревьев в таблице - это то, что мы не знаем заранее, какова будет глубина вложенности разделов. Можно было бы создать таблицу с 10 полями, например. Но если вложенных разделов будет меньше, то таблица будет неэффективна - останется много пустых полей. А если больше - ограничивать пользователя?
Самый простой способ сохранения структуры дерева и ее считывания обратно - воспользоваться тем, что дерево - это список узлов, и имеет хорошо знакомые нам методы:
//сохраняем в файл:
TreeView1.SaveToFile('myfile.txt');
//читаем из файла:
TreeView1.LoadFromFile('myfile.txt');Однако этот способ имеет массу недостатков. Во-первых, в результате получим простой текстовый файл, в котором вложенные узлы располагаются ниже родителя и имеют отступ. Пользователь легко может случайно или намеренно испортить такой файл, отредактировав или просто удалив его с диска, и программа будет работать с ошибками. Во-вторых, обычно древовидная структура тесно связана с другими данными, например, таблица отделов предприятия связана со служащими этого предприятия - запись каждого служащего имеет ссылку на отдел, где он работает. Если структуру предприятия хранить в простом текстовом файле, то такую связь сложно будет обеспечить.
Когда программист впервые сталкивается с необходимостью хранения древовидных структур в базе данных, обычно он первым делом подключается к Интернету и ищет какой-нибудь компонент, который бы позволил это делать. Но не все нестандартные компоненты работают качественно, да и зачем искать какой-то новый компонент, когда имеется стандартный TreeView на вкладке Win32 Палитры компонентов? Именно с этим компонентом мы и будем работать в данной лекции.
Рецептов работы с деревьями в базах данных много, мы рассмотрим лишь один из них, достаточно эффективный и в то же время простой. Смысл этого способа состоит в том, чтобы в каждой записи таблицы сохранять номер узла раздела, номер его родителя, если он есть, и название узла. В случае если узел не имеет родителя (главный узел, например, "Художественная литература" в рисунке 10.1), то в соответствующее поле запишем ноль.
Подготовка проекта
Для реализации примера нам потребуется новая база данных. Загрузите MS Access и создайте базу данных " TreeBD ", а в ней таблицу " Razdels ". Вообще-то, в базе данных MS Access как таблицы, так и поля могут иметь русские названия, однако мы будем использовать средства SQL, который не всегда корректно обрабатывает русские идентификаторы. Кроме того, данный способ можно использовать в любой СУБД, а далеко не все из них так предупредительны, как MS Access, поэтому название таблицы и ее полей выполним латиницей.
| № | Имя поля | Тип поля | Дополнение |
|---|---|---|---|
| 1 | R_Num | Счетчик | Ключевое поле |
| 2 | R_ Parent | Числовой | Целое |
| 3 | R_Name | Текстовый | Длина 50 символов |
Созданную базу данных сохраните в папке, где будем разрабатывать наш проект (не забудьте сделать резервную копию пустой базы данных на всякий случай.).
Далее создадим в Delphi новый проект и простую форму:
Как всегда, назовите форму fMain, в свойстве Caption напишите "Реализация сохранения дерева в БД", модуль формы сохраните как Main, а проект в целом назовите, например, TreeToBD. Сделанная база данных TreeBD должна быть в той же папке, что и проект.
Далее установите компонент TreeView (дерево) с вкладки Win32. Его свойству Align присвойте alLeft, чтобы дерево заняло весь левый край. Затем можете установить сплиттер - разделитель, ухватившись за который пользователь сможет менять ширину дерева. Компонент Splitter находится на вкладке Additional и его свойство Align по умолчанию равно alLeft - разделитель "прилепится" к правому краю дерева.
Правее установите сетку DBGrid с вкладки Data Controls, и его свойству Align присвойте alClient, чтобы сетка заняла все оставшееся место. Ни главное меню, ни панель инструментов нам здесь не потребуются, используем лишь два всплывающих PopupMenu - первый для дерева, второй для сетки (выберите соответствующие PopupMenu в свойстве PopupMenu этих компонентов).
Далее с вкладки ADO нам потребуется компонент ADOConnection для соединения с базой данных, таблица ADOTable и запрос ADOQuery для вспомогательных нужд. С вкладки Data Access - компонент DataSource, для связи сетки с таблицей. Подключите ADOConnection к базе данных и откройте соединение ( "ADO. Связь с таблицей MS Access" ). Таблицу подключите к ADOConnection (свойство Connection ), затем выберите в свойстве TableName нашу таблицу " Razdels ", а свойство Name переименуйте в tRazdels - так будем обращаться к таблице. Для удобства отображения названия полей откройте редактор полей таблицы (дважды щелкнув по ней), добавьте все поля и у каждого поля измените свойство DisplayLabel, соответственно, на "№", "Родитель" и "Название". Не забудьте открыть таблицу.
Компонент DataSource подключите к tRazdels, а сетку - к DataSource, в сетке должны отобразиться поля. Кроме того, переименуйте свойство Name запроса ADOQuery1 в Q1, ведь нам часто придется обращаться к нему по имени. Запрос также подключите к ADOConnection, но делать его активным не нужно.
На этом приготовления закончены.
Создание и сохранение в таблицу дерева разделов
Работа с деревьями состоит из двух этапов:
- Сохранение дерева в таблицу.
- Считывание дерева из таблицы.
В этом разделе лекции разберем первый этап. Щелкните дважды по компоненту PopupMenu1, который "привязан" к дереву, и создайте в нем следующие разделы:
- Создать главный раздел
- Добавить подраздел к выделенному
- Переименовать выделенный
- Удалить выделенный
- -
- Свернуть дерево
- Развернуть дерево
Все эти команды относятся к работе с разделами дерева. Прежде всего, создадим обработчик для команды "Создать главный раздел". Листинг процедуры смотрите ниже:
{Создать главный раздел}
procedure TfMain.N1Click(Sender: TObject);
var
s: String; //для получения имени раздела (подраздела)
NewRazd: TTreeNode; //для создания нового узла дерева
begin
//вначале очистим s
s:= '';
//Получим в s имя нового раздела:
if not InputQuery('Ввод имени раздела',
'Введите заголовок раздела:', s) then Exit;
//снимаем возможное выделение у дерева:
TreeView1.Selected:= nil;
//создаем главный раздел (ветвь):
NewRazd:= TreeView1.Items.Add(TreeView1.Selected, s);
//Сразу же сохраняем его в базу:
tRazdels.Append; //добавляем запись
tRazdels['R_Parent']:= 0; //не имеет родителя
//присваиваем значение созданного раздела:
tRazdels['R_Name']:= NewRazd.Text;
//сохраняем изменения в базе:
tRazdels.Post;
end;Разберем код. Переменная NewRazd имеет тип TTreeNode, к которому относятся все разделы и подразделы (узлы) дерева. В текстовую переменную s с помощью функции InputQuery() мы получаем имя нового главного узла. Функция имеет три строковых параметра:
- Заголовок окна.
- Пояснительная строка.
- Переменная, куда будет записан введенный пользователем текст.
Если переменная, передаваемая в качестве третьего параметра, пуста, то поле ввода будет пустым. Если же в ней содержался текст - он будет выведен как текст "по умолчанию". Функция возвращает True, если пользователь ввел (или изменил) текст, и False в противном случае. В результате работы функции для пользователя будет выведено простое окно с запросом:
Далее строкой
TreeView1.Selected:= nil;
мы снимаем выделение, если какой либо раздел был выделен, ведь мы создаем главный раздел, не имеющий родителя. Свойство Selected компонента TreeView указывает на выделенный узел и позволяет производить с ним различные действия, например, получить текст узла:
TreeView1.Selected.Text;
А присваиваемое значение nil (ничто) снимает всякое выделение, если таковое было. Далее мы создаем сам узел:
NewRazd:= TreeView1.Items.Add(TreeView1.Selected, s);
Разберем эту строку подробней. Переменная NewRazd - это новый узел дерева. Каждый узел - объект, обладающий своими свойствами и методами. Все узлы хранятся в списке - свойстве Items дерева TreeView, а метод Add() этого свойства позволяет добавить новый узел. У метода два параметра - выделенный узел (у нас он равен nil ) и строка текста, которая будет присвоена новому узлу. Таким образом, в дереве появляется новый главный узел.
Затем мы сохраняем его в базу данных, предварительно добавив в таблицу новую запись:
tRazdels.Append; //добавляем запись tRazdels['R_Parent']:= 0; //не имеет родителя //присваиваем значение созданного раздела: tRazdels['R_Name']:= NewRazd.Text; //сохраняем изменения в базе: tRazdels.Post;
Вы помните, что такие методы, как Append или Insert автоматически переводят таблицу в режим редактирования, поэтому вызывать метод Edit излишне?
Обратите внимание на то, что мы сохраняем ноль в поле "R_ Parent ", так как это - главный раздел, не имеющий родителя. Свойство Text нового узла NewRazd содержит название нового узла, которое мы присваиваем полю "R_Name".
Далее сгенерируем процедуру для команды меню "Добавить подраздел к выделенному":
{Добавить подраздел к выделенному разделу(подразделу)}
procedure TfMain.N2Click(Sender: TObject);
var
s: String; //для получения имени раздела (подраздела)
z: String; //для формирования заголовка окна
NewRazd: TTreeNode; //для создания нового узла дерева
begin
//Проверим - есть ли выделенный раздел?
//Если нет - выходим:
if TreeView1.Selected = nil then Exit;
//вначале очистим s
s:= '';
//сформируем заголовок окна запроса:
z:= 'Раздел " + TreeView1.Selected.Text +
'";
//Получим в s имя нового раздела:
if not InputQuery(PChar(z), 'Введите заголовок подраздела:',
s) then Exit;
//создаем подраздел:
NewRazd:= TreeView1.Items.AddChild(TreeView1.Selected, s);
//перед сохранением подраздела в базу, прежде получим
//номер его родителя:
Q1.SQL.Clear;
Q1.SQL.Add('select * from Razdels
where R_Name="+
NewRazd.Parent.Text+");
Q1.Open;
//Теперь сохраняем его в базу:
tRazdels.Append; //добавляем запись
//присваиваем № родителя:
tRazdels['R_Parent']:= Q1['R_Num'];
//присваиваем название узла:
tRazdels['R_Name']:= NewRazd.Text;
//сохраняем изменения в базе:
tRazdels.Post;
end;Код этой процедуры очень похож на код предыдущей, но есть и отличия. Прежде всего, мы проверяем - а имеется ли выделенный раздел? Ведь фокус ввода мог быть и на сетке DBGrid, когда пользователь щелкнул правой кнопкой по дереву, и выбрал эту команду. В этом случае, если не делать проверки, мы получим ошибку, пытаясь добавить дочерний узел к пустоте.
Далее, мы ввели строковую переменную z, чтобы сформировать запрос. Ведь пользователю будет удобней, если в окне InputQuery() он сразу увидит, к какому именно разделу он добавляет подраздел.
Затем, при добавлении дочернего узла вместо метода Add() мы используем метод AddChild().
Ну и, наконец, при сохранении узла в таблицу мы записываем не только созданный узел, но и номер его родителя, получив его с помощью запроса
Q1.SQL.Add('select * from Razdels where R_Name='"'+
NewRazd.Parent.Text+'"');Запрос формирует набор данных с единственной строкой - записью родителя добавляемого элемента. Поле Q1['R_Num'], как вы понимаете, хранит номер этого родителя в запросе.
Код процедуры переименования выделенного раздела выглядит так:
{Переименовать выделенный раздел (подраздел)}
procedure TfMain.N3Click(Sender: TObject);
var
s: String; //для получения имени раздела (подраздела)
z: String; //для формирования заголовка окна
begin
//Проверим - есть ли выделенный раздел?
//Если нет - выходим:
if TreeView1.Selected = nil then Exit;
//получаем текущий текст:
s:= TreeView1.Selected.Text;
//формируем заголовок:
z:= 'Редактирование "' + s + '"';
//если не изменили, выходим:
if not InputQuery(PChar(z), 'Введите новый заголовок:', s) then Exit;
//находим эту запись в таблице, учитывая, что ее по каким то
//причинам может и не быть:
if not tRazdels.Locate('R_Name', TreeView1.Selected.Text, [])
then begin
ShowMessage('Ошибка! Указанный раздел не существует в таблице.');
Exit;
end; //if
//если до сих пор не вышли из процедуры, значит запись найдена,
//и является текущей. изменяем ее:
tRazdels.Edit;
tRazdels['R_Name']:= s;
tRazdels.Post;
//теперь меняем текст выделенного узла:
TreeView1.Selected.Text := s;
end;Здесь комментарии достаточно подробны, чтобы вы разобрались с кодом. Следует обратить внимание на то, что вначале мы исправляем запись в таблице, и только потом - в узле. Если бы мы сначала исправили текст узла, как бы затем нашли старую запись в таблице? Пришлось бы вводить дополнительную переменную для хранения старого текста.
Удаляется выделенный узел еще проще:
{Удалить выделенный раздел (подраздел)}
procedure TfMain.N4Click(Sender: TObject);
var
s: String; //для строки запроса
begin
//Проверим - есть ли выделенный раздел?
//Если нет - выходим:
if TreeView1.Selected = nil then Exit;
//иначе формируем строку запроса:
s:= 'Удалить "' +
TreeView1.Selected.Text + '"?';
//запросим подтверждение у пользователя:
if Application.MessageBox(PChar(s), 'Внимание!',
MB_YESNOCANCEL+MB_ICONQUESTION) <> IDYES then Exit;
//если не вышли - пользователь желает удалить раздел.
//найдем и удалим его вначале из таблицы:
if tRazdels.Locate('R_Name', TreeView1.Selected.Text, []) then
tRazdels.Delete;
//теперь удаляем раздел из дерева:
TreeView1.Items.Delete(TreeView1.Selected);
end;Далее нам осталось сгенерировать процедуры для сворачивания и разворачивания дерева. Делается это одной строкой:
{свернуть дерево}
TreeView1.FullCollapse;
{развернуть дерево}
TreeView1.FullExpand;Итак, метод FullCollapse дерева TreeView сворачивает его узлы, а метод FullExpand разворачивает.
Теперь сохраните проект и скомпилируйте его. Попробуйте заполнить дерево разделами и подразделами, убедитесь, что параллельно данные сохраняются и в таблице.


