Доступ к базе данных
После того, как мы инициализировали сессию связи с Lotus Notes, мы можем обращаться к любым серверам Lotus Domino и базам данных на них. Принципиально получить доступ к БД Lotus Notes можно 2 способами – либо обратиться к текущей базе данных, открытой в Lotus Notes, либо вызвать соответствующий метод NotesSession и открыть любую другую БД []. Последний случая является наиболее востребованным, поэтому рассмотрим его:
procedure TMyButtomClick(Sender: TObject); var MyServer: string; begin // Необходимо вычислить имя сервера, // на котором находится необходимая нам БД MyServer:=... // Теперь открываем БД – например, откроем адресную книгу сервера MyLNDataBase:=MySession.GetDataBase(MyServer, ‘names.nsf’); end;Мы в своей работе используем реестр Windows, в котором храним имя сервера Domino по умолчанию, к которому подключаются приложения. Для упрощения разработки и администрирования программ мы использую функцию GetDefaultServerName, которая работает по следующему алгоритму:
При вызове функции она проверяет наличие определенного ключа в реестре Windows. Если этот ключ отсутствует или он содержит пустое значение, функция обращается к файлу notes.ini и считывает из него значение параметра почтового сервера Notes. Имя сервера записывается как значение ключа Windows Если ключ в реестре Windows имеется и не содержит пустое значение, функция считывает его и возвращает в качестве ответа.Таким образом, при первом старте приложения оно самостоятельно выполняет настройку реестра Windows на подключение к текущему серверу Domino. Если в дальнейшем требуется перенаправить работу приложения на другой сервер, то либо пользователь при помощи встроенных в приложение визуальных средств, либо администратор сети вручную меняют значение ключа. Т.о. со следующего запуска начинает обращаться к другому серверу – обеспечивает поддержка мультисерверной конфигурации информационной системы.
Инициализация сессии.
Основной принцип в написании программ состоит в использовании встроенных классов Lotus Notes в коде программ. Для этого в первую очередь необходимо инициализировать сессию связи с Lotus Notes. Для этого требуется, чтобы клиентское программное обеспечение Lotus Notes было инсталлировано на каждом компьютере, использующим программу и подключено к одному или нескольким серверам Domino.
Создадим новое приложение. В разделе uses главного окна приложения укажем ComOBJ – это библиотека, позволяющая вызывать и обращаться к OLE -объектам [].
В разделе public объявим переменные, общие для всего приложения:
public { Public declarations } MySession : OLEVariant; // текущая сессия Lotus Notes MyLNDataBase : OLEVariant; // база данных Lotus Notes...Теперь необходимо написать обработчик события OnCreate главной формы приложения, в котором мы должны создать объект NotesSession, чтобы, используя его в дальнейшем, иметь возможность в рамках одного приложения обращаться сразу к нескольким базам данных, серверам, документам, представлениям и т.д. Обработчик должен иметь следующий вид:
procedure TfmMain.FormCreate(Sender: TObject); begin MySession:= createOLEObject('Notes.Notessession'); if varisempty(MySession) then begin ShowMessage('Не могу создать сессию с сервером Lotus Notes'); Exit; end; end;Следует иметь в виду, что в рамках одного приложения следует только один раз инициализировать объект Notessession, т.к. каждая последующая инициализация будет закрывать предыдущую сессию, а все объекты, созданные на основе этой сессии, потеряют свою актуальность и их обработка будет невозможна.
Литература:
Гусев А.В., Дмитриев А.Г. Microsoft SQL Server против MySQL в медицинских информационных системах. Гусев А.В., член-корр. РАМН Дуданов И.П., Романов Ф.А., Дмитриев А.Г. Особенности в проектировании и практической разработке медицинской информационной системы. Линд Дебби, Керн Стив. Lotus Notes и Domino R5. Энциклопедия пользователя: Пер. с англ. /Дебби Линд, Стив Керн. – К.: Издательство «ДиаСофт», 2000. – 656 с. Кэнту М. Delphi 6 для профессионалов. – СПб.: Питер, 2002. – 1088 с.
Об авторах:
– к.т.н., ст. инженер-программист вычислительного центра ОАО "Кондопога"
– инженер-программист вычислительного центра ОАО "Кондопога"
– инженер-программист вычислительного центра ОАО
Работа с базой данных
Из программы, написанной в Borland Delphi, доступны практически все свойства и методы, предусмотренные разработчиками Lotus Notes / Domino. В том числе Вы можете осуществлять навигацию по представлениям, осуществлять поиск документов в базе данных, в том числе и гипертекстовый поиск и т.д. Особенностей по работе с базой данных вследствие использования Delphi мы не обнаружили. Поэтому в качестве примера приведем фрагмент кода, осуществляющий последовательный перебор и считывание документов из коллекции документов NotesDocumentCollection базы данных адресной книги сервера.
procedure TfmMainWindow.BitBtn1Click(Sender: TObject); var DocumCount: longint; // количество документов в коллекции i : longint; // шаг цикла B1: OLEVariant; // переменная для объекта NotesDatabase BodyQuery: ansistring; C1: OLEVariant; // переменная для объекта NotesDocumentCollection D1: OLEVariant; // переменная для объекта NotesDocument begin DocumCount:=0; // Получаем доступ к БД. B1:= MySession.GetDatabase(GetDefaultServerName,'names.nsf'); BodyQuery:='Form = "Person"'; // Для поиска используем специальную функцию LNSearch C1:=LNSearch(MySession,B1,’Пример запроса’,BodyQuery); DocumCount:=C1.Count; if DocumCount=0 then Exit; // искомые документы не найдены D1:=C1.GetFirstDocument; for i:=1 to DocumCount do begin.... здесь осуществляется обработка документа D1:=C1.GetNextDocument(D1); end; end;В этом примере программа обращается к текущему серверу и открывает на нем базу данных адресной книги. Затем, используя специально разработанную функцию LNSearch, производит поиск документов в базе данных. Если не найдено ни одного документа, то работа процедуры завершается. Если какие-то документы найдены, то они последовательно обрабатываются в цикле. Применение специальной функции LNSearch обусловлено тем, что стандартный метод Search в классе NotesDatabase, кроме формулы для поискового запроса, требует передать дату самого старого документа, который этот запрос сможет вернуть в качестве результата. При этом дата должна быть передана не в качестве переменной типа TDate или TDateTime, а в качестве OLEVariant -переменной, созданной как объект класса NotesDataTime.
function LNSearch(LNSession, LNDataBase: OLEVAriant; Logo: string;query: string):OLEVariant; var r1:WideString; r2: OLEVariant; r3: Smallint; C1: OleVariant; begin r1:=query; r2:=LNSession.CreateDateTime('01.01.1990'); // здесь может быть любая дата r3:=0; C1:=LNDataBase.SEARCH(r1,r2,r3); Result:=C1; end;Отметим, что по нашим наблюдениям, при написании программ в Borland Delphi следует стремиться использовать навигацию по представлениям вместо использования метода search. При этом скорость обработки одной и той же коллекции документов, полученной из представления, примерно на 40% выше, чем при обработке документов, полученных поиском в базе данных.
Работа с документами
В отличие от работы с базами данных, обработка документов Lotus Notes имеет массу подводных камней. При этом в программах приходится выполнять много одинаковых операций, которые целесообразно выделять в отдельные функции. Вначале рассмотрим стандартную функцию считывания текстового значения поля из документа.
function LNGetFieldStrValue(Document: OLEVariant; FieldName: string; DefaultValue: string): AnsiString; var SendValue, RetValue: OLEVariant; TmpS: Ansistring; MyPos: integer; begin TmpS:=' '; if FieldName<>'' then begin SendValue:=FieldName; if not varisempty(Document) then begin Try RetValue:=Document.HasItem(FieldName); except begin RetValue:=false; end; // do end; // Try if RetValue then begin RetValue :=Document.GetFirstItem(SendValue); try TmpS:=RetValue.Text; except TmpS:=DefaultValue; end; end else TmpS:=DefaultValue; end else TmpS:=DefaultValue; // varisempty chek end else TmpS:=DefaultValue; if TmpS='' then TmpS:=DefaultValue; Result :=tmpS; end;Эта простая функция позволяет значительно упростить написание программ, особенно в случае, когда документ содержит большое количество полей, значения которых необходимо считать и обработать. Необходимо отметить, что очень часто в полях документов Lotus Notes хранится несколько значений или значения записаны с символами, препятствующими корректной работе со строками в Borland Delphi. Мы в таком случае используем перегруженную версию представленной функции, которая может возвращать «очищенную» строку или определенную подстроку [].
По аналогии с представленным примером разработаны и версии для числовых полей, а также полей с датой или временем, что представляет особую трудность при написании программ. В таком случае, кроме стандартных проверок на корректность документа, наличия в нем указанного поля, функция может проверить тип возвращаемого значения или выполнить необходимые преобразования. Например, возможна конвертация строкового значения в число или смена символа разделителя разрядов на основании текущих настроек операционной системы.
Несколько усложненный пример – это считывание значения поля в профайле. Как известно, Lotus Notes, кроме стандартных документов, позволяет поддерживать хранение информации в т.н. профайлах – документах, к которым можно обратиться по имени формы и, как дополнение, по имени текущего пользователя. Для чтения текстового значения из профайла рассмотрим следующую функцию:
function LNGetProfileField(MySession, MyDBName: OLEVariant; MyServerName, MyProfileName, MyUserName, MyFieldName: string):string; var D1: OLEVariant; tmpS: AnsiString; begin if MyServerName='' then tmpS:= GetDefaultServerName else tmpS:=MyServerName; if varisempty(MyDBName) then begin ShowMessage('Фатальная ошибка! Переданный объект <База данных> пуст. Продолжение невозможно!'); Exit; end; D1:=MyDBName.GetProfileDocument( MyProfileName, MyUserName); if varisempty(D1) then begin ShowMessage ('Ошибка при получении профайла '+MyProfileName+' из базе данных '+MyServerName+ ' / '+MyDBName.Name+'. Продолжение невозможно!'); Exit; end; tmpS:=LNGetFieldStrValue(D1,MyFieldName,'',False); Result :=tmpS; end;Как видно из примера, эта функция использует стандартную функцию LNGetFieldStrValue, представленную ранее, но перед этим выполняет ряд дополнительных проверок и операций.
Разработка приложений для Lotus Notes/Domino в среде Borland Delphi
Гусев А.В., Дмитриев А.Г., Тихонов С.И.,
Вычислительный центр ОАО "Кондопога", КНМЦ СЗО РАМН
Lotus Notes / Domino – прекрасная платформа для создания мощных корпоративных информационных систем, ориентированных на групповую работу с электронными документами. В своей работе над комплексной медицинской информационной системой мы на основе тщательного анализа средств разработки и имеющихся на рынке СУБД выбрали Lotus Notes / Domino в качестве основы всей системы. Разработка осуществляется с 1999 года, за это время мы постепенно перешли с версии 4.6 на версию R 5, а затем – на R 6. В данный момент идет тестирование R 6.5 на совместимость с существующим ПО.
Lotus Notes / Domino полностью отвечает ключевым требования к созданию медицинской информационной системы по надежности, безопасности, отказоустойчивости и масштабированию. Работа пользователя в этой среде в максимальной степени приближена к привычной работе с документами – фактически, бумага и авторучка у медицинских сотрудников заменена на компьютер. Формы электронных документов могут быть разработаны по точной аналогии с их бумажными аналогами (при необходимости), а стандартные средства для работы с документами (создание, редактирование, печать, отправка по e - mail, электронная цифровая подпись и т.д.) требуют от пользователя минимального объема обучения.
Однако, как и в любой информационной технологии, имеется ряд недостатков, с которыми приходится мириться и искать пути их преодоления. Основной их недостатков Lotus Notes / Domino для применения в медицинской сфере – это слабая поддержка таблиц в электронных документах. На практике даже с точки зрения пользователя встроенные в клиентское программное обеспечение Lotus Notes средства для работы с таблицами значительно уступают аналогичным инструментам в Microsoft Office. А с точки зрения инструментария разработчика средства для управления таблицами тем более являются малоэффективными. Некоторые изменения в этом направлении были сделаны в версии R 6 Domino, однако и они являются недостаточными. Фактически, в Lotus Notes таблица, как средство отображения, управления и хранения информации, отсутствует как класс. Но это и понятно – ведь Lotus Notes – это, прежде всего, объектно-ориентированная СУБД, предназначенная для групповой работы над документами.
Вместе с тем в нашей работе поддержка табличного формата хранения информации является неотъемлемой функцией системы. Некоторые документы (лист назначений, например) и некоторые приложения (бухгалтерия, аптека, склад, автоматизация службы питания и т.д.) несравненно более эффективно работаю под управлением реляционной СУБД, чем в среде Lotus Notes / Domino. Все это породило необходимость совместного использования Lotus Notes / Domino и реляционной СУБД, в качестве которой был выбран Microsoft SQL Server []. В качестве средства разработки в Lotus Notes / Domino используется специальное программное обеспечение Lotus Designer, позволяющее создавать мультиплатформенные приложения на Visual Basic -подобном языке Lotus Script, @-формулах или Java Script. Это мощное приложение позволяет за очень небольшое время разрабатывать необходимые программы как для выполнения в среде Lotus Notes, так и для работы в обычном браузере Internet. Однако для создания приложения для реляционной СУБД его возможностей явно недостаточно. Поэтому в качестве дополнительного инструментария мы используем Borland Delphi (в настоящее время – версию 6.0).
Одним из серьезных препятствий на использовании Delphi является задача совместного доступа как к информации в реляционной базе данных, так и для доступа к базам данных Lotus Notes / Domino. Для решения этой задачи имеется несколько подходов:
Использование компонентов сторонних производителей (, ) Использование приложения Lotus Notes SQL () Разработка собственных компонентов, используя Notes API () Доступ к ресурсам Lotus Notes посредством OLE.Первый подход подразумевает использование компонентов для доступа к Lotus Notes. При этом программы могут обращаться к базам данных и документам, используя инкапсулированные в эти компоненты свойства и методы. Мы апробировали имевшиеся в свое время предложения и не нашли решения, полностью удовлетворяющего наши требования. Использование компонентов вносило нестабильность в создаваемые приложения, которые нам не удавалось быстро локализовать и исправить, т.к. либо с разработчиками не возможно было наладить контакт, либо тех.поддержка требовала дополнительных финансовых затрат и времени.
Второй подход также, к сожалению, не отвечал требованиям. При этом Notes SQL фактически эмулирует обращение к базе данных Lotus Notes, как к обычной реляционной таблице. Тестирование различных версий Notes SQL показала нестабильность этого программного обеспечения. Особенно ярко недостатки Notes SQL проявлялись при обработке больших объемов информации – в случайные моменты работы программы возникали неустранимые ошибки, которые приводили к полном прекращению работы программ.
Третий подход является более предпочтительным, однако и от него мы со временем отошли в силу его трудоемкости, большой сложно написания программы, массы низкоуровневого кода и высоких требований к знанию внутренний архитектуры Lotus Notes.
Первое время доступ к Lotus Notes посредством OLE казался нам неприемлемым вариантом с точки зрения скорости работы. Однако наш 5-летний опыт работы доказал высокую устойчивость программ на основе этого подхода и вполне приемлемую скорость обработки информации.
Далее мы на примерах покажем, как написать приложение в среде Borland Delphi для баз данных Lotus Notes.
в практической разработке медицинской информационной
Мы используем представленную технологию в практической разработке медицинской информационной системы "Кондопога" вот уже в течение 5 лет. За это время многократно убедились в прекрасной устойчивости и приемлемой скорости работы программ, написанных на Borland Delphi для баз данных Lotus Notes / Domino. Фактически мы убедились, что способны создавать программы на Borland Delphi, которые используют весь арсенал встроенных в Lotus Notes классов [].
Отметим, что со временем в арсенале программиста накапливается самая разнообразная масса готовых функций и процедур, которые целесообразно аккумулировать либо в виде подключаемых библиотек, либо в виде отдельных модулей (pas -файлов). При этом, по нашим наблюдениям, время на разработку новой программы можно сократить в несколько раз именно за счет использования готовых и отлаженных приложений. А это позволяет снизить стоимость разработки и повысить устойчивость приложений, что является уже не столько инструментарием разработчика, сколько экономическим стимулом.
Постепенно нами была накоплена целая библиотека класса middleware, которая реализует практически весь необходимый функционал для написания программ в Borland Delphi для среды Lotus Notes. Это позволило разработать нашу информационную систему таким образом, что взаимные недостатки реляционных и объектно-ориентированных баз данных фактически полностью компенсируются взаимными достоинствами. Поэтому пользователи ИС "Кондопога" одинаково комфортно используют и возможности совместной работы над электронными документами Lotus Notes и встроенные в базы данных Domino приложения, предоставляющие расширенные возможности работы с таблицами реляционных баз данных, мгновенное построение диаграмм на основе данных из документов Lotus Notes и т.д.
Эффективный способ применения интерфейсов в MDI приложениях
Валерий Шер-хан, Королевство Дельфи
В книгах по программированию при рассмотрении различных приёмов и методов приводятся в основном "игрушечные" примеры. Иногда даже можно встретить высказывания автора: "профессиональные программы так не пишут". В самом начале изучения современного объектно-ориентированного программирования я не задумывался над тем, что значит писать профессионально. Задумался, когда стал писать масштабный проект. В этой статье хочу поделиться своим опытом — описать несколько своих решений.
Изначально ставилась задача: разработать модель для построения приложений, ориентированных на работу с базами данных (БД). Под таким приложением подразумевается набор форм, каждая из которых обычно отображает одну таблицу БД. Например, в бухгалтерской или складской программе таблицы "Накладные", "Клиенты", "Товары" удобно расположить на отдельных формах. Несколько таблиц с малым числом строк и столбцов можно было бы расположить на одной форме, например: "Категории товаров", "Типы накладных", "Единицы измерения". Пользователь должен иметь возможность выбирать окно, с которым он хочет работать. Поэтому где-то должно быть меню или список всех или почти всех окон. Понятно, что окно "Накладная" в этом списке отсутствует. Оно будет открываться из списка накладных (окно "Накладные"). Было бы так же удобно открывать последнюю приходную накладную (окно "Накладная") для товара под курсором из окна "Товары". Вот для таких приложений и предназначена описанная в статье модель.
Модель приложения можно свести к абстракции "Окно—>Документ", где Окно — это список Документов, например "Окно—Накладные"—>"Документ—Накладная". Нечто похожее на модель "Master—>Detail", только на разных формах (у нас). В свою очередь Документ может быть Окном, из которого можно открыть другой Документ и т.д., т.е. опять "Окно—>Документ". Например "Окно—Накладная"—>"Документ—Клиенты". И по большому счёту, чем отличается Окно от Документа? Ведь связь может быть и обратной: Документ—>Окно. Под связью понимаем любое действие, инициированное из текущего окна (формы) по отношению к другому окну (форме). Это действие даже может и не требовать отображения того другого окна. Поэтому модель можно упростить ещё: "Документ<=>Документ". Иными словами — множество окон с множеством связей между ними.
Модель будет рассмотрена на примере Delphi, но может быть реализована и на других объектно-ориентированных языках имеющих такие конструкции, как классы, наследование и интерфейсы. Модель построена на основе многооконного интерфейса MDI. На Рис.1 изображено несколько уровней иерархии классов форм. Начальный, наиболее абстрактный уровень — уровень платформы. Под платформой понимается библиотека абстрактных классов и универсальных функций. На этом уровне расположены два базовых класса — класс главной формы TBaseMDIForm и класс дочерней формы TBaseMDIChildForm. Если мы пишем программу складского учёта (для абстрактного заказчика), переходим на другой уровень путём наследования (пунктирные стрелки) необходимых форм от соответствующих базовых классов. Это я называю уровнем схожих проектов. Здесь содержится вся функциональность окон конкретного проекта для абстрактного приложения. Из этих окон уже можно строить полнофункциональное приложение. Но конкретное приложение для конкретного заказчика строится из окон следующего уровня — уровня конкретного приложения. На этом уровне может быть несколько изменён внешний вид окон, переопределены некоторые методы и функции под конкретного заказчика. Для большей ясности приведён Рис.2. Если мы пишем программу для бухгалтерии с базой данных, отличной от базы данных в программе складского учёта, то мы переходим с уровня платформы путём наследования на уровень схожих проектов 2, т.е. это будет параллельная ветвь. И т.д.
Связи между окнами (Рис. 1) показаны сплошными линиями. Т.к. основная функциональность окон находится на уровне схожих проектов, все основные связи между окнами тоже. И сейчас возникает интересный вопрос: как правильно организовать эти связи? Если бы мы строили приложение из окон этого уровня, всё было бы хорошо — каждое окно "знало" бы о других окнах (классах форм) из секции uses. Но мы то строим приложение из наследников этих окон. Получается сложная ситуация — наследники должны "знать" о наследниках. Т.е. часть функциональности, общей для ряда заказчиков, должна уйти на уровень конкретного приложения для конкретного клиента. Это недопустимо, потому что теряется преимущество объектного программирования. Не будем же мы каждый раз после изменений основной функциональности копировать программный код между соседними ветвями уровня конкретного приложения. Вот здесь может помочь использование интерфейсов (специальная конструкция языка). Можно создать отдельные интерфейсы для всех классов окон с нужными свойствами, функциями и методами. Тогда уже окнам будет незачем "знать" друг о друге. Им нужно будет "знать" только об интерфейсах, которые реализуют нужные классы окон. Следовательно, связи между окнами будут находиться там, где и положено, а наследники окон будут нести только функциональность для конкретного приложения (заказчика). И при необходимости смогут иметь свои связи к другим окнам (используя интерфейсы), которых не предусмотрено на уровне выше.
Одно из решений выглядит так. Параллельно с созданием функциональности множества окон надо параллельно создать для каждой группы связей свой интерфейс, содержащий нужные функции, свойства, методы. А при вызове интерфейса надо перебрать все окна в приложении, найти то, которое реализует нужный интерфейс, потом вызвать нужную функцию (свойство). Поскольку функция (свойство) интерфейса может вызываться из многих мест, никто не мешает автоматизировать этот процесс путём создания некого универсального механизма поиска нужного интерфейса среди существующих и "несуществующих"(классов) окон. Дело в том, что окна с нужным интерфейсом в момент его поиска может ещё не существовать. Мы не собираемся при запуске программы создавать сразу все возможные окна. Ведь пользователь может вообще не воспользоваться многими окнами и их интерфейсами в данном сеансе работы с программой. Предположим сейчас, найдено существующее окно "Документ", реализующее связь "Открыть определённый документ". А вдруг пользователь производил там редактирование и не закрыл его (отложил на время). Если мы позволим создать связь с этим окном, оно уже должно будет отображать другой документ и все произведённые пользователем изменения могут пропасть. Значит, необходим некий критерий, позволяющий универсальному механизму поиска определять — можно ли установить связь с окном, либо надо создать другое окно того же класса.
Предлагается способ решить все вышеуказанные сложности весьма простым механизмом. В абстрактной модели "Документ<=>Документ" есть только один объект — Документ. Поэтому достаточно использовать только один интерфейс (IDoc) с одной функцией (ProcessParams), аргументом которой будет массив с любым числом элементов любого типа. Способ обработки этого универсального параметра определяет сам программист без привлечения других интерфейсов, наследования, функций-оболочек. При помощи такого универсального параметра можно организовать создание большого разнообразия связей между формами. Интерфейс IDoc будет реализоваться на уровне платформы классом TBaseMDIChildForm. Поэтому все наследники от этого класса автоматически реализуют этот интерфейс. Поскольку функция ProcessParams должна быть универсальной, тип единственного параметра (Params) используем array of const (array of TVarRec) — массив с любым числом членов любого типа. Таким образом, мы сняли необходимость добавлять новый интерфейс для каждого нового класса формы (или набора действий) и добавлять в него новую функцию при создании новой связи между формами. Интерфейс IDoc мы будем вызывать не напрямую, а посредством вспомогательного объекта DocManager. При запуске программы мы регистрируем (RegisterFormClass) в DocManager классы всех необходимых окон конкретной программы. Регистрация осуществляется с указанием номера класса и заголовка формы. Номер класса уникален для ветви уровня схожих проектов (Рис. 2). Заголовок формы необходим, т.к. предполагается автоматически создавать меню со списком окон без необходимости сразу создавать все окна. При организации связи с другим окном будем пользоваться функциями ShowDoc и ProcessDocParams. В качестве параметров для этих функций нужно задать номер класса и параметр типа array of const (Params). Поэтому для связи с другим окном данное окно должно "знать" только номер класса. Ссылки на класс (вызываемой формы) и интерфейс IDoc не требуются. ShowDoc отображает окно с передачей в него нужного параметра. ProcessDocParams организует обработку параметра без необходимости отображать окно (в фоновом режиме). Обе функции создают при необходимости окно нужного класса и затем вызывают ProcessParams (IDoc) созданного окна.
Этот механизм очень напоминает технологию COM в ОС Windows, только внутри одного приложения.
Рассмотрим один из случаев применения вышеуказанного принципа. Из списка накладных (окно "Накладные") мы хотим увидеть содержимое накладной под курсором. Для этого мы вызываем ShowDoc с указанием номера класса. В качестве параметра Params массив, один из членов которого является уникальным номером накладной из списка накладных. DocManagerst создаёт окно "Накладная" и передаёт туда массив Params с номером накладной (и др. параметрами при необходимости). В окне "Накладная" по этому номеру мы загружаем список товаров соответствующей накладной. А что будет, если пользователь не закрыв это окно, вернётся к списку накладных и опять инициирует открытие окна "Накладная"? Тут возможно два случая — пользователь хочет просмотреть содержимое той же накладной или он хочет просмотреть уже другую накладную. Для таких случаев существует вот какой механизм. IDoc имеет вспомогательные процедуры SetParams для сохранения Params в форме и ParamsIs для определения идентичности с Params, сохранённым через SetParams. При вызове DocManager.ShowDoc если найдена уже существующая форма нужного класса, происходит вызов ParamsIs для проверки равенства Params из ShowDoc и Params существующей формы. Если они равны, показываем существующую форму на переднем плане, если Params`ы не равны, то создаём новую форму на переднем плане с передачей туда нового Params.
В форме TBaseMDIChildForm после вызова SetParams происходит сохранение Params не в виде array of const, а в виде динамического массива типа Variant. Конвертация происходит функцией VarOpenArrayToVarArray в модуле Misc. Там же есть функция VarEqual, которая вызывается из ParamsIs. VarEqual и VarOpenArrayToVarArray построены специальным образом, который определяет степень свободы задания элементов массива Params типа array of const. В нём можно задавать элементы практически любых типов. Ординарные типы, ссылки на объекты, адреса переменных с соответствующим преобразованием при их интерпретации. Даже можно задать в качестве элемента динамический массив типа Variant, элементами которого могут быть тоже массивы типа Variant. При этом VarEqual будет работать корректно (на основе рекурсии). Замеченное ограничение — невозможность передачи строк String со служебными кодами типа 0х0, 0х1, 0х2 и т.д. Ничего с этим пока поделать не смог.
Ещё несколько особенностей. ProcessDocParams не влияет на Params, сохранённый в TBaseMDIChildForm с помощью SetParams (т.е. из ShowDoc). ProcessDocParams не вызывает ParamsIs и SetParams формы. ProcessDocParams и ShowDoc вызывают вспомогательные методы интерфейса IDoc DocInit и ProcessParams. Их можно переопределить в наследниках. DocInit предназначен для инициализации формы, там можно открывать таблицы БД, обрабатывать Params из ShowDoc. А ProcessParams предназначен для обработки Params из ShowDoc и из ProcessDocParams.
В DocManager встроен механизм заполнения пункта меню списком заголовков зарегистрированных классов форм с целью предоставления пользователю способа открытия желаемой формы. Функция CreateMenuItems принимает параметр типа TMenuItem, где хотим создать вышеуказанный список (Обычно это пункт главного меню главной формы). Причём параллельно автоматически заполняется свойство объекта DocManager ActionList типа TActionList. Его можно использовать для заполнения "вручную" (программистом) альтернативного средства выбора окон не меняя код TDocManager.
При регистрации класса окна (DocManager.RegisterFormClass) необходимо указать дополнительный параметр — это тип окна. Есть три типа "Окно", "Документ" и "Отчёт". При вызове CreateMenuItems всё, что зарегистрировано как "Документ" не входит в меню, а то, что помечено как "Отчёт", попадает в конец меню после разделителя. Предполагается, что "Документ" вызывается из других окон (например окно "Накладная"), а количество и порядок "Отчётов" могут часто меняться, поэтому в конце. В качестве пункта меню выбора доступных окон удобно использовать пункт главного меню главной формы.
DocManager создавать вручную не надо, создаётся и уничтожается автоматически при добавлении в проект ссылки на модуль Doc.
Некоторые рекомендации по использованию Params: array of const. Рекомендуется первым элементом массива использовать целое число — номер команды (связи), достаточно сделать уникальным в пределах класса формы на уровне схожих проектов и ниже. Т.о. при вызове ShowDoc и ProcessDocParams, чтобы попасть в нужное место, указываем номер класса (TypeId: Integer), номер команды (Например первый элемент Params: array of const). В нужной форме в ProcessParams анализируем первый элемент массива Value :Variant, в DocInit анализируем первый элемент массива FParams :Variant (поле данных TBaseMDIChildForm). В остальных элементах Params: array of const передаём всё, что необходимо для связи с другой формой.
Рассмотрим один частный случай применения вышеуказанного принципа. Предположим, что мы хотим из нескольких мест программы ("Список документов" "Список товаров") открывать окно "Накладная", в котором находится содержимое соответствующего документа. В качестве параметра при организации связи используем уникальный номер накладной в рамках БД. Всё бы хорошо. Но есть одно "но". Реальная ситуация — от общего родителя "Абстрактный документ" наследовано несколько конкретных: "Приход", "Расход", "Акт переоценки". Это разные классы, имеющие разные номера при регистрации. Т.о. напрямую вызывать ShowDoc можем но это не удобно, нам надо ещё знать тип документа: "Приход", "Расход", "Акт переоценки". Это чтоб выбрать необходимый номер класса. Решение у меня такое. Вызываем окно "Список документов" при помощи ProcessDocParams, с передачей номера документа. В окне "Список документов" в ProcessParams организуем механизм запроса из БД типа документа по его номеру. Далее вызываем ShowDoc с указанием номера класса, который соответствует типу данного документа, и транслируем туда же номер документа (другой элемент массива Params), полученный от другой формы через ProcessDocParams. Что у нас получилось. Допустим, пользователь из "Списка товаров" хочет открыть последний документ, содержащий товар под курсором. Им может оказаться как "Приход", так и "Акт переоценки". После нажатия <Enter> к примеру он сразу увидит нужное окно, а как организован механизм его открытия он может даже и не догадываться. Ну а из "Списка документов" открыть нужный документ можно вызвав напрямую "свой" ProcessParams либо тоже через DocManager (для однообразия). Изящно, не правда ли?
Прилагается рабочий код уровня платформы, демонстрационный код уровня схожих проектов и конкретного приложения. См. комментарии в исходном коде. Необходимо: Delphi 7, BDE. После распаковки запустить Proj1Firm1.dpr, скомпилировать.
Распространение статьи приветствуется, целиком с указанием источника. Использование программного кода и идей приветствуется.
К материалу прилагаются файлы: Демонстрационный проект (151 K) обновление от 3/5/2007 3:14:00 AM
Чудо четвертое (String Trick).
Ну, что ж, добавим опять кнопку на нашу форму и зададим следующий код для события OnClick:
procedure TfrmAllMiracles.btnCopyMrclClick (Sender: TObject); const cs: array[0..1] of char='01'; begin ShowMessage(copy(cs,0,1)+copy(cs,1,1)); end; Figure 6. |
Как обычно обратимся к Help'у, смотрим функцию Copy:
Returns a substring of a string or a segment of a dynamic array.
...
function Copy(S; Index, Count: Integer): string;
function Copy(S; Index, Count: Integer): array;
...
Дело в том, что в выражении copy(cs,0,1)+copy(cs,1,1) оба раза вызываются разные версии функции copy, первый раз - для динамических массивов, которые нумеруются с 0, а второй раз - для строчек, первый элемент которых имеет индекс 1. Оба раза cs преобразуется к необходимому типу, и то, что cs, как массив начинается с нулевого элемента, в данном случае не имеет никакого значения.
А теперь, наконец, мы добрались и до обьектов. Множество Дельфийских чудес связаны с тем, что обьекты в Delphi - автоматически разыменуемые ссылки, которые могут указывать на освобожденную или занятую кем-то другим область памяти. О таких случаях написано немало. Наше чудо - иное.
Чудо Первое (Round Miracle).
Откройте Delphi, создайте новый проект, назовите его AllMiracles, положите кнопку на главную форму и напишите в обработчике события OnClick следующий код:
procedure TfrmAllMiracles.btnRoundMrclClick(Sender: TObject); begin ShowMessage( IntToStr( Round(3.5) - Round(2.5) ) ); end; Figure 1. |
А теперь остановитесь и скажите, какой результат вы ожидаете увидеть. Я надеюсь вы не сказали "1", ведь иначе это не было бы чудо. Те, у кого хорошо развита интуиция, могут сказать "0", и это будет еще дальше от правильного ответа. И только те, кто часто играет в Спортлото или, на худой конец, внимательно читает документацию, ответит "2" и это будет правильно. Не верите? - жмите F9.
Читаем Help по функции Round:
Round returns an Int64 value that is the value of X rounded to the nearest whole number. If X is exactly halfway between two whole numbers, the result is always the even number.
Надеюсь, теперь вы поняли, о чем мы будем говорить сегодня. В этой статье нет сложных, замысловатых примеров. Код - предельно упрощен что бы выделить саму суть проблемы. А наше с вами дело - разобраться в ней и, если можно, исправить ситуацию. Как, например, в следующем случае.
Чудо пятое (Is-Miracle).
Опишите в разделе protected нашей формы поле FControl типа TСontrol и задайте для еще одной - новой кнопки такую вот реакцию на ее нажатие:
procedure TfrmAllMiracles.btnIsMrclClick(Sender: TObject); begin if (FControl is TControl) then begin if not Assigned(FControl) then FControl := TControl.Create(Self); end else ShowMessage('Not a Control'); end; Figure 7. |
Такое "Чудо" я видел несколько раз и в разных проявлениях. Сколько раз бы вы не нажимали на кнопку btnIsMrcl, вы каждый раз будете видеть сообщение 'Not a Control', а конструктор TControl так никогда и не будет вызван.
Вот, что говорит Help:…The expression object is class returns True if object is an instance of the class denoted by class or one of its descendants, and False otherwise. (If object is nil, the result is False.)
Дело в том, что оператор is использует ссылку на класс обьекта, а не то, как описана переменная, которая по сути - простой указатель. Так что TControl не всегда TControl.
Да, я надеюсь вы понимаете, что TControl здесь выбран случайно, с таким же успехом это мог быть и любой другой класс.
Случай когда FControl ссылается на уже освобожденный обьект или является локальной и непроинициализированной переменной, дает непредказуемые результаты и может привести к совсем не чудесному краху аппликации.
А вот для следующего чуда я нашел только косвенное обьяснение в Help'е и поэтому мы будем вынуждены провести небольшой эксперимент.
Чудо седьмое (Miracle with Variants).
Как вы уже догадались, начнем с новой кнопки, которая выполняет следующие действия при нажатии:
procedure TfrmAllMiracles.btnVarMrclClick(Sender: TObject); var X,Y,Z: variant; begin X := '1'; Y := '2'; Z := 3; ShowMessage(X+Y+Z); end; Figure 14. |
Можете ли вы предсказать результат выражения '1'+ '2'+3? Если вы сказали '6', то вы тоже попались. Посмотрим повнимательнее, '1'+ '2' будет... конечно '12', 12+3=15. Это и есть правильный ответ.
Итак, мы увидели семь чудес Delphi, семь - из многих. Это не значит, что они - самые яркие или самые чудесные. Но на них можно многому научиться. Возьмем последнее, только что рассмотренное нами, чудо. Задумайтесь, как Delphi удается сводить в одном выражении значения разных типов? А если один из членов выражения - variant?
Чудо шестое (Is-Miracle II)
Давайте посмотрим еще на одно, похожее чудо связанное с оператором is. Добавим к нашей группе проектов (ProjectGroup1) новый проект - DLL с именем AllMirrLib, в единственном модуле которого будет следующий код:
library AllMirrLib; uses Controls; function IsControlLib(const anObj: TObject): boolean; begin Result := anObj is TControl; end; exports IsControlLib; Figure 9. |
Как вы видите эта библиотека экспортирует только одну очень простую функцию, которая возвращает знечение True в том случае, если ее единственный параметр происходит от TControl и False - в остальных случаях.
В модуль формы нашего основного проекта добавим следующее определение:unit AllMir; interface ... implementation {$R *.DFM} function IsControlLib(const anObj: TObject): boolean; external 'AllMirrLib.DLL'; Figure 10. |
procedure TfrmAllMiracles.btnIsMrcl2Click(Sender: TObject); begin FControl := TControl.Create(nil); try if not IsControlLib(FControl) then ShowMessage('Not a Control'); finally FreeAndNil(FControl); end; end; Figure 11. |
Как вы уже наверное догадались FControl опять окажется не TControl. Найдите в модуле System процедуру _IsClass. Хоть она и написана на ассемблере, нетрудно понять, что в ней происходит - в цикле просматриваются ссылки на классы (сначала собственная - обьекта, а потом - всех предков) и среди них ищется равная правому операнду. Давайте изменим немного процедуру:
procedure TfrmAllMiracles.btnIsMrcl2Click(Sender: TObject); var p1, p2: pointer; begin FControl := TControl.Create(nil); try p1 := pointer(FControl.ClassType); p2 := pointer(TControl); if not IsControlLib(FControl) then ShowMessage('Not a Control'); finally FreeAndNil(FControl); end; end; Figure 12. |
Посмотрите под отладчиком значения p1 и p2 - они равны. Теперь изменим и функцию IsControlLib:
function IsControlLib(const anObj: TObject): boolean; var p3,p4: pointer; begin p3 := pointer(anObj.ClassType); p4 := pointer(TControl); Result := anObj is TControl; end; Figure 13. |
Косвенное указание на эту проблему в Help'е можно найти в описании метода ClassNameIs. Читаем Help:
Use ClassNameIs when writing conditional code based on an object's type or to query objects across modules, or DLLs.
Да, кстати, не забудьте, что у вас два проекта в группе и компилируется всегда только активный проект. Так что не забывайте перпеключаться на нужный проект по мере необходимости или компилируйте сразу все: Alt-P, U.
Следующее чудо я встретил в программе одного начинающего программиста и оно было конечно слегка закамуфлировано, так что я, к своему стыду, даже не сразу понял в чем дело. Я видел значения переменных, знал, что это - переменные типа variant, но никак не мог понять почему результат вычисления некоего несложного выражения все время ошибочный. Проверьте себя и вы.
Чудо третье (One more low integer miracle).
Новая кнопка на форме будет реагировать на нажатие следующим образом:
procedure TfrmAllMiracles.btnLowIntMrclClick( Sender: TObject); var lowInt: integer; begin lowInt := -2147483648; ShowMessageFmt('%d',[lowInt]); end; Figure 4. |
Видимо компилятор пытается определить константу целого типа со значением 2147483648, а только затем изменить ее знак, но это ему не удается. Перепишем код:
procedure TfrmAllMiracles.btnLowIntMrclClick( Sender: TObject); var lowInt: integer; begin lowInt := -int64(2147483648); // lowInt := -2147483648; ShowMessageFmt('%d',[lowInt]); end; Figure 5. |
Вот теперь - все нормально. Пример очень незамысловат, но дает нам представление о том, как компилятор Delphi обрабатывает константы и определяет их тип.
А вот следующее чудо - пример того, к какой путанице может привести перегрузка функций. Такие чудеса мы зачастую сами устраиваем себе по невнимательности, а потом часами ищем ошибки.
Чудо Второе (Absolute Miracle).
Положите на главную форму созданного ранее проекта новую кнопку и напишите в его обработчике события OnClick такой код:
procedure TfrmAllMiracles.btnAbsMrclClick (Sender: TObject); var i1: int64; begin i1:= abs(low(integer)); ShowMessage(IntToStr(i1)); end; Figure 2. |
Help не говорит о функции Abs ничего нового: Abs returns the absolute value of the argument X. X is an integer-type or real-type expression.
Переменная i1 описана как int64, и это правильно, потому что 2147483648 - уже выходит за границы типа integer. Это значение (2147483648) мы и ожидаем увидеть на экране, не так ли? А вот и нет. Проверьте. На экране вновь -2147483648. Как абсолютное значение может быть отрицательным?
Давайте еще раз, повнимательнее рассмотрим выражение abs(low(integer)). Что можно еще сказать про него? Не смотря на наличее в нем функций, это - константа
Читаем Help по теме "Constant expressions":...Constant expressions cannot include variables, pointers, or function calls, except calls to the following predefined functions: Abs...Low... попробуем описать константу со значением равным этому выражению:
... const ci = abs(low(integer)); ... Figure 3. |
procedure TfrmAllMiracles.btnAbsMrclClick (Sender: TObject); const ci = abs(low(integer)); var i1: int64; begin // i1:= abs((low(integer))); i1:= abs(int64(low(integer))); ShowMessage(IntToStr(i1)); end; Figure 4. |
Следующее чудо - пример того, как вполне правильный код отказывается компилироваться.
Фокус первый (Variant trick)
Читаем Help в разделе "Variants in expressions":
...In a binary operation, if only one operand is a variant, the other is converted to a variant..
Не кажется ли вам это удивительным - variant можно складывать с чем угодно. Например, integer плюс variant - будет variant, а variant можно опять складывать с чем угодно...
Новая кнопка на форме будет выполнять следующие действия:procedure TfrmAllMiracles.btnVarTrickClick(Sender: TObject); var v: variant; b: boolean; i: integer; s: string; d: TDatetime; x: Double; begin v:=0; b := true; i := 2; s := '3'; d := StrToDateTime('01/01/01'); x := 5; v := v+b+i+s+d+x; ShowMessage(VarToStr(v)); end; Figure 15. |
Не кажется ли вам, что чудо уже то, что этот код компилируется, а ведь он еще и выдает какой-то результат. А ведь все очень просто - "variant можно складывать с чем угодно" и снова получим - variant.
Однажды ко мне обратился один мой знакомый с вопросом нет ли в Delphi чего-то подобного скрытому параметру Self, но для оператора with. Нет - ответил я ему сперва, а потом задумался...
Фокус второй (With-trick)
Предположим у нас есть следующая функция:
procedure ShowText(sl: TStringList); begin ShowMessage(sl.text); end; Figure 16. |
procedure TfrmAllMiracles.btnWithSelfTrickClick(Sender: TObject); var sl: TStringList; begin sl := TStringList.Create; try sl.CommaText := '1,2,3,4,5,6,7,8,9,0'; ShowText(sl); finally sl.Free; end; end; Figure 17. |
И мы, по каким-то причинам, хотим избавиться от локальной переменной sl. Но для того, что бы обратиться к функции ShowText, мы должны передать ей параметр типа TStringList. Откуда же его взять?
Давайте порассуждаем. Каждый метод получает скрытый параметр Self, может быть как-то можно вытащить его оттуда? Писать для этого специальный метод какого-то класса не хотелось бы - ведь это работало бы только для его потомков.
Давайте почитаем Help, раздел "TMethod type":...This type can be used in a type cast of a method pointer to access the code and data parts of the method pointer... Не это ли то, что мы ищем?
Определим тип и функцию:
type TSimpleMethod = procedure of object; function GetWithSelf(const pr: TSimpleMethod): TObject; begin Result := TMethod(pr).Data; end; Figure 18. |
Как видите, функция принимает указатель на метод, а возвращает обьект, являющийся владельцем этого метода. Но каким же методом мы воспользуемся? Например, метод Free, ведь его история восходит еще к самому TObject'у. Теперь проверим себя:
procedure TfrmAllMiracles.btnWithSelfTrickClick(Sender: TObject); begin with TStringList.Create do try CommaText := '1,2,3,4,5,6,7,8,9,0'; ShowText(TStringList(GetWithSelf(Free))); finally Free; end; end; Figure 19. |
Автор —
Живет и работает в Израиле. Женат, имеет двоих детей.
Сфера интересов - Delphi, Windows, Oracle, GSM биллинг.
Семь чудес и два фокуса на Дельфи
, Королевство Дельфи
18 августа 2003г.
Верите ли Вы в чудеса или нет, Вы наверняка согласитесь со мной, что иногда что-то такое случается с кодом наших программ, и они вдруг перестают компилироваться или, что еще коварнее, начинают выдавать совершенно непредсказуемый результат. И вот тогда, сознайтесь, вас начинают посещать странные мысли об участии во всех этих чудесах неких потусторонних сил.
В этой статье мы попытаемся сдернуть таинственный покров с нескольких, самых простых "чудес" и убедимся, что все это - только обман, иллюзия, а зачастую - искусное мошейничество.
Мы рассмотрим семь (из многих) таких чудес и попробуем разгадать их секреты. Поняв механизм их происхождения, мы, в заключении, покажем два примера использования этих тайных сил в "мирных целях". Наша цель - лучше узнать Delphi и в будущем избежать некоторых труднообьяснимых ошибок.
Для того, что бы вы поняли, что я имею в виду, давайте рассмотрим один очень простой пример.Поддержка MS-макросов в DELPHI
,
Многие из вас наверняка пробовали свои силы в написании макросов в Word, Excel, Access и других продуктах Microsoft. И немало программистов завидовало Word'у и мечтало встроить поддержку макрокоманд и в свои приложения
Послесловие
Встроенный макроязык - это то средство, которое может превратить вашу программу в мощный и универсальный продукт. Но имейте в виду, что показанная техника - только вершина айсберга, в составе библиотеки имеется еще много компонент (IScriptError, IScriptModule, IScriptModuleCollection, IScriptProcedure, IScriptProcedureCollection), которые позволяют всесторонне и тонко управлять интерпретатором.
AddCode | Запись в компонент исходных текстов процедур и функций для последующего их выполнения |
AddObject | Добавление объекта к внутренней объектной модели макросов |
Eval | Выполнение вычисления и возврат результата. То же что и if в нормальных языках программирования |
ExecuteStatement | Немедленное выполнение представленного кода |
Reset | Восстановление первоначального состояния интерпретатора. Очистка от всех предыдущих исходных кодов |
Run | Выполнение предопределенной при помощи AddCode процедуры или функции с заданными параметрами |
OnError | Событие, возникающее при ошибке времени выполнения |
OnTimeOut | Событие, возникающее при таймауте |