Продолжается подписка на наши издания! Вы не забыли подписаться?

Delphi 4: новое слово Inprise
в семействе Borland Delphi.

В.Чистяков, С.Михайлов

wpeA.jpg (21285 bytes)

Требования к аппаратному обеспечению

Intel 486/66 MHz и выше (на P120,
64MB RAM с NT WorkStation 4.0
(Service Pack 3) работает вполне пристойно)
MS Windows 95 или Windows NT 4.0 (SP3)
Оперативная память 16Mb
(рекомендуется 32Mb и более)
Свободного места на жестком диске:
60Mb (Compact install), 190 Mb (Full Install)
CD-ROM (только для инсталляции)
Monitor VGA или выше
Мышь
Сетевая поддержка: Windows95/NT.

Корпорация Inprise, до недавнего времени гордо именовавшаяся “Borland”, выпустила в свет новую версию Delphi — популярной у нас среды быстрой разработки приложений, опирающейся на могучие плечи языка Object Pascal (Собственная объектно-ориентированная модификация языка программирования Pascal). Предыдущие версии этого продукта снискали себе славу действительно быстрых и достаточно удобных в использовании RAD, дающих помимо всего прочего возможность опуститься на уровень-другой ниже и обойти таким образом неизбежные проблемы языков высокого уровня. К сожалению, похвальное стремление слить в единое целое преимущества RAD и языков системного уровня приводит, кроме очевидного выигрыша, еще и к объединению проблем обоих подходов к программированию.

Начиная со второй версии Delphi поддерживает технологю COM и, надо отметить, что поддерживается неплохо: удобные Wizard’ы для создания ActiveX-компонентов и средства для работы с ними, такие как редактор Type Library, позволяют быстро собрать желанный ActiveX-control и встроить его куда-нибудь в Internet Explorer… Однако при более-менее длительной работе с редактором Type Library, в один прекрасный момент появляется сообщение: “access violation in module vcl40.dpl” или что-либо еще более занимательное – и Delphi приходится выгружать и запускать по новой (обычно такая беда приключалась при переходе из проекта с ActiveX-library в проект, тестирующий этот ActiveX).

Таблица 1.

Как всегда, в Delphi прекрасно поддерживается создание чего-нибудь стандартного, скрывая от программиста тонкости реализации : не нравится работать с TreeView через Windows API – возьмите соответствующий компонент Delphi; хотите организовать легкий доступ к базе данных – нет проблем, используйте TTable, TDataSet и TDBGrid; желаете реализовать COM/DCOM (а в Enterprise-версии – CORBA) — милости просим!

Однако, при всей этой кажущейся простоте разработка приложений заметно замедляется из-за проскальзывающих в различных местах недоделок. Так, например, при работе с теми же самыми ActiveX’ами было обнаружено любопытное явление — если вы хотите создать новый ActiveX, и перед этим забыли создать или открыть ActiveX Type library, то вас предупредят об этом и предложат создать новую Type Library. Если вы согласитесь, то при попытке запустить тестовый проект, содержащий ваш ActiveX, Delphi вежливо сообщит вам Error следующего содержания: “Property does not exist”, но стоит только добавить вновь созданному ActiveX’у свойство TabOrder, как все начинает работать! Причем, если вы добавляете новый ActiveX в уже созданную библиотеку, такой проблемы не возникает (по крайней мере в Delphi 4). В DelphiК3 происходили занятнейшие приключения с передачей фокуса в ActiveX – судя по всему, в четвертой версии эта ошибка исправлена.

На первый взгляд не совсем понятно, почему и в третьей, и в четвертой Delphi не срабатывает событие onDestroy для ActiveX form (onCreate выполняется вполне корректно). Если этому есть разумные объяснения, то почему их нельзя найти в help’е (может быть, help плохо организован?..). Нет, конечно, переопределение Destructor’a позволяет обойти возникающие трудности, но все же хотелось бы иметь работающее событие onDestroy, раз уж оно объявлено!

В целом Delphi 4 оставляет приятное впечатление, наследуя лучшие качества предыдущей версии и исправляя некоторые ее упущения. Большое количество компонентов “на все случаи жизни”, безусловно, очень ускоряет процесс создания приложения. Кроме того, достаточно просто можно создать свои собственные компоненты на базе уже существующих, или импортировать компоненты, написанные кем-то другим.

Чем же хорош Delphi 4?

Расширен язык Object Pascal:

  • введена поддержка динамических массивов (ранее динамические массивы считались прерогативой 4GL-языков, а в языках третьего поколения, таких как С++ и Pascal динамические массивы реализовывались с помощью классов или набора процедур)

  • перегружаемые методы

  • задание значений по умолчанию для параметров функций и процедур (это и предыдущие нововведение давно напрашивались, так как без них, с точки зрения полиморфизма, Object Pascal сильно уступал своему конкуренту С++)

  • Но совершенство еще не достигнуто… так, на наш взгляд, самым сильным преимуществом С++ являются шаблоны (templates). С их помощью можно писать “чистые алгоритмы”, то есть классы и процедуры, не зависящие от конкретных входных типов параметров. Не лишним было бы добавить переопределение операторов и макросы, более прозрачную работу с адресами…но тогда язык пришлось бы называть не Object Pascal, а Pascal++.

    Улучшена среда разработки:

  • Новый Менеджер Проектов (Project Manager) позволяет объединять связанные между собой проекты в единую проектную группу. Это позволяет организовать удобную работу над зависимыми друг от друга проектами, такими, как разработка приложений во многоуровневой архитектуре, библиотеки и т.п.

  • Code Explorer позволяет осуществить навигацию по модулям, то есть просматривать реализацию используемых методов и их описания, и автоматизирует процесс создания классов. По умолчанию Code Explorer прилеплен к левой части Code Editor’a. В Code Explorer’е в виде “дерева” показываются все типы, классы, свойства, методы, глобальные переменные и глобальные константы, определенные в модуле. Таким же образом отображаются другие модули, описанные в разделе uses. Code Explorer позволяет выполнять поиск по названиям для всех содержащимся в нем объектов. Выделение ветки в Code Explorer приводит к перемещению курсора в Code Editor на реализацию выбранного метода (это работает и в обратном порядке). Следует признать, что на сегодняшний день это лучшая реализация подобного средства. Она превосходит аналогичное средство из MS Visual C++ 6.0, и по удобству и полноте функций может сравниваться только с Object Browser из Visual Basic. Но Object Browser рассчитан только на показ описания функций и атрибутов объектов, а Code Explorer в первую очередь предназначен для навигации по исходным текстам программ. Разумеется, это сложнее реализовать.

  • Complete Word, появившийся еще в Delphi 3, начинает работать в Code Editor’e при одновременном нажатии на Ctrl-Space: показывается список пригодных в текущем случае методов, из которых можно выбрать требуемый. Поиск осуществляется по первым буквам, которые были набраны перед вызовом Complete Word.

  • Complete Class удачно дополняет Complete Word. Он позволяет избавиться от рутинного вбивания описания методов и их реализации. Complete Class вызывается из Code Explorer’a при добавлении новой ветки – при этом создается новое объявление в разделе Interface для создаваемого класса и создает “скелет” его реализации в разделе Implementation.

  • Среда разработки стала более удобной, перемещение и встраивание окон методом “drag and drop” позволяет настроить рабочий стол так, как удобно программисту. Теперь можно, например, объединить Code Explorer и Project Manager в одном окне и быстро переключаться между ними. Иногда это даже начинает надоедать. Так что если вы двигаете окно, а оно как назло начинает прилипать к соседним окнам, нажмите и удерживайте Control — и окно перестанет быть липучим (кстати, это относится и к продуктам Microsoft).

  • Добавлено много нового для работы в режиме отладки, включая отладку на удаленной машине, дизассемблер (почему-то под именем CPU view). Усовершенствована работа с точками останова (breakpoints), отладка специфичных sub-menu и прилипающих окон (dockable windows).

  • Расширен набор поставляемых компонентов и библиотек:

  • Появилась поддержка MTS (Microsoft Transaction Server). В дополнение к этому, новые Wizard’ы очень облегчают создание MTS-серверных объектов.

  • Delphi 4 расширяет поддержку создания ActiveX, которая впервые была введена в Delphi 3 (но в споре с C++ Object Pascal уступает своем конкуренту как по размеру исполняемых файлов, так и по качеству поддержки спецификации).

  • VCL дополнена новыми компонентами для создания Служб WindowsКNT (WindowsКNT Service). Плюс к этому, имеется возможность централизовать управление меню и ToolBar’ом, для VCL компонентов расширенна поддержка “drag and drop” и др.

  • Поддержка RTL для 2000 года: глобальная переменная TwoDigitYearCenturyWindow используется функциями StrToDate и StrToDateTime для контроля правильности интерпретации двухзначных лет при преобразовании даты.

  • Поддержка CORBA: Client/Server и Enterprise версии Delphi 4 включают в себя поддержку клиентских и серверных приложений для CORBA. Wizard’ы делают очень легким создание CORBA-сервера, а Dynamic Invocation Interface (DII) позволяет писать клиентов для существующих CORBA-серверов. Поддерживаются также возможности доступа к базам данных при работе с CORBA. Вы можете также создать сервер, который одинаково хорошо поддерживает COM- и CORBA-клиентов.

  • В Delphi4 не забыт и DCOM.

  • Client DataSet позволяет использовать широкий набор фильтров, осуществляет поддержку расширенных агрегатов и допускает типы полей объектно-реляционных баз данных . Сделанные усовершенствования облегчают создание dataset’ов для приложений, работающих с плоско-файловыми (flat-file) базами данных.

  • В Delphi 4 много компонентов для многоуровневых приложений, что облегчает взаимодействие с серверными приложениями. Расширение клиентских dataset’ов упрощает посылку параметров на сервер.

  • Компоненты доступа к базам данных работоспособны уже в процессе разработки. Изменения, внесенные в Borland Database Engine (BDE), делают возможной поддержку: новых СУБД (включая Access’97 и Oracle8), ADT (абстрактные типы данных), массивов, ссылок и вложенных таблиц. Visual Query Builder заменил SQL Builder (если это вам что-нибудь говорит).

  • Требования к свободному месту на диске показаны в табл.1

    Тестирование

    Мы решили попробовать в работе Delphi4 Client/Server (любезно предоставленную нам корпорацией Inprise), чтобы на собственном примере испытать те ее достоинства и недостатки, которые приходится испытывать программистам на Delphi.

    Мы построим три исполняемых модуля: Компонент, Приложение, обращающееся к Серверу и сам Сервер. Поскольку в последнее время широкое распространение получили технологии COM и DCOM, а Inprise заявляет о широкой поддержке этих технологий, то рассмотрим именно эти аспекты работы Delphi4, для чего разделим наш тест как бы на две независимые части:

    Создадим ActiveX-компонент, напрямую (через BDE) связанный с базой данных. Этот ActiveX будет читать данные из таблицы базы данных, отображать их в виде дерева (TreeView) и редактировать (записывать изменения обратно в базу).

    Создадим DCOM – EXE-сервер на удаленной машине, связанный с базой данных, и EXE – клиент, связанный с базой данных через DCOM-сервер. Клиент будет отображать полученные данные в таблице TDBGrid, давать возможность редактировать и обновлять их.

    Приложение, содержащее ActiveX-компонент

    Первым реализуем ActiveX– компонент, который назовем “testTreeViewAX”.

    Запустив Delphi 4.0 выберем в меню File/New, где на странице ActiveX выберем ActiveX Library. При этом Delphi создает новый проект – пустую библиотеку ActiveX компонентов. Лучше сразу сохранить проект, выбрав в меню File/SaveAll. Назовем проект “ptestAXLib.dpr”.

    Теперь добавим в созданную библиотеку новый ActiveX. Для этого выберем в меню File/New, где на странице ActiveX выберем ActiveX Form. В появившемся ActiveForm Wizard’e в поле New ActiveX Form введем имя компонента: testTreeViewAX. Больше ничего изменять не нужно – просто нажмем OK, и продолжим…

    После непродолжительной задумчивости на экране появится форма вновь созданного компонента – во избежание лишних хлопот лучше сразу же сохранить ее с именем “utestTreeViewAX.pas”.

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

    Выберем в меню View/TypeLibrary или в TollBar’e нажмем на кнопку с подсказкой “Register Type Library” (вторая справа на рисунке 1.).

    delphi32.GIF (11810 bytes)

    Рисунок 1.

    Теперь ActiveX с именем “testTreeViewAX” зарегистрирован в системе и доступен для использования другими программами.

    Поскольку созданный ActiveX компонент предназначен для отображения информации из базы данных, добавим в интерфейс ItestTreeViewAX несколько новых методов:

    Connect – выполняет соединение с базой данных;

    CurID_RT – определяет текущую запись (т. е. выделенную ветку дерева) по значению уникального идентификационного номера записи в таблице RT (подробнее о таблице RT поговорим позже).

    Для добавления новых методов щелкнем правой кнопкой мыши на ветке “ItestTreeViewAX” (слева на рис.1) и во всплывающем меню выберем New/Method. Кстати, того же самого можно добиться, нажав на пятую справа кнопку на ToolBar’e (с подсказкой “New method”). Выделенная ветка с именем “Method1” - это и есть только что созданный метод.

    По умолчанию Method1 имеет тип Integer и не имеет параметров. Изменим имя Method1 на Connect и переключимся на закладку “Parameters”. Здесь изменим значение поля “Return Type” на “WordBool” и четыре раза нажмем на кнопку “Add”, чтобы добавить четыре параметра, необходимых для работы Connect. Заменим названия (Name) и соответствующие значения по умолчанию (Default Value) на “ServerName”, “DBName”, “UserName” и “Password”.

    Теперь попробуем изменить тип (поле “Type”) для всех четырех параметров на WideString, сохраняя пустым поле “Modifier”… К сожалению, не получилось: стоит только сместить фокус в соседнее поле, как Delphi аккуратно восстанавливает Integer!!! Однако если установить “Modifier” в “none”, там сменить тип, а затем вернуть в “Modifier” пустое значение, то тип WideStrind успешно установится в поле “Type”. Проще всего на время забыть об удобствах визуальных средств Delphi и, перейдя на закладку “Text”, по старинке, изменить в тексте:

    “function Connect(ServerName:Integer = ‘ServerName’; 
    DBName:Integer = ‘DBName’; UserName:Integer = ‘UserName’; 
    Password:Integer = ‘Password’): WordBool [dispid $0000000F]; safecall;”  

    все Integer на WideString, чтобы получилось:

    “ function Connect(ServerName:WideString = ‘ServerName’; 
    DBName:WideString = ‘DBName’; UserName:WideString = ‘UserName’; 
    Password:WideString = ‘Password’): WordBool [dispid $0000000F]; safecall;”.

    Следующим шагом сохраним все изменения (File/SaveAll) и перерегистрируем ActiveX (кнопка “Register Type Library” на ToolBar’e). Затем выберем “New Property” и добавим свойство CurID_RT. Если вы обратили внимание, появились две ветки с именем “Property1”. Это объясняется просто – мы выбрали “New Property”, что подразумевает создание методов Get и Put. Если переключаться между ними, то в списке “Attributes” (справа) меняется только значение поля Invoke Kind (Property Get – Property Put).

    Сохраним все и перерегистрируем ActiveX – вот и все, что нужно было сделать для создания ActiveX c заглушками для необходимых нам методов. С точки зрения реализации COM работа закончена. Теперь остается только создать выполняемый код для объявленных методов.

    Перейдем в окно редактора на закладку utestTreeViewAX.pas (это можно сделать, например, выбрав в меню View/Code Explorer) и нажмем F12 –появится форма, с которой мы и будем в дальнейшем работать.

    Для тестирования создадим приложение testAXApplication, содержащее в себе наш utestTreeViewAX. Закроем проект (File/Close All), если предложат сохранить изменения – согласимся.

    Откроем новый проект (File/New Application) и сразу скажем ему “Save All”. Назовем файл “utestAXApplication.pas”, а проект “ptestAXApplication.dpr”. Перейдем в “Object Inspector” и изменим свойство Caption формы на более человеческое (например: “Тест ActiveX-компонента “utestTreeViewAX” ”). Свойство Name изменим на “formAXTestApp”.

    Выберем Component/Import ActiveX Control и найдем в появившемся списке ptestAXLib. В списке классов этой библиотеки содержится созданный нами testTreeViewAX, в поле “Palette Page” указано название закладки Delphi, в которую он будет добавлен (по умолчанию: “ActiveX”) – для подключения библиотеки ptestAXLib к Delphi нажмем кнопку “Install”. В открывшемся диалоге указывается название того Package (аналог dll, используемый Delphi для хранения компонентов), в который будет добавлен инсталлируемый компонент – по умолчанию “dclusr40.dpk”. Если все прошло успешно, то в закладке “ActiveX” палитры компонентов Delphi появится новый компонент – testTreeViewAX, который можно узнать по стандартной иконке (по умолчанию это изображение трех геометрических фигур: красного треугольника, синего квадрата и желтого круга) . Подсказка сообщает название компонента.

    Щелкнем левой кнопкой мыши на компоненте, а затем на форме – компонент появился в виде пустого прямоугольника. Если вам не нравится размер – измените его. Проверим, есть ли в списке свойств CurID_RT (для этого надо заглянуть в Object Inspector). Сохраним все, после чего нажмем F9 для компиляции и запуска. Итак, перед нами тестовое приложение, которое включает в себя только что созданный ActiveX – что может быть прекраснее… (ну, разве что бутылочка холодного пива в июльскую жару…)

    Закроем проект с тестовым приложением (File/Close All) и откроем проект библиотеки ptestAXLib (она должна быть в списке File/Reopen). В Run/Parameters установим в качестве Host Application тестовое приложение “ utestAXApplication.exe” (найти его можно с помощью кнопки “Browse”). Запустим проект (F9).

    Вот теперь пора начинять компонент кодом. Сначала реализуем метод Connect – для этого перейдем на закладку Data Access палитры компонентов и сбросим на форму компоненты Query и DataBase. Затем перейдем на закладку Win32 и сбросим на форму компонент TreeView, после чего установим его свойство Align в alClient, чтобы компонент растягивался на всю форму. С этой же закладки сбросим компонент ImageList – выполнив на нем двойной щелчок мыши, перейдем в диалог для заполнения ImageList’a иконками и, с помощью кнопки Add, добавим две иконки (их нетрудно найти, например, в …\Delphi4\Demos\Doc\Graphex\…). Первая будет символизировать открытую ветку в компоненте TreeView1, вторая – закрытую. После этого для компонента TreeView1 установим свойство Images = ImageList1.

    Открыв всплывающее меню правой кнопкой мыши на компоненте DataBase1,выберем DataBase Editor. Набьем в поле Name строку “TestDB” – это будет псевдоним базы данных для Delphi. Установим в поле Driver Name значение “MSSQL” (мы используем в качестве сервера БД MSКSQLКServer, но вы можете попытаться использовать любой другой сервер или даже Paradox), после чего нажмем на кнопку “Default”. Из всех появившихся установок оставим только три:

    DATABASE NAME= введем имя вашей базы данных (физическое имя)

    SERVER NAME= введем имя вашего сервера

    USER NAME= введем ваше имя пользователя (это не обязательно).

    Скажем OK и, выделив компонент DataBase1, перейдем в Object Inspector и установим свойство Connected в True. Если вам это удалось, значит связь с базой данных состоялась…Наши поздравления! Если нет, то вам предстоит веселая ночка, но не отчаивайтесь, ибо тернист путь познания!!! Теперь вернем Connected в False.

    Установим для компонента Query1 в свойстве DatabaseName значение “TestDB” (оно должно быть в выпадающем списке). В свойстве SQL вызовем редактор SQL-запросов (String List Editor – появляется по нажатию на кнопку с тремя точками) и напишем там следующий SQL-запрос:

    SELECT 
       ID_RT, 
       Parent, 
       Name, 
       (SELECT Count(*) 
       FROM RT as RT2 
         WHERE RT2.Parent = RT.ID_RT) as ChildrenCount 
    FROM RT
    WHERE Parent = :Parent AND ID_RT <> Parent 
    ORDER BY Name

    В этом SQL-запросе из таблицы RT вынимаются все записи, которые являются дочерними ветками для выбранной ветки (:Parent), причем для каждой из дочерних веток считается количество вложенных в нее веток (поле ChildrenCount). Записи сортируются по полю Name.

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

    Parent – содержит ID ветки, для которой данная ветка является дочерней (т.е. ID ветки-родителя)

    ID_RT – содержит ID ветки (ID для Root (первой ветки) в таблице RT = 0)

    ImageNum – содержит номер иконки, асоциированной с веткой (в нашем примере не используется)

    Name – содержит название ветки.

    Вот текст SQL/DDL создающий эту таблицу:

    CREATE TABLE RT 
    ( ID_RT int NOT NULL 
        CONSTRAINT RT_PK PRIMARY KEY,
      Parent int NOT NULL,
      ImageNum int NOT NULL, 
      Name varchar(255) NULL
    )

    Примерно так могут выглядеть данные в этой таблице:

    Для нашего примера необходимо, чтобы компонент TreeView (“дерево”) отображал ветки, считанные из базы данных и позволял раскрывать/закрывать ветки, имеющие подветки. Ветка, имеющая дочерние ветки, в компоненте TreeView маркируется плюсом (по нажатию на плюс ветка раскрывается) - поэтому нам необходимо знать количество дочерних веток у каждой отображаемой ветки даже тогда, когда сами эти ветки не показываются. Кроме того, нужно знать ID всех отображаемых веток – иначе как мы будем с ними работать?!

    Значение Parent (ID той ветки, для которой запрашивается список дочерних ветвей) передается в запрос с помощью параметра :Parent (“:” – означает что Parent не является частью SQL запроса, а является программно определяемым параметром). Этот параметр был автоматически создан Delphi в списке Params компонента Query1 после того, как мы поместили его в SQL-запрос. Найдем в Object Inspector’e свойство Params компонента Query1 - в списке параметров будет наш Parent, причем Object Inspector будет отображать его свойства. Установим свойства: DataType в ftInteger; ParamType в ptInput; Value в 0; Value.Type в Integer.

    Скажем OK и установим свойство Active в True. Если все в порядке (нет сообщения об ошибке), значит запрос выполнен - вернем Active в False. Теперь удалим компонент DataBase1 (мы будем создавать его динамически в разделе initialisation). Это позволит первому из создаваемых TtestTreeViewAX создать единственный для всего приложения компонент Database1, который будет удален при уничтожении последнего компонента TtestTreeViewAX (мы поместим удаление DataBase1 в раздел finalisation).

    Перейдем в Code Editor, найдем там заглушку для метода Connect и впишем в нее следующее:

    function TtestTreeViewAX.Connect(const ServerName, DBName, UserName, Password: WideString): WordBool;
    begin
       try
          if not DataBase1.Connected then begin
             DataBase1.Params.Append('DATABASE NAME=' + DBName);
             DataBase1.Params.Append('SERVER NAME=' + ServerName);
             DataBase1.Params.Append('USER NAME=' + UserName);
             DataBase1.Params.Append('PASSWORD=' + Password);
             DataBase1.Connected := True;
          end;
          Query1.SessionName := DataBase1.SessionName;
          //SetRoot();// Эту строку впишем позже
          Result := DataBase1.Connected;
       except
          Result := False;
       end;
    end;

    Все что здесь делается - это заполнение листа параметров компонента DataBase1, которые мы устанавливали раньше с помощью DataDase Editor’a. О вызове метода SetRoot() поговорим позже. try … except необходимы для того, чтобы в случае возникновения исключения функция все же возвратила False.

    Добавим перед разделом implementation следующие строки:

    var
    	DataBase1 : TDataBase;
    	Session1 : TSession;

    Добавим в конец раздела initialization следующий код:

       if DataBase1 = nil then begin
          DataBase1 := TDataBase.Create(nil);
          DataBase1.Connected := False;
          DataBase1.Params.Clear;
          // замените значение следующего параметра, если это необходимо
          DataBase1.DriverName := 'MSSQL'; 	 
          DataBase1.DatabaseName := 'TestDB';
          DataBase1.SessionName := 'testAXSession';
       end;
       Session1 := Sessions.FindSession(DataBase1.SessionName);
       if Session1 = nil then begin
          Session1 := Sessions.OpenSession(DataBase1.SessionName);
       end;
    
    finalization
       DataBase1.Free;
       Session1.Free;

    Пришла пора заставить компонент TreeView считывать и показывать записи из таблицы RT. Для этого напишем несколько процедур.

    Объявим в разделе Protected :

    procedure SetRoot;
    procedure CreateSubNodes(tnNode : TTreeNode);
    function ExpandParentNode(iID_RT : Integer): TTreeNode;

    Добавьте в разделе Implementation:

    procedure TtestTreeViewAX.SetRoot;
    var
       sqlRoot : TQuery;
       tnRoot : TTreeNode;
    begin
       try
          //Создаем динамический Query и получаем имя для Root-ветки
          sqlRoot := TQuery.Create(Self);
          sqlRoot.DataBaseName := Query1.DataBaseName;
          sqlRoot.SessionName := Query1.SessionName;
          sqlRoot.SQL.Text := 'SELECT Name FROM RT WHERE ID_RT = 0';
          sqlRoot.Open;
       except
          // Ключевое слово "Raise" позволяет сгенерировать 
          // исключительную ситуацию.
          Raise Exception.Create('Ошибка при выполнении SQL-запроса ' 
             ' (считывание Root из таблицы RT)');
       end;
       if sqlRoot.RecordCount = 0 then 
       Raise Exception.Create('В таблице RT отсутствует корень (Root)');
       // Создает в TreeView первую ветку	
       tnRoot := TreeView1.Items.AddFirst (nil,'Root'); 
       tnRoot.HasChildren := True; // Указываем, что есть подветки
       tnRoot.Data := Pointer(0); // Запоминаем ID_RT (для Root = 0)
       tnRoot.SelectedIndex := 1;
       tnRoot.Expand(False); // Раскрывает первую ветку (Root)
       sqlRoot.Free; // Уничтожаем динамически созданный Query
    end;
    
    procedure TtestTreeViewAX.CreateSubNodes(tnNode : TTreeNode);
    var
       tnNewNode : TTreeNode;
    begin
       Query1.Close; // Передаем ID раскрывающейся ветки и получаем подветки
       Query1.ParamByName('Parent').AsInteger := Integer(tnNode.Data);
       Query1.Open;
       while not Query1.EOF do begin // Заполняем подветки (текст, плюс, ID) 
       tnNewNode := TreeView1.Items.AddChild(tnNode, 
          Query1.FieldByName('Name').AsString);
       tnNewNode.HasChildren := Query1.FieldByName('ChildrenCount').AsInteger
          > 0;
       tnNewNode.Data := Pointer(Query1.FieldByName('ID_RT').AsInteger);
       tnNewNode.SelectedIndex := 1;
       Query1.Next;
       end;
    end;
    
    function TtestTreeViewAX.ExpandParentNode(iID_RT : Integer): TTreeNode;
    var
       sqlFindID_RT : TQuery;
       i,iNodeCount,iParentID: Integer;
    begin
       Result := nil;
       sqlFindID_RT := TQuery.Create(Self);
       sqlFindID_RT.DatabaseName := Query1.DatabaseName;
       sqlFindID_RT.SessionName := Query1.SessionName;
       // получить ветку, для которой данная является подветкой
       sqlFindID_RT.SQL.Text := 
           Format('SELECT Parent FROM RT WHERE ID_RT = %d',
           [iID_RT]);
       sqlFindID_RT.Open;
       if sqlFindID_RT.RecordCount = 0 then 
          iParentID := -1
       else 
          iParentID := sqlFindID_RT.FieldByName('Parent').AsInteger;
       sqlFindID_RT.Free;
       if iParentID = -1 then Exit;
       if iParentID = 0 then begin //Дошли до корня (Root)   
          Result := TreeView1.Items[0];
          Result.Expand(False);//Раскрыть Root и начать выход из рекурсии
          Exit;
       end;
       //Рекурсивный вызов
       Result := ExpandParentNode(iParentID);
       if Result = nil then Exit; //Выход из рекурсии
       iNodeCount := Result.Count - 1;
       //Найти следующую ветку для раскрытия
       for i := 0 to iNodeCount do begin 
          if Result.Item[i].Data = Pointer(iParentID) then begin
            Result := Result.Item[i];
            Break;
          end;
       end;
       if Result = nil then Exit;
       Result.Expand(False); //Раскрыть найденную ветку
    end;

    Процедура SetRoot считывает из таблицы RT название корневой ветки (root). Если все прошло без ошибок, то в TreeView1 создается первая ветка и ей в ультимативном порядке говорится, что у нее есть дочерние ветки (HasChildren := True). Эта операция приводит к появлению перед веткой плюса. В свойство ntRoot.Data вписывается ID ветки (в данном случае 0), приведенный к типу Pointer. Вообще, TNodeTree.Data является указателем на структуру, которую можно динамически создать и использовать (потом, желательно, не забыть удалить). Такой структурой может быть, например, Record, включающая в себя строку с именем ветки, адресом, телефоном и т.п. Для нашего же примера вполне достаточно использовать само по себе значение Data, не затрудняя себя динамическим выделением-освобождением памяти.

    Свойство SelectedIndex указывает на номер иконки в StringList1, которую следует показывать рядом с веткой при ее выделении (нумерация с 0). Свойство ImageIndex позволяет установить иконку для невыделенной ветки, но мы обходимся без этого, подключив ImageList1 и этим обеспечив отображение про умолчанию первой содержащейся в нем иконки.

    Впишем код “SetRoot();”, вызывающий эту процедуру, в реализацию метода Connect после строки “DataBase1.Connected := True;”.

    Процедура CreateSubNodes будет вызываться при попытке раскрыть ветку. Она получает в качестве параметра TTreeNode (раскрывающаяся ветка), определяет, не была ли она раскрыта ранее и, если не была, то считывает из базы данных все дочерние ветки и устанавливает плюсы имеющим подветки.

    Теперь выделим компонент TreeView1, перейдем на вторую закладку в Object Inspector (Events), где дважды щелкнем мышью на событии OnExpanding. Впишем в появившуюся заглушку код так, чтобы обработка события выглядела следующим образом:

    procedure TtestTreeViewAX.TreeView1Expanding(Sender: TObject;
       Node: TTreeNode; var AllowExpansion: Boolean);
    begin
        if Node.Expanded then Exit;
        CreateSubNodes(Node);
    end;

    Затем аналогичным образом Создадим заглушку для события OnCollapsed и впишем туда следующий код:

    procedure TtestTreeViewAX.TreeView1Collapsed(Sender: TObject;
       Node: TTreeNode);
    begin
        Node.DeleteChildren;
        Node.HasChildren := True;
    end;

    Запустим проект (F9) и убедимся, что дерево работает и действительно отображает содержимое таблицы RT.

    Теперь хотелось бы реализовать свойство CurID_RT (Set и Put). Это свойство позволяет узнать ID выделенной записи или выделить, предварительно раскрыв ветку, в которой она находится. Для этого найдем заглушки для методов Get_CurID_RT и Set_CurID_RT и впишем в них следующий код:

    function TtestTreeViewAX.Get_CurID_RT: Integer;
    begin
       if TreeView1.Selected <> nil then
          Result := Integer(TreeView1.Selected.Data)
       else
          Result := -1; 
    end;
    
    procedure TtestTreeViewAX.Set_CurID_RT(Value: Integer);
    var
       tnParentNode : TTreeNode;
       iOldValue, i, iNodeCount: Integer;
    begin
       // Проверки и поиск ветки с ID = Value.
       // если нет – не выделять никакой
       if not Query1.Active then Exit;
       if Value < 0 then Exit;
       if TreeView1.Selected = nil then 
          iOldValue := -1
       else 
          iOldValue := Integer(TreeView1.Selected.Data);
       if Value = iOldValue then Exit;
       tnParentNode := ExpandParentNode(Value);
       iNodeCount := tnParentNode.Count - 1;
       for i := 0 to iNodeCount do begin
          if tnParentNode.Item[i].Data = Pointer(Value) then begin
             tnParentNode.Item[i].Selected := True;
             Break;
          end;
       end;
    end;

    Функция Get_CurID_RT реализует метод Property Get для свойства CurID_RT и возвращает значение ID текущей (выделенной) ветки. В случае, если никакая ветка не выделена, возвращается отрицательное значение (-1).

    Процедура Set_CurID_RT реализует метод Property Set для свойства CurID_RT и передает ID ветки, на которую следует установить фокус. Если передается неотрицательное значение, и ветка с таким ID существует, то она становится выделенной.

    Процедура ExpandParentNode вызывается для установки выделения на ветку с ID_RT = Value. Это рекурсивная (самовызывающаяся) функция, которая последовательно определяет родительские ветки вверх по дереву до корня, а после этого раскручивает их вниз, вызывая для каждой Expand.

    Сохраним все и переключимся в проект с тестовым приложением ptestAXApplication.dpr и добавим с закладки Standart две кнопки (tButton) и один компонент tEdit. Для всех имеющихся (теперь уже трех) кнопок установим свойства Caption в “Connect”, “Set CurID_RT” и “Get CurID_RT”. Установим компоненту Edit1 свойство Text = 0.

    Двойным щелчком мыши на кнопке “Set CurID_RT” получим заглушку для процедуры нажатия на кнопку и введем в нее следующий код:

    procedure TformAXTestApp.Button2Click(Sender: TObject);
    begin
       testTreeViewAX1.CurID_RT := StrToInt(Edit1.Text);
    end;

    Для кнопки “Get CurID_RT” поступим аналогично и введем:

    procedure TformAXTestApp.Button3Click(Sender: TObject);
    begin
       Edit1.Text := IntToStr(testTreeViewAX1.CurID_RT);
    end;

    Из обработки события Button1Click (для кнопки “Connect”) уберем лишние строчки сообщений, чтобы код выглядел так:

    procedure TformAXTestApp.Button1Click(Sender: TObject);
    var
       Value : WordBool;
    begin
       testTreeViewAX1.Connect('Имя сервера','Название базы данных',
          'Имя пользователя', 'Пароль', Value);
       // Не забудьте заменить параметры на имеющиеся в 
       // вашем случае
    end;

    Запустим проект и убедимся, что при нажатии на кнопку “Get CurID_RT” в поле Edit1 появляется ID выделенной ветки, а при нажатии на кнопку “Set CurID_RT” выделенной становится ветка, ID которой записан в поле Edit1.

    Если где-то на переключении проектов Delphi вылетит с сообщением типа “access violation in vcl40.bpl” или каким-нибудь еще — не расстраивайтесь! Перезагрузки Windows, как правило, хватает для восстановления работоспособности. Правда, бывают случаи, когда проект безнадежно портится и отказывается работать в Delphi — тогда остается только сожалеть, что вы вовремя не вспомнили о резервном копировании. Как правило, такие проблемы возникают при переключении от проекта с ActiveX-библиотекой к проекту с тестом этого ActiveX’a.

    Теперь, когда компонент научился сносно отображать содержимое таблицы RT, не лишним будет научить его изменять эти данные. Сбросим на форму компонент PopupMenu с закладки Standart. Установим компоненту TreeView1 в свойстве PopupMenu значение “PopupMenu1” (оно должно быть в выпадающем списке для этого свойства) — после этого с нашим компонентом TreeView будет ассоциировано всплывающее по правой кнопке мыши меню PopupMenu1.

    Выделим компонент PopupMenu1 и дважды щелкнем по нему левой кнопкой мыши. В появившейся форме пока только один элемент – установим его свойства следующим образом: Name = “miAdd”; Caption = “Добавить”. Добавим следующий элемент и установим его свойства в: Name = “miDelete ” и Caption = “Удалить”.

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

    Объявим в разделе Protected:

    procedure AddNewNode;
    procedure DeleteNode;

    Опишем в разделе Implementation :

    procedure TtestTreeViewAX.AddNewNode;
    var
      iParent : Integer;
      tnNewNode, tnParentNode : TTreeNode;
      sqlAddNewNode : TQuery;
    begin
       // если нет выделенной ветки – выйти
       // иначе взять ее за Parent
       tnParentNode := TreeView1.Selected;
       if tnParentNode = nil then Exit;
       iParent := Integer(tnParentNode.Data);
       // Получить ID и добавить новую ветку 
       // в подветки к Parent в базе данных
       sqlAddNewNode := TQuery.Create(Self);
       sqlAddNewNode.DatabaseName := Query1.DatabaseName;
       sqlAddNewNode.SessionName := Query1.SessionName;
       // !!! Следующий SQL код можно выполнить только на 
       // MS SQL Server или Sybase SQL Server!
       // (может быть его поймет Sybase SQL Anywhere),
       // для других серверов или локальных СУБД вам
       // придется переписать запрос, а возможно даже
       // разбить его на несколько промежуточных вызовов.
       // При разбиении вам понадобятся промежуточные переменные…
       // Главный недостаток такого разбиения заключается в потере
       // производительности, так как самые большие накладные расходы
       // при работе с SQL-сервером возникают в момент "общения" 
       // сервера с клиентом.
       sqlAddNewNode.SQL.Text := format(
         // объявление переменной в SQL скрипте
         'DECLARE @NewID_RT int ' +
         // получаем уникальное (для таблицы RT) значения поля ID_RT
         // вы можете заменить эту строку на более совершенную реализацию
         'SELECT @NewID_RT = MAX(ID_RT) + 1 FROM RT ' +
         // добавляем новую запись к таблице RT
         'INSERT INTO RT VALUES(%d, @NewID_RT, 0, '+
         // функция Convert преобразует значение
         // заданное во втором параметре к типу
         // данных, указанному в первом параметре
         '"Новая запись №" + Convert(varchar(15), @NewID_RT)) ' +
         // SQL Server не поддерживает обратно возвращаемые параметры,
         // но зато может в одном запросе как модифицировать данные,
         // так и возвращать набор записей. Этим мы и воспользуемся.
         'SELECT @NewID_RT' ,[iParent]);
       sqlAddNewNode.Open;
      // Если запрос не выполнится, то произойдет программное исключение
      // и дальнейший код не выполнится…
    
      // Добавить новую ветку в подветки к Parent 
      // в TreeView и заполнить ее.
      tnNewNode := TreeView1.Items.AddChild(tnParentNode, 
         'Новая запись №' + sqlAddNewNode.Fields[0].AsString);
      tnNewNode.Data := Pointer(sqlAddNewNode.Fields[0].AsInteger);
      tnNewNode.SelectedIndex := 1;
      sqlAddNewNode.Free;
      tnParentNode.HasChildren := True;
      // Раскрыть ветку Parent и выделить новую ветку
      tnParentNode.Expand(False);
      tnNewNode.Selected := True;
    end;
    
    procedure TtestTreeViewAX.DeleteNode;
    var
      iID_RT : Integer;
      tnDeletingNode : TTreeNode;
      sqlDeleteNode : TQuery;
    begin
      // Если нет выбранной ветки, то выйти
      tnDeletingNode := TreeView1.Selected;
      if tnDeletingNode = nil then Exit;
      if tnDeletingNode.Parent = nil then Exit; //нельзя удалять Root
      // получить ID выбранной ветки и удалить ее из базы
      iID_RT := Integer(tnDeletingNode.Data);
      sqlDeleteNode := TQuery.Create(Self);
      sqlDeleteNode.DatabaseName := Query1.DatabaseName;
      sqlDeleteNode.SessionName := Query1.SessionName;
      sqlDeleteNode.SQL.Text := format(
         'DELETE FROM RT WHERE ID_RT = %d ',[iID_RT]);
      sqlDeleteNode.ExecSQL;
      sqlDeleteNode.Free;
      // удалить ветку из TreeView и обновить Query1
      tnDeletingNode.Delete;
      Query1.Close;
      Query1.Open;
    end;

    В процедуре DeleteNode вместо TQuery.Open при отсутствии ResultSet’a (когда SQL-запрос не возвращает данных) используется TQuery.ExecSQL, т.к. этот метод предназначен именно для такого случая. Вообще-то, это неудобно, если заранее неизвестно, будет ли возвращен набор записей.

    Добавим вызов процедуры AddNewNode в заглушку для события miAddClick (которую создадим, дважды щелкнув мышью на событии “onClick” закладки “Events” Object Inspector’a). Вызов процедуры DeleteNode вставим в обработчик события miDeleteClick. После этого в коде появятся следующие строки:

    procedure TtestTreeViewAX.miAddClick(Sender: TObject);
    begin
       AddNewNode();
    end;
    
    procedure TtestTreeViewAX.miDeleteClick(Sender: TObject);
    begin
       DeleteNode();
    end;

    Осталось одно маленькое неудобство, которое может со временем превратиться в большую неприятность – если вызывать всплывающее меню нажатием правой кнопки мыши не на выделенной ветке дерева, то выделенной остается другая ветка, и все добавления-удаления будут относиться к ней. Устраним это безобразие: первым делом установим компоненту TreeView1 свойство RightClickSelected = True. Попробуем запустить проект и не удивиться – выделение действительно временно переходит в ветку, на которой нажата правая кнопка мыши, но выделенной по-прежнему считается предыдущая ветка (если было установлено свойство HotTreck = True, то будет виден “след” на записи, имеющей в этот момент фокус). Собственно, удивительно не то, что выделение не меняется автоматически, а то, насколько колдовские действия необходимо сделать для того чтобы передать фокус выделенной записи.

    Сейчас мы выполним магическую операцию, на первый взгляд полностью лишенную смысла. Для этого сначала создадим заглушку для обработки события OnPopup компонента PupupMenu1 и добавим в него следующий код:

    procedure TtestTreeViewAX.PopupMenu1Popup(Sender: TObject);
    begin
       if TreeView1.Selected = nil then begin
         miAdd.Enabled := False;
         miDelete.Enabled := False;
       end else begin
         miAdd.Enabled := True;
         miDelete.Enabled := True;
         // Это работает !!! 
         // Воистину шедевр объектно-ориентированного программирования 
         TreeView1.Selected := TreeView1.Selected;
       end;
    end;

    Первая строчка защищает от случая, когда TreeView1.Selected отсутствует, и в этом случае делает невозможным вызов процедуры AddNode или DeleteNode, ибо элементы всплывающего меню переведены в состояние Disabled. Если выделенная ветка существует, эти элементы возвращаются в состояние Enabled.

    А вот следующая строчка (TreeView1.Selected := TreeView1.Selected;) действительно интересна… На самом деле, если посмотреть в исходные тексты VCL, то становится понятно: вызов метода Set_Selected тянет за собой вызов Set_Focused и т.д. и т.п… Остается не до конца проясненным вопрос, почему это приходится делать именно так? Хорошо еще, что эту гениальную операцию Delphi не оптимизирует в процессе компиляции!

    Создадим обработчик события onEdited для компонента TreeView1 — чтобы можно было изменять название ветки. После этого нужно добавить свой код в заглушку для события onEdited. Напишем:

    procedure TtestTreeViewAX.TreeView1Edited(Sender: TObject; Node: TTreeNode;
       var S: String);
    var
       sqlUpdateNodeName : TQuery;
    begin
       sqlUpdateNodeName := TQuery.Create(Self);
       sqlUpdateNodeName.DatabaseName := Query1.DatabaseName;
       sqlUpdateNodeName.SessionName := Query1.SessionName;
       sqlUpdateNodeName.SQL.Text := format(
          'UPDATE RT SET Name = "%s" WHERE ID_RT = %d',
          [S,Integer(Node.Data)]);
       sqlUpdateNodeName.ExecSQL;
       sqlUpdateNodeName.Free;
       Query1.Close;
       Query1.Open;
    end;

    Теперь при изменении названия ветки дерева в компоненте TreeView1 будет модифицирована соответствующая запись в таблице RT, после чего обновится содержимое Query1.

    На этом первый этап закончен — запустим проект и насладимся его работой в полной мере! После того, как вам это надоест, мы перейдем к следующей части Марлезонского балета, а именно к DCOM: создадим клиентское приложение для отображения и редактирования данных и серверное приложение, который будет выполнять всю работу с базой данных и через некоторый набор методов взаимодействовать с приложениями-клиентами .

    Многоуровневое приложение

    Создадим в Delphi новый проект (File/New Application) и изменим для его главной формы свойство Name c “Form1” на “Server”.

    C помощью File/New откроем меню “New Items”, где на закладке “Multitier” выберем элемент “Remote Data Module”. ClassName назовем “testDCOM_server” и, не меняя больше никаких настроек, нажмем OK - только что мы создали DataModule для DCOM-сервера. Сбросив на него компоненты Query и Database с закладки Data Access палитры компонентов Delphi. для компонента Query 1 в свойство SQL впишем запрос: “SELECT * FROM Ref ORDER BY Name”

    Примечание: Здесь используется таблица Ref, находящаяся на сервере баз данных (MS-SQL server 6.5). Таблица Ref состоит из двух полей: ID_Ref, в котором хранятся ID записей, и Name с их названиями. Ниже представлен SQL-запрос, создающий таблицу Ref и устанавливающий уникальность для поля ID_Ref:

    CREATE TABLE Ref 
       (ID_Ref int NOT NULL, 
       Name varchar(255) NOT NULL)
    CREATE UNIQUE INDEX XPKRef ON Ref (ID_Ref)

    Если вы желаете разместить свой DCOM-сервер на той же машине, где установлена Delphi, в этом примере можете не связываться с SQL-Server’ом, а обращаться к демонстрационной базе данных, которая автоматически создается при инсталляции Delphi. В этом случае компонент DataBase вам не нужен, а компоненту Query следует указать DatabaseName = DEMO и TableName = Counters. Вместо компонента Query для работы с содержимым таблицы можно использовать компонент Table – такой подход имеет ряд ограничений, но вполне может быть применим для случаев, когда в одном ResultSet’e содержатся данные только из одной таблицы.

    Свойство RequestLife для Query1 установим в True: это делает ResultSet (результат запроса) редактируемым. Кстати, такой подход сработает только для простых запросов по единственной таблице – в более сложных случаях следует подключать компонент UpdateSQL. Можно также записывать изменения в базу данных из других компонентов TQuery с использованием SQL-выражений “UPDATE…”, “DELETE…” и “INSERT…”. Тогда желательно не забыть выполнить Refresh для отображаемого Query, для обновления ResultSet. Компонент UpdateSQL тоже имеет свои причуды. Так, при его использование невозможно добиться записи при сходе с отредактированной строки. UpdateSQL поддерживает только пакетную запись и перечитывает данные с сервера после того как вы вызовете ApplyUpdates и CommitUpdates.

    Примечание: Если вы программируете на Delphi 2-4 и используете в своей работе SQL-сервер, то могли наблюдать следующую ситуацию – при больших (начиная с 500 записей) выборках, первые записи показываются почти моментально и сразу же становятся доступными для изменения, но при попытке записать внесенных изменений в базу данных возникает ощутимая задержка. Эта задержка тем ощутимей, чем больше выборка, а на выборке порядка ста тысяч записей дождаться окончания работы практически невозможно (Не всегда можно заранее определить размер выборки - например, запрос к Alta Vista может возвратить и миллионы вхождений, но не зависнете же вы из-за этого!). Такого же результата можно добиться при попытке переместится в середину выборки (например с помощью движка прокрутки компонента TDBGrid). Эта проблема связанна с тем что многие серверы (в их число входит и используемый нами MS SQL Server) не могут возвращать более одной выборки на одном подключении одновременно. Данная проблема, очевидно, известна и разработчикам Delphi. Они нашли простое, универсальное, но к сожалению, весьма непроизводительное решение. В случае, если вы пытаетесь выполнить следующий запрос (на одних и тех же компонентах TSession и TDatabase), они попросту вытягивают содержимое предыдущей выборки на клиентскую машину и кэшируют ее в скрытой таблице Paradox. Как же обойти эту неприятную ситуацию?

  • Не делать больших выборок (без комментариев).

  • Ограничить размер выборки параметрами BDE (самый простой выход).

  • Создавать для потенциально больших выборок отдельные компоненты TSession и TDatabase. (самый ресурсоемкий способ и к тому же самый трудоемкий).

  • Отказаться от использования штатных средств Delphi. Например MS SQL Server имеет в своем арсенале так называемые курсоры…но к этой проблеме мы надеемся вернуться в одном из следующих номеров журнала. (самый радикальный способ J)

  • Выделив компонент Query1, щелкнем правой кнопкой мыши, чтобы вызвать всплывающее меню, и выберем “Export Query1 from data module”. Это приведет к созданию метода “Query1” для DCOM-сервера (теперь этот метод можно обнаружить в Type Library), который будет использоваться для передачи в клиентское приложение информации о поставщике данных (Provider). Реализация метода “Query1” также автоматически встраивается в код проекта:

    function TtestDCOM_server.Get_Query1: IProvider;
    begin
      Result := Query1.Provider;
    end;

    Все остальные установки для Query1 и Database1 выполним динамически – для этого откроем TypeLibrary (View/TypeLibrary) и добавим в интерфейс “ItestDCOM_server” новый метод (New Method): “Connect”.

    Добавим для него четыре параметра (как мы это делали раньше при создании ActiveX-компонента “TtestTreeViewAX” для его метода “Connect”) и установим Return Type в WordBool. Нажмем на кнопку “Refresh Implementation” для обновления кода и создания заглушки. Если вдруг, по каким-то причинам ни заглушки, ни объявления для метода Connect в коде не появилось (а это иногда бывает), то добавим их вручную, после чего впишем в заглушку следующий код:

    В var перед разделом Implamentation объявим глобальную переменную

    iDBCounter : Integer = 0;

    В разделе Implementation добавим:

    function TtestDCOM_server.Connect(const ServerName, DBName, User,
       Password:_WideString): WordBool;
    begin
       try
         if Database1.Connected then Database1.Connected := False;
         Database1.Params.Clear;
         Database1.DriverName := 'MSSQL';
         Database1.DatabaseName := Format('TestDCOM_DB_%d', [iDBCounter]);
         Inc(iDBCounter);
         Query1.DatabaseName := Database1.DatabaseName;
         Database1.LoginPrompt := False;
         Database1.Params.Append('DATABASE NAME='+ DBName);
         Database1.Params.Append('SERVER NAME=' + ServerName);
         Database1.Params.Append('USER NAME=' + User);
         Database1.Params.Append('PASSWORD=' + Password);
         Database1.Connected := True;
         Result := Database1. Connected;
    except
         Result := False;
       end
    end;

    В этой функции устанавливаются параметры для Database1 и Query1, а также дается новое условное название базы данных DatabaseName (по сути, это – Alias) для нового соединения с базой данных.

    Сохраним и запустим проект – при этом DCOM-сервер будет создан и зарегистрирован в системе.

    Откроем менеджер проектов (View/Project Manager) и добавим новый проект – это будет клиентское приложение. Изменим название новой формы (ее свойство Name) на “Client”.

    Сбросим на форму клиента компонент DCOMConnection с закладки Midas и установим его свойство ServerName в “ptestDCOM_server.testDCOM_server” (это название должно быть в выпадающем списке). Затем сбросим туда же компонент ClientDataSet и установим ему RemoutServer = DCOMConnection1, ProviderName = Query1.

     delphi4.gif (4664 bytes)

    Рисунок 2

    Сбросим на клиентскую форму компонент DataSourse с закладки Data Access и установим ему свойство DataSet в ClientDataSet1 (это название должно быть в выпадающем списке). Добавим компонент TDBGrid с закладки DataControls и установим ему свойство DataSource в DataSource1, а Align в alBottom.

    Сбросим на форму 5 компонентов Edit с закладки Standart, а потом добавим еще две кнопки (Button) с этой же закладки. Первой кнопке установим свойство Name в Connect, второй в Update. Компонентам TEdit присвоим следующие имена (Name): DCOMServerComputerName, ServerName, DataBaseName, UserName, Password. Для Password установим свойство PasswordChar в “*” (чтобы пароль заменялся звездочками). Изменим свойство Text для этих компонентов в соответствии с нашими установками по умолчанию (имя сервера, базы данных и т.д.).

    Щелкнем дважды на кнопке Connect и в появившуюся заглушку впишем код:

    procedure TClient.ConnectClick(Sender: TObject);
    begin
      DCOMConnection1.ComputerName := DCOMServerComputerName.Text;
      DCOMConnection1.Connected := True;
      DCOMConnection1.AppServer.Connect(ServerName.Text,
      DataBaseName.Text, UserName.Text, Password.Text);
      ClientDataSet1.Active := True;
    end;

    DCOMConnection1.ComputerName – это сетевое имя компьютера, на котором расположен DCOM-server. Если ввести пустое имя, то поиск DCOM-server’a будет производиться в соответствии с настройками DCOM на той машине, с которой запускался клиент.

    Щелкнем дважды на кнопке Update и в появившуюся заглушку впишем код:

    procedure TClient.UpdateClick(Sender: TObject);
    begin
      if ClientDataSet1.ApplyUpdates(-1) <> 0 then
        ClientDataSet1.CancelUpdates;
      ClientDataSet1.Refresh;
    end;

    Теперь, если запустить проект и выполнить Connect, то в компоненте DBGrid появится список записей из таблицы базы данных. Записи можно добавлять, удалять и редактировать… однако изменения не будут записаны в базу данных до тех пор, пока вы не нажмете на Update. При этом будет сделана попытка записать все измененные данные из ClientDataSet1’a таблицу базы данных: если количество ошибок при выполнении ApplyUpdate превысит установленное в параметре значение, то Update будет отменен (для значения –1 отмена происходит при первой же ошибке). Ошибки возникают, в основном, если запись или записи, присутствующие в ClientDataSet1 были измены другим пользователем или сработало ограничение на сервере (ссылочная целостность, триггера и т.п.). Чтобы каким-либо способом обойти это препятствие, используем событие OnReconcileError для ClientDataSet1. Создадим заглушку для обработчика этого события (закладка Events в Object Inspector’e) и впишем туда такой код:

    procedure TClient.ClientDataSet1ReconcileError(DataSet: TClientDataSet;
      E: EReconcileError; UpdateKind: TUpdateKind;
      var Action: TReconcileAction);
    begin
      if MessageDlg(‘Отказ при попытке записи в базу данных:’
        + #10#13 + E.Message + #10#13
        + ‘Перечитать эту запись (записи) из базы?’,
        mtWarning,[mbYes, mbNo],0) = mrYes 
      then Action := raRefresh
      else Action := raAbort;
    end;

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

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

    ClientDataSet1.Close;
    ClientDataSet1.Open;

    Однако, почему-то в этом случае ClientDataSet1 получает значения, не соответствующие содержимому базы данных. Они могли быть введены другими или тем же самым клиентом, но ранее – сейчас в базе другое значение, в чем не трудно убедиться с помощью какого-нибудь средства доступа к данным!). Несомненно, существует какой-нибудь документированный способ обновить содержимое ClientDataSet1, но потратив некоторое время на перерывание документации, мы остановились на простом варианте. Создаем дополнительный метод для интерфейса ItestDCOM_server DCOM-сервера (через TypeLibrary), называем его Refresh и заполняем кодом:

    Query1.Close; Query1.Open;

    Добавляем на клиентскую форму еще одну кнопку, называем ее Refresh и на OnClick вызываем:

    procedure TClient.RefreshClick(Sender: TObject);
    begin
       DCOMConnection1.AppServer.Refresh;
       ClientDataSet1.Refresh;
    end;

     

    Вот теперь Refresh заработал как надо, можно запустить несколько клиентов на разных машинах (или на той же машине) и поработать с ними (на рис.3 – DCOM клиент и сервер, запущенные на одной машине).

    Чтобы не было никаких проблем, запустим из каталога System32 (который находится в системном каталоге WindowsNT4) программу DCOMcnfg.exe и настроим в ней доступ к своему объекту “testDCOM_server”. Для этого найдем его в списке и, нажав “Properties” на закладке “Sequrity” откроем его всем пользователям на полный контроль (“Everyone” с правами “AllowAccess” или “FullControl”).

    Чтобы попробовать запустить сервер на другой машине (на которой не установлена Delphi), сначала установим туда BDE. Для этого проще всего выполнить Custom-инсталляцию Delphi, где выбрать только BDE и SQL-links для используемого сервера баз данных. Кстати, чтобы не мучиться с дополнительными динамическими библиотеками Delphi, последнюю компиляцию проекта проведем с выключенным режимом “Build with runtime package” (он устанавливается в Component/Install Packages). Затем скопируем testDCOM_server.exe на эту машину, запустим его первый раз, чтобы зарегистрировать (если не получится, выполним команду “testDCOM_server.exe /regserver”). Затем попробуем запустить клиента (указав в поле “DCOMServerComputerName” имя компьютера, на котором установлен DCOM сервер). После того, как нажав на кнопку Connect в клиентском приложении, получим от Delphi сообщение: “Error loading type library/DLL” J… скопируем с той машины, где установлена Delphi4, библиотеки DBCLIENT.DLL и STDVCL40.DLL в каталог System32 – вот такое дополнительное действие пришлось применить для реального запуска DCOM (к сожалению, мы не имели свободного месяца для внимательного изучения документации – возможно, этот случай там подробнейшим образом описан… но за четыре часа, отведенные нами на тестирование, мы не смогли найти ответа и … пошли методом научного тыка, перепробовав все DLL библиотеки, перечисленные в теле EXE файла).

    Кстати, в поставке Delphi4 есть прекрасный AVI, демонстрирующий последовательность действий при создании DCOM-клиент-сервера: чрезвычайно наглядное пособие, очень способствовавшее ускорению нашей работы над тестом.

    Посмотрим теперь, каков результат нашего DCOM-теста с точки зрения объемов памяти, занимаемых приложениями. При отключенном режиме “Build with runtime package” (включение этого режима приводит к необходимости таскать за собой “кучу” дополнительных библиотек и увеличит объем занимаемого на диске места) объем необходимых для работы файлов занял (округленно):

    pDCOM_client.exe 600 Kb
    ptestDCOM_server.exe 650 Kb
    dbclient.dll 200 Kb
    stdvcl40.dll 450 Kb
    BDE/SQL-links (MSSQL) 15700 Kb
    Итого: 17600 Kb

    Эти цифры следует пояснить: во-первых реальный размер необходимого BDE для одного конкретного выбранного вами сервера значительно меньше (где-то 1500-3000 Kb), и если вы будете устанавливать BDE вручную, то, возможно, приблизитесь к этой величине. Кроме того, не забывайте, что на машинах-клиентах будет установлен только pDCOM_client.exe – все остальное находится на серверной машине.

    При загрузке DCOM-сервер занимает около 750 Kb, DCOM-клиент, запущенный на той же машине добавляет примерно 800 Kb. следующий DCOM-клиент на той же машине съедает еще 800 Kb (поскольку мы создавали проект без “runtime package”, все библиотеки подгружаются заново для каждого нового клиента). При выполнении Connect и получении на клиента ResultSet’a было занято еще 7900 Kb – вероятно, отчасти из-за подгрузки dbclient.dll и stdvcl40.dll, из-за размеров самого ResultSet’a (результат запроса в нашем случае содержал 1074 записи по два поля в каждой, причем первое поле типа Integer, а второе – varchar(250)), а в основном, вероятно, из-за подгрузки BDE. При получении нового Result Set’a после выгрузки-загрузки клиента было занято 800Kb, что подтверждает это предположение. Конечно, эти цифры (по занимаемым объемам оперативной памяти для NT) не претендуют на высокую точность, но характеризуют порядок значений.

    Delphi — воистину выдающийся продукт. Можно только удивляться трудолюбию и изобретательности программистов фирмы Inprise. Жаль, что в погоне за оличеством разнообразных средств и свойств слегка упущен был момент надёжности. Порой работа с Delphi похожа на экстремальный вид спорта — увлекательно, но опасно.


    Copyright © 1994-2016 ООО "К-Пресс"