Delphi - сбориник статей

         

Доступ к базе данных


После того, как мы инициализировали сессию связи с 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.

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

Как обычно обратимся к 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.

Здесь тоже поставим точку останова и сравним значения. Переменные p1, p2 и p3 имеют одно и тоже значение, а вот p4 - указывает куда-то ни туда. Проблема в том, что в аппликации и в DLL сосуществуют два разных класса TControl, вот поэтому равества быть и не может.
Косвенное указание на эту проблему в 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.

Совершенно обычная процедура. У нас возникло желание присвоить некоторой переменной вполне законное значение. Но этот код не компилируется: Overflow in conversion or arithmetic operation Жмем F1 на сообщении об ошибке и читаем: The compiler has detected an overflow in an arithmetic expression: the result of the expression is too large to be represented in 32 bits.

Видимо компилятор пытается определить константу целого типа со значением 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.

Прежде чем нажать F9, проанализируем написаное. Low от integer - значение известное всем, записанное даже в Help'е и равное -2147483648, т.е. число отрицательное.
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.

Код компилируется. Значит мы - правы, а это значит, что результат выражения определяется еще на стадии компиляции. Далее, low(integer)) имеет целый тип. Abs от integer - тоже целое, а нам нужно int64. Поробуем переписать код следующим образом:

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.

Теперь - заработало. Секрет "Абсолютного чуда" раскрыт! Кстати, abs(int64(low(integer))) - тоже константа.

Следующее чудо - пример того, как вполне правильный код отказывается компилироваться.



Фокус первый (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), которые позволяют всесторонне и тонко управлять интерпретатором.

Некоторые методы и свойства компонента ScriptControl Член класа Описание
AddCode Запись в компонент исходных текстов процедур и функций для последующего их выполнения
AddObject Добавление объекта к внутренней объектной модели макросов
Eval Выполнение вычисления и возврат результата. То же что и if в нормальных языках программирования
ExecuteStatement Немедленное выполнение представленного кода
Reset Восстановление первоначального состояния интерпретатора. Очистка от всех предыдущих исходных кодов
Run Выполнение предопределенной при помощи AddCode процедуры или функции с заданными параметрами
OnError Событие, возникающее при ошибке времени выполнения
OnTimeOut Событие, возникающее при таймауте


К этому времени вы уже


К этому времени вы уже запустили среду разработки (которая, естественно, должна поддерживать работу с ActiveX, к примеру, Delphi) и создали новое приложение (New > Application).
Теперь нужно импортировать данные из библиотеки msscript.ocx в наш проект. Для этого воспользуемся пунктом меню Project > Import Type Library - и для достоверности выберем нашу библиотеку, щелкнув на кнопке Add…(рис. 1). После чего выберем файл msscript.ocx.
Импортировав библиотеку, мы обнаружим в составе нашего проекта файл MSScriptControl_TLB.pas, в котором содержатся все необходимые определения интерфейсов и констант. Теперь в главной форме нашего приложения, в реакции кнопки (которую мы уже разместили на форме), напишем следующий код:
procedure TForm1.Button2Click (Sender: TObject); var SC:TScriptControl; Code:WideString; begin SC:=TScriptControl.Create (Self); SC.Language:='VBScript'; try Code:='Function DoSmth () '#13#10+ 'DoSmth = "This is the Simple Test"'#13#10+ 'End Function'; SC.AddCode (Code); SC.ExecuteStatement ('MsgBox "Testing:"+DoSmth ()'); finally SC.Free; end; end; После выполнения этого кода увидим на экране сообщение системы (Testing:This is the Simple Test).
Теперь рассмотрим приведенный выше код более подробно. Сначала создаем объект TScriptControl, который, собственно, и проделывает за нас всю грязную работу. Далее присваиваем свойству Language значение "VBScript", уведомляя тем самым компонент о том, что переданный ему код будет написан на Visual Basic. Помимо VBScript: тут возможны и другие значения: можно, например, воспользоваться Jscript - при этом будет использован синтаксис JavaScript или же синтаксис любого другого интерпретатора, поддерживающего технологию ActiveX-скриптов (Visual Basic, Java, ActivePython, ActivePerl и т.п).
В следующих строчках пишем исходный код функции DoSmth, которая возвращает нам вторую часть предложения. Далее записываем этот код в компонент - а в следующей строчке исполняем его, передавая возвращаемое им значение в функцию MsgBox. Все это пишется с использованием синтаксиса Visual Basic. Функции AddCode и ExecuteStatement имеют следующий вид:
procedure AddCode (const Code: WideString); safecall; Где Code - код процедуры, функции (или любого их сочетания в любом количестве), который записывается в компонент и после этого может быть вызван с помощью ExecuteStatement или Run:
procedure ExecuteStatement (const Statement: WideString); safecall; Где Statement - текст программы, который будет сразу же исполнен.

Осуществлять вывод сообщений при помощи


Осуществлять вывод сообщений при помощи макроязыка мы уже научились, однако это не единственная возможность компонента. Так, компонент TScriptControl представляет нам возможность использования собственной объектной модели в создаваемых макросах - то есть доступ к специфическим объектам нашего приложения. Для этого в нашем приложении потребуется сначала создать объект автоматизации Automation Object (пользователи Microsoft Visual Basic могут пропустить этот раздел, так как в Visual Basic поддержка объектов автоматизации встроена изначально). Чтобы создать этот объект, при открытом приложении щелкнем на пункте меню Новый и выберем закладку ActiveX. Здесь выберем пункт Automation Object.
Далее предстоит создать интерфейс, который мы собираемся включить в объектную модель ScriptControl. Для начала просто создадим объект с единственной функцией print, которая будет выводить в компонент TlistBox, размещенный на главной форме, некоторый текст. Все существенные настройки показаны на рис. 2.
Далее обновляем информацию об объекте, щелкнув для этого на соответствующей кнопке (рис. 2), и переходим к секции реализации объекта.
Здесь уже нас поджидает созданный средой разработки шаблон, в который остается внести только некоторые исправления:
unit SimpleTest; {$WARN SYMBOL_PLATFORM OFF} interface uses ComObj, ActiveX, ScriptTest_TLB, StdVcl; type TSimpleTest = class (TAutoObject, ISimpleTest) protected procedure Print (Data: OleVariant); safecall; {Protected declarations} end; implementation uses ComServ,Main; procedure TSimpleTest.Print (Data: OleVariant); begin Main.Form1.ListBox1.Items.Add (Data); end; initialization TAutoObjectFactory.Create (ComServer, TSimpleTest, Class_SimpleTest, ciMultiInstance, tmApartment); end. Осталось один раз прогнать наше приложение вхолостую - для регистрации и проверки на наличие ошибок. Если все прошло удачно, можно приступать к дальнейшему написанию макросов.
Регистрация объекта
Как и в прошлый раз, создадим на нашей главной форме кнопку и объект ListBox1. Затем в реакцию кнопки на нажатие напишем следующий код:
procedure TForm1.Button1Click (Sender: TObject); var SC:TScriptControl; Test:ISimpleTest; begin SC:=TScriptControl.Create (Self); Test:=CoSimpleTest.Create; try SC.Language:='VBScript'; SC.AddObject ('PrintTest',Test,True); SC.ExecuteStatement ('PrintTest.Print "This is the Test"'); finally Test:=nil; SC.Free; end; end; Опять же, как и в прошлый раз, сначала создаем компонент ScriptControl, затем инициализируем интерфейс ISimpleText и добавляем его в нашу объектную модель посредством функции:
procedure AddObject (const Name: WideString; const Object_: IDispatch; AddMembers: WordBool); safecall; Где:
Name - название нашего компонента во внутреннем пространстве имен. Object - ссылка на наш объект. AddMembers - опциональный параметр, который устанавливается в True, если все члены класса Object должны быть доступны глобально, и False - в противном случае. Следующая строка кода демонстрирует использование объекта Test при написании макроса. Как видно, в тексте макроса мы пользуемся названием, определенным при помощи параметра Name функции AddObject. Результат - на рис. 3.

С чего начать


Конечно, выбор, как всегда, есть.

Самый трудный путь - это написание собственного интерпретатора макрокоманд. Естественно, возиться с написанием собственных разборщиков синтаксиса, исполнителей команд и т.д., не каждый захочет - вот если бы найти такой компонент, который принимал бы исходный текст макроса и выводил результат. К счастью несчастных программистов и здесь на помощь пришла всеми любимая Microsoft, разработавшая Windows Script Control - компонент, который соответствует практически всем требованиям, выдвигаемым к поддержке макросов в ваших программах.

Итак, для начала нам понадобится сам компонент Windows Script Control, который можно загрузить с сайта разработчика (название архива - sct10en.exe). Распаковав его, мы увидим собственно компонент msscript.ocx и дополнительные файлы справки.

Теперь смело можно браться за разработку поддержки макросов в вашем приложении.



Имитация внутренних группировок


Для создания внутренних группировок необходимо подготовить не только TDBGrid, но и набор данных, которые он будет отображать. Ведь TDBGrid не умеет показывать строк, которых нет в его источнике данных (TDataSource). Подготовим данные по такому запросу: выберем всю информацию по странам и добавим список континентов с суммами полей "население" и "площадь". Обычный UNION-запрос:

Select 1 as TypeRecord , Continent , Name, Area , Population From country Union Select 0 as TypeRecord,Continent ,Continent as Name, Sum(Area) as Area , Sum(Population) as Population From country Group By Continent Order by 2,1

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

procedure TfExDBG.__GridFixDrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); Var Alignment : TAlignment; begin // рисуем на строке итогов имитацию заголовка грида IF Column.Field.DataSet.FieldByName('TypeRecord').AsInteger = 0 Then TexDBGrid(Sender).DrawCellButton(Rect,Column.Field.DisplayText,[fsBold],State,Alignment) end;
Вуаля! :о)

А вот еще один вариант группировок — без итогов по каждой колонке, только отделение групп данных друг от друга (рис. 5).
рис. 5

Для его реализации добавим метод, аналогичный DrawCellButton, вернее создадим новый на его основе. Метод DrawRowButton делает тоже самое, что и DrawCellButton, но только всегда растягивает картинку на всю видимую строку грида.

procedure TexDBGrid.DrawRowButton(Rect: TRect; Text: String; Style: TFontStyles; Alignment: TAlignment); Var FullRect : TRect; Col : TColumn; begin FullRect:=Rect; FullRect.Left:=IndicatorWidth + 1; FullRect.Right:=CalcTitleRect(Columns[Columns.Count-1],0,Col).Right; DrawCellButton(FullRect,Text,Style,[],Alignment); end;



Имитация внутренних группировок и метки колонок


Работая с заголовками мы не один раз их перерисовывали, вписывая текст и добавляя 3D-окантовку. Это умение можно использовать в любом месте сетки грида, а не только в заголовках



Использование фиксированных колонок


И последнее, что мы сотворим с нашим гридом :о), это снабдим его свойством FixedCols, которого так не хватает в стандартном TDBGrid'е.

Для тех, кто может быть не знает, отметим, что у стандартного TDBGrid есть фиксированный столбец, он используется гридом для внутренних нужд. Это тот самый индикатор слева, в котором рисуется треугольник, указывая на текущую строку. Добавляя свое свойство FixedCols, необходимо это учитывать.

TexDBGrid = class(TDBGrid) private ... FFixedCols : Integer; ... public Property FixedCols : Integer read GetFixedCols write SetFixedCols; ... //************************************************************************************************** procedure TexDBGrid.SetFixedCols(const Value: Integer); Var FixedCount,i : Integer; begin // Следует учесть индикатор грида IF Value IndicatorOffset + 1) Then Begin IF FixedCount >= ColCount Then FixedCount:=ColCount - 1; Inherited FixedCols := FixedCount; // На фиксированных колонках нельзя останавливаться по табуляции For i := 1 To FixedCols Do TabStops[I] := False; End; FFixedCols := FixedCount - IndicatorOffset; end; //************************************************************************************************** function TexDBGrid.GetFixedCols: Integer; begin IF DataLink.Active Then Result := Inherited FixedCols - IndicatorOffset Else Result := FFixedCols; end; //**************************************************************************************************

Необходимо восстанавливать данные о фиксированных колонках каждый раз, когда параметры колонок будут пересчитываться. Смотрите в иллюстрирующем проекте процедуры TexDBGrid.LayoutChanged; и TexDBGrid.SetColumnAttributes.

Для того, чтобы в нашем гриде фиксированные колонки вели себя также, как ведут они себя, например, в TStringGrid, нужно обработать реакцию на мышь и клавиатуру.

//************************************************************************************************** Procedure TexDBGrid.KeyDown(var Key: Word; Shift: TShiftState); Var KeyDownEvent: TKeyEvent; Begin KeyDownEvent := OnKeyDown; IF Assigned(KeyDownEvent) Then KeyDownEvent(Self, Key, Shift); IF NOT Datalink.Active OR NOT CanGridAcceptKey(Key, Shift) Then Exit; // наша задача - не пустить в область фиксированных колонок, // то есть SelectedIndex не может быть меньше, чем FFixedCols IF ssCtrl IN Shift Then Begin IF (Key = VK_LEFT) AND (FixedCols > 0) Then Begin SelectedIndex := FixedCols; Exit; End; End Else Case Key Of VK_LEFT: IF (FixedCols > 0) AND NOT (dgRowSelect in Options) Then IF SelectedIndex 0) AND (ColCount <> IndicatorOffset + 1) AND NOT (dgRowSelect IN Options) Then Begin SelectedIndex := FixedCols; Exit; End; End; OnKeyDown := Nil; Try Inherited KeyDown(Key, Shift); Finally OnKeyDown := KeyDownEvent; End; end; //************************************************************************************************** procedure TexDBGrid.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); Var Cell : TGridCoord; begin Cell:=MouseCoord(X,Y); //При скроллировании данных фиксированные колонки должны оставаться на месте IF (Cell.X >= 0) AND (Cell.X < FixedCols + IndicatorOffset) AND Datalink.Active Then Begin IF (dgIndicator IN Options) Then Inherited MouseDown(Button, Shift, 1, Y) Else IF (Cell.Y >= 1) AND (Cell.Y - Row <> 0) Then Datalink.Dataset.MoveBy(Cell.Y - Row); End Else inherited MouseDown(Button, Shift, X, Y); end; //**************************************************************************************************

Вот, собственно и все, что мы хотели рассказать.

Елена Филиппова и Игорь Шевченко
Специально для Скачать проект: (25К)



Компонент в ячейке редактирования


Отвлечемся на некоторое время от заголовков TDBGrid и обратимся к редактированию данных. Стандартный внутренний редактор ячеек грида (TInplaceEditor) не всегда самый удобный вариант. Можно использовать собственные диалоговые окна для выбора значений и их редактирования, а можно просто встроить нужный компонент в сам грид. Вот этим мы сейчас и займемся.



Метки колонок: рисуем в заголовке TCheckBox или TRadioButton


Вновь вернемся к заголовкам. Допустим нам надо реализовать возможность как-то отметить колонку. В принципе для таких целей может служить два контрола TCheckBox и TRadioButton. Для рисования в заголовках воспользуемся специальным событием нашего нового грида: OnDrawTitleRect

procedure TfExDBG.OnDrawTitleRect(Sender: TObject; ACol: Integer; Column: TColumn; ARect: TRect); Var Style, TypeButton : Word; FRect : TRect; begin IF ACol >= TexDBGrid(Sender).FixedCols Then Begin InflateRect(ARect, -1, -1); TDBGrid(Sender).Canvas.FillRect(ARect); // Ширина прямоугольника для рисования контрола - 20 пикселей FRect:=ARect; IF RectWidth(FRect) > 20 Then FRect.Right:=FRect.Left + 20; // Определяем отмечено или нет текущее поле IF Column.Field.Tag = 1 Then Style:=DFCS_CHECKED Else Style:=0; // Выбираем тип контрола для отметки колонки IF FTitleIsCheckBox Then TypeButton:=DFCS_BUTTONCHECK Else TypeButton:=DFCS_BUTTONRADIO; // Рисуем отметку DrawFrameControl(TDBGrid(Sender).Canvas.Handle, FRect, DFC_BUTTON, TypeButton OR Style); FRect.Left:=FRect.Right + 1; FRect.Right:=ARect.Right; // Текст заголовка WriteText(TDBGrid(Sender).Canvas,FRect,Column.Title.Caption,Column.Title.Alignment); End; end;

Обработку нажатия на метку колонки проводим в обработчике события OnMouseUp. В приведенном примере для хранения отметки столбца используется свойство TField.Tag. Естественно, это только один из возможных вариантов.

procedure TfExDBG.GridFixMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); Const MinX = 2; MaxX = 20; Var Row, Col , i : Integer; Grid : TexDBGrid; Begin Grid:=TexDBGrid(Sender); // Получим номер строки и столбца грида, над которыми произошел клик мышкой Grid.MouseToCell(X,Y,Col,Row); IF Button = mbLeft Then Begin // Левая кнопка мыши — проверяем попадание в заголовок // и обязательное попадание на сам крыжик IF (Row = 0) AND (Col > Grid.FixedCols ) AND (Grid.Columns[Col - 1].Field <> nil) Then Begin Dec(X, Grid.TitleRect(Col-1).Left); // Проверяем попадание в область крыжика IF (X > MinX) and (X < MaxX) Then Begin Tag:=Grid.Columns[Col - 1].Field.Tag; // Снимаем отметку со всех колонок (если это TRadioButton) IF NOT FTitleIsCheckBox Then For i:=0 To Grid.Columns.Count - 1 Do Grid.Columns[i].Field.Tag:=0; // И отмечаем текущую Grid.Columns[Col - 1].Field.Tag:=1 - Tag; // Перерисовываем только заголовки, а не весь грид Grid.RefreshTitles; RefreshSelect; End; End; End; End;



НеОбычный TDBGrid


Игорь Шевченко, Елена Филиппова, Королевство Дельфи
10 июня 2003г. Подмена стандартного Inplace-Editor'a в DBGrid отдельным компонентом на примере TDBComboBox.

Материал рассчитан на начинающих программистов, которые хотят научиться не только использовать чужие компоненты, но и писать свои. Авторы ни в коем случае не отрицают положительные стороны использования сторонних компонентов, более того, нередко сами их используют. Тем не менее, придерживаются четкого мнения, что если хочешь контролировать ситуацию — нужно знать "как оно там все работает". Обычный TDBGrid можно превратить в мощный инструмент своими руками, заточив его под определенные задачи. Именно этому и посвящена наша статья. Итак, создаем из стандартного компонента необычный грид :о)



Подмена стандартного Inplace-Editor'a в DBGrid отдельным компонентом на примере TDBComboBox.


Для того, чтобы вместо стандартного редактора в колонке DBGrid'а появился другой компонент, проделаем несколько действий: Создадим отдельный компонент, который будет редактором (в примере используется TDBComboBox). При его создании следует установить свойство Visible в False, для того, чтобы вне грида он не отображался.
Компонент DBComboBox выбран для того, чтобы обеспечить автоматическую связь с данными в DataSet'е, который отображается в Grid'е.

При создании компонента, свяжем его с тем же набором данных, что и Grid, в качестве DataField установим имя того поля, редактор которого в гриде мы хотим подменять. Вместо создания вручную компонент можно положить на форму в design-time

FEditor := TDBComboBox.Create(Self); FEditor.Parent := Self; FEditor.Visible := false; FEditor.Style := csDropdownList; FEditor.DataSource := DBGrid.DataSource; FEditor.DataField := 'STATE';
В данном примере список ComboBox'а заполняется значениями из Picklist нужного столбца грида.

for I:=0 to Pred(DBGrid.Columns.Count) do if DBGrid.Columns[I].Field.FieldName = FEditor.DataField then begin { Присвоение списка PickList списку строк ComboBox'a } FEditor.Items.Assign(DBGrid.Columns[I].PickList); Break; end;
Показывать этот компонент мы будем в обработчике события OnDrawColumnCell, когда нужная колонка получает фокус (рис. 2).
рис. 2

procedure TForm1.DBGridDrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); begin if (gdFocused in State) then if (Column.Field.FieldName = FEditor.DataField) then begin { Вместо стандартного InplaceEditor'а показываем ComboBox } FEditor.Left := Rect.Left + DBGrid.Left; FEditor.Top := Rect.Top + DBGrid.top; FEditor.Width := Rect.Right - Rect.Left + 2; FEditor.Visible := True; end; end;

Для того, чтобы нарисованный компонент не оставался видимым после того, как нужная ячейка потеряет фокус, спрячем его в обработчике события ColExit

procedure TForm1.DBGridColExit(Sender: TObject); begin { При выходе с поля ComboBox надо скрыть } if DBGrid.SelectedField.FieldName = FEditor.DataField then FEditor.Visible := false; end;

Для того, чтобы менять значение поля можно было не только выбором мышью из списка, но и с клавиатуры, необходимо передавать ComboBox'у нажатия клавиш DBGrid'а, при редактировании поля. Это можно сделать как в обработчике события OnKeyPress DBGrid'a, так и в обработчике OnKeyDown. Я приведу пример обработчика OnKeyPress.

procedure TForm1.DBGridKeyPress(Sender: TObject; var Key: Char); begin { Передаем все нажатия клавиш в InplaceEditor'е созданному ComboBox'у } if (Key <> chr(9)) then if (DBGrid.SelectedField.FieldName = FEditor.DataField) then begin FEditor.SetFocus; SendMessage(FEditor.Handle, WM_CHAR, word(Key), 0); end; end;

В примере использован TDBComboBox, по аналогии с ним можно использовать для редактирования и другие компоненты. Ниже на рисунке показан пример, где аналогичным образом в грид встроен TDBDateEdit для редактирования полей типа "дата":



Рисование многострочных заголовков с использованием стандартного компонента TDBGrid.


При использовании стандартного компонента TDBGrid для рисования доступна только область данных колонок, изначально не включающая в себя фиксированные области TDBGrid, рисующиеся самим компонентом. Зная тот факт, что при событиях рисования доступна вся клиентская область окна, можно попробовать обмануть компонент и рисовать в другой области, чем та, которая передается процедуре рисования. Так как событие OnDrawCell вызывается для каждой ячейки Grid'а, а заголовки желательно рисовать один раз, заводим массив признаков нарисованных заголовков: GridTitles : : array of Boolean; Обработчик события OnDrawColumnCell выглядит достаточно просто:

procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); begin if not GridTitles[Column.Index] then DrawGridTitle(Column.Index); end;

Если заголовок колонки не нарисован, то нарисовать его. Процедура рисования должна определить координаты области заголовка и ее размеры и заново перерисовать эту область. Сама процедура оформлена как локальная, для того, чтобы не передавать параметры, переданные обработчику события. Для простоты заголовок делается двухстрочным, но ничего не мешает рисовать произвольное количество строк. RowCount объявлено константой и равно 2.

procedure DrawGridTitle(ColIndex : Integer); var Titles : array[1..RowCount] of String; ARect : TRect; { Собственно область заголовка } RH : Integer; { Высота области заголовка } BlankPos : Integer; { Позиция разбиения заголовка } begin BlankPos := Pos(' ', Column.Title.Caption); if BlankPos <> 0 then begin { Рисуем многострочный заголовок только для тех колонок, у которых есть пробел в названии. Заголовки остальных колонки DBGrid нарисует сам. } Titles[1] := Copy(Column.Title.Caption, 1, BlankPos-1); Titles[2] := Copy(Column.Title.Caption, BlankPos+1, Length(Column.Title.Caption) - BlankPos); RH := RectHeight(Rect); { В прямоугольнике Rect передаются координаты текущей ячейки, область для рисования заголовка можно получить, указывая в качестве вертикальной координаты 0. Высота области рисования сейчас равна высоте стандартной ячейки DBGrid, как раз на одну строку заголовка. } SetRect(ARect, Rect.Left, 0, Rect.Right, RH); InflateRect(ARect, -2, -2); { Поправка на окантовку Titles } Dec(RH, 2); { Смещение для отступа текста от края по вертикали } with DBGrid1.Canvas do begin Brush.Color := DBGrid1.FixedColor; FillRect(ARect); { Залить область заголовка, стерев все, что там нарисовано DBGrid'ом } { Рисование первой строки в заголовке } ARect.Bottom := RH; DrawText(Handle, PChar(Titles[1]), -1, ARect, DT_CENTER or DT_SINGLELINE); { Рисование второй строки в заголовке, предварительно сместив область рисования вниз на размер строки. } OffsetRect(ARect, 0, RH-2); DrawText(Handle, PChar(Titles[2]), -1, ARect,DT_CENTER or DT_SINGLELINE); end; end; GridTitles[ColIndex] := true; //Нарисовали заголовок для этой колонки end;

Высота любой строки любого наследника TCustomGrid определяется свойством RowHeights[номер строки]. Так как это свойство объявлено protected, для того, чтобы высота области заголовков DBGrid'а была большая, чем стандартная, используется обычный прием доступа к защищенным свойствам компонента, с описанием наследника от требуемого класса и повышением области видимости требуемого свойства: type THackGrid = class(TCustomGrid) public property RowHeights; end; Высоту области надо задать один раз, что и делается в обработчике события FormShow

procedure TForm1.FormShow(Sender: TObject); var .... H : Integer; { Определение необходимой высоты строки для многострочных заголовков } H := DbGrid1.Canvas.TextHeight('gW'); THackGrid(DBGrid1).RowHeights[0] := (H + 2) * RowCount; { RowCount принудительно объявлено 2 } end;
Результат работы:
рис. 1

После первого запуска программы обнаружен интересный эффект - при переключении на другое окно и обратном переключении на окно с Grid'ом многострочность заголовков пропадает. Аналогичным образом она пропадает при перемещении по гриду с помощью вертикального и горизонтального ScrollBar'ов. Для события переключения окна положение можно исправить, указав необходимость перерисовки заголовков в событии FormActivate, со ScrollBar'ами бороться придется подменой оконной процедуры DBGrid'а. Сделаем метод формы, сбрасывающий признаки рисования у всех заголовков:

procedure TForm1.InvalidateGridTitles; var I : Integer; begin for I:=0 to Pred(DBGrid1.Columns.Count) do GridTitles[I] := false; end;
И будем вызывать его каждый раз, когда потребуется полная перерисвока заголовков. procedure TForm1.FormActivate(Sender: TObject); begin InvalidateGridTitles(); end; И в подмененной оконной процедуре DBGrid'а:

procedure TForm1.GridWndProc(var Message: TMessage); begin case Message.Msg of WM_ERASEBKGND, WM_VSCROLL: InvalidateGridTitles(); WM_HSCROLL: begin InvalidateGridTitles(); // сожалению, приходится мириться с необходимостью перерисовки всего // DBGrid'а при горизонтальном скроллинге, иначе, все усилия по рисованию // многострочных заголовков пропадают :-( InvalidateRect(GridWnd, nil, true); end; end; with Message do Result := CallWindowProc(OldWndProc, GridWnd, Msg, wParam, lParam); end;

В первом варианте при обработке собщения WM_HSCROLL не был написан код для перерисовки всего окна DBGrid. Как я ни старался, победить ситуацию пропадания многострочных заголовков мне не удалось, поэтому и был добавлен код, принудительно перерисовывающий все окна DBGrid.



Рисование многострочных заголовков с использованием наследника компонента TDBGrid.


В отличие от рисования нестандартных заголовков при использовании стандартного компонента TDBGrid, в наследнике такое рисование выполняется проще, так как в компоненте есть виртуальный метод DrawCell, который вызывается для всех ячеек грида, а не только для содержащих данные. Рисование нестандартных заголовков в этом случае выполняется в перекрытом методе DrawCell в наследнике.

Кроме того, так как метод DrawCell вызывается гридом при любой его перерисовке, затрагивающей клиентскую область окна, нет нужды отслеживать, какие заголовки были нарисованы или обновлять все окно грида при скроллинге. Наше рисование будет вызвано только тогда, когда возникнет реальная необходимость в отрисовке области заголовков грида.

procedure THSDBGrid.DrawCell(ACol, ARow: Integer; ARect: TRect; State: TGridDrawState); var TitleText : String; { Полный заголовок } Titles : array of String; { Части заголовка } { Разбиение полного заголовка на части с возвращением числа получившихся частей } function SplitTitle : Integer; const TitleSeparator = ' '; { Можно этот символ вынести в published property } var CurPos, J: Integer; CurStr: string; begin SetLength(Titles, FTitleLines); { Определяем, сколько реально строк присутсвует в заголовке. Просто считается количество символов TitleSeparator } J := 0; CurStr:= TitleText; repeat CurPos:= Pos(TitleSeparator, CurStr); if (CurPos > 0) and (J < Pred(FTitleLines)) then begin Titles[J] := Copy(CurStr, 1, Pred(CurPos)); CurStr:= Copy(CurStr, CurPos+Length(TitleSeparator), Length(CurStr)-CurPos-Length(TitleSeparator)+1); Inc(J); end else begin Titles[J] := CurStr; if J >= Pred(FTitleLines) then { Не надо копировать больше, чем может вместить заголовок } Break; end; until CurPos=0; Result := J+1; end; var DataCol, I, TitleParts : Integer; TextRect : TRect; LineHeight : Integer; begin if (dgTitles in Options) AND (gdFixed in State) AND (ARow = 0) AND (ACol <> 0) then begin { Должна быть нарисована ячейка заголовка } { Стандартное действие DBGrid } if csLoading in ComponentState then begin Canvas.Brush.Color := Color; Canvas.FillRect(ARect); Exit; end; DataCol := ACol; if dgIndicator in Options then Dec(DataCol); { Изменение размеров области заголовка под окантовку, если хочется сделать плоские заголовки, то InflateRect надо пропустить } if [dgRowLines, dgColLines] * Options = [dgRowLines, dgColLines] then InflateRect(ARect, -1, -1); TitleText := Columns[DataCol].Title.Caption; Canvas.Brush.Color := FixedColor; { Если захочется сделать прозрачный заголовок, то вызов FillRect надо будет пропустить } { Если будет желание рисовать фоновую картинку в области заголовка, то нарисовать ее можно здесь } Canvas.FillRect(ARect); { Теперь можно нарисовать собственно текст } Canvas.Font := Font; if FTitleLines = 1 then begin WriteText (Canvas, ARect, 1, 1, TitleText, Columns[DataCol].Title.Alignment); end else begin TitleParts := SplitTitle(); TextRect := ARect; LineHeight := RectHeight(ARect) DIV TitleParts; TextRect.Bottom := TextRect.Top + LineHeight; for I:=0 to Pred(TitleParts) do begin WriteText (Canvas, TextRect, 1, 0, Titles[I], Columns[DataCol].Title.Alignment); OffsetRect(TextRect, 0, LineHeight); end; end; { Окантовка ячейки заголовка, если хочется сделать плоские заголовки, то DrawEdge надо пропустить } if [dgRowLines, dgColLines] * Options = [dgRowLines, dgColLines] then begin InflateRect(ARect, 1, 1); DrawEdge(Canvas.Handle, ARect, BDR_RAISEDINNER, BF_BOTTOMRIGHT); DrawEdge(Canvas.Handle, ARect, BDR_RAISEDINNER, BF_TOPLEFT); end; DoDrawTitleCell (DataCol, Columns[DataCol], ARect); end else inherited; end;

Кроме того, появляется возможность вызывать пользовательское событие при рисовании области заголовков, причем после того, как заголовок уже нарисован самим компонентом.

Задание высоты заголовков в наследнике также выполняется проще, так как имеется доступ к защищенным свойствам родительского компонента.

procedure THSDBGrid.CalcTitleHeight; begin if dgTitles in Options then RowHeights[0] := (Canvas.TextHeight('gW') + 2) * FTitleLines; end;

Высоту области заголовка необходимо задавать один раз при создании окна грида и каждый раз, при изменении свойств грида, влияющих на его внешний вид. При создании окна и при изменении свойств грида вызываются виртуальные методы CreateWnd и LayoutChanged, в перекрытые версии которых добавлен вызов процедуры CalcTitleHeight.



Рисуем ячейку в стиле заголовка в любом месте TDBGrid



Добавим нашему гриду еще один метод — DrawCellButton, который будет рисовать в любой ячейке 3D-окантовку, то есть делать имитацию заголовка. Передавать в нее будем прямоугольник этой ячейки, текст, выравнивание текста, шрифт, которым текст будет выведен и состояние (State) грида. Состояние нам понадобится для нормальной работы с фиксированными колонками.

procedure TexDBGrid.DrawCellButton(Rect: TRect; Text: String; Style: TFontStyles; State: TGridDrawState; Alignment: TAlignment); Var Shift : Integer; begin //Очищаем ячейку Canvas.Brush.Color:=clBtnFace; Canvas.Font.Color:=clBtnText; Canvas.Font.Style:=Style; Canvas.FillRect(Rect); // Если ячейка фиксирована, то мы получим TRect меньшего размера, // чем для обычной ячейки. Это нужно учесть Shift:=-2 + ORD(gdFixed In State); // вписываем текст InflateRect(Rect,Shift,0); WriteText(Canvas, Rect, Text , Alignment ); InflateRect(Rect,(-1)*Shift,0); // рисуем по размеру ячейки button // только если это не фиксированная ячейка, так как для нее окантовка уже нарисована IF NOT (gdFixed in State) Then Begin // Рисуем аналог разделительных линий между фиксированными ячейками грида // (они рисуются черным цветом, в отличие от серых линий между ячейками // данных (grids.pas)) InflateRect(Rect, 1, 1); Rect.Top:=Rect.Top + 1; FrameRect(Canvas.Handle, Rect, GetStockObject(BLACK_BRUSH)); Rect.Top:=Rect.Top - 1; // Закончили имитацию линий между фиксированными ячейками. InflateRect(Rect, -2, -2); Paint3dRect(Canvas.Handle, Rect); End; end;
Такой, на первый взгляд экзотический, вариант ячейки поможет нам создать видимость внутренних группировок в гриде (рис. 4).


рис. 4



Синхронизация размеров и положения колонок двух гридов


Задача состоит в том, чтобы заставить два TDBGrid, расположенных один под другим, полностью синхронизировать свою работу с колонками: изменение размеров колонок и их перемещение должно происходить в обоих гридах отдновременно. Самое распространенное применение этой задачи в отображении грида с данными и грида с итогами (см. рис. 3). Верхний грид содержит список всех стран с данными по площади и населению(MainGrid), нижний — список, где эти же данные сгруппированы по континентам(TotalGrid).
рис. 3

При синхронизации действий будем считать, что тот грид, который инициирует это действие — ведущий, а второй в этой ситуации — ведомый. Чтобы не зациклить синхронизацию, введем дополнительную переменную: SynchProccesed : Boolean; Для синхронизации необходимо обработать три события: Изменение позиции колонки; Горизонтальный скролинг(изменение колонки, которая оказывается первой видимой в гриде); Изменение ширины колонки. Для отслеживания перемещения колонок воспользуемся событием OnColumnMoved. Синхронизацию проведем незатейливо: полностью перепишем колонки ведомого грида, взяв за основу колонки ведущего:

//-------------------------------------------------------------------------------------------------- procedure TfExDBG.mainGridColumnMoved(Sender: TObject; FromIndex, ToIndex: Integer); Var Grid : TDBGrid; begin // TDBGrid(Sender) инициирует перемещение колонок, он — ведущий грид // определяем "ведомый" грид IF TDBGrid(Sender).Name = 'TotalGrid' Then Grid:=MainGrid Else Grid:=TotalGrid; // Сейчас ведомому гриду не нужно реагировать на изменение его колонок, // инициируя в свою очередь синхронизацию с другим гридом SynchProccesed:=True; Grid.Columns.Assign(TDBGrid(Sender).Columns); // Синхронизация завершена SynchProccesed:=False; end; //--------------------------------------------------------------------------------------------------

Для отслеживания горизонтального скролинга как нельзя лучше подходит метод TCustomDBGrid.TopLeftChanged. К сожалению, в стандартном TDBGrid этот метод не доступен (protected). Поэтому, лучшим вариантом будет не мучить стандартный грид, а создать собственного наследника. Положительные стороны этого способа уже описывались в начале статьи.

TexDBGrid = class(TDBGrid) private FOnTopLeftChanged : TNotifyEvent; ... public Procedure TopLeftChanged; override; ... published Property OnTopLeftChanged : TNotifyEvent read FOnTopLeftChanged write FOnTopLeftChanged; ... End; ... //-------------------------------------------------------------------------------------------------- Procedure TexDBGrid.TopLeftChanged; Begin Inherited; IF Assigned(FOnTopLeftChanged) then FOnTopLeftChanged(Self); End;

Теперь нам доступно событие OnTopLeftChanged. Синхронизация заключается в том, чтобы сделать первой видимой колонкой ведомого грида ту же колонку, что и у ведущего. Для этого нам понадобится свойство TCustomGrid.LeftCol (см. help). Это свойство protected, но так как мы создаем собственного наследника от TDBGrid, то повысить его видимость нам не составит труда.

//-------------------------------------------------------------------------------------------------- procedure TfExDBG.GridTopLeftChanged(Sender: TObject); Var Grid : TexDBGrid; begin IF NOT SynchProccesed Then Begin // TDBGrid(Sender) инициирует скролинг, он — ведущий грид // определяем "ведомый" грид IF TDBGrid(Sender).Name = 'TotalGrid' Then Grid:=MainGrid Else Grid:=TotalGrid; SynchProccesed:=True; Grid.LeftCol:=TexDBGrid(Sender).LeftCol; SynchProccesed:=False; End; end; //--------------------------------------------------------------------------------------------------

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

//-------------------------------------------------------------------------------------------------- Procedure TfExDBG.SynchronizeGrids( MasterGrid , SlaveGrid : TDBGrid ); Var i : Integer; Begin IF NOT SynchProccesed Then Begin SynchProccesed:=True; For i:=0 To MasterGrid.Columns.Count - 1 Do SlaveGrid.Columns[i].Width:=MasterGrid.Columns[i].Width ; SynchProccesed:=False; End; End; //--------------------------------------------------------------------------------------------------
А вот в какой момент применить этот метод? Ведь у грида нет события OnResizeColumn... Внимательно изучив help, обратим внимание на метод SetColumnAttributes: Sets the column widths and disables tabbing to cells that can’t be edited. procedure SetColumnAttributes; virtual; Description Applications cannot call this protected method. It is called automatically when the Columns property is recomputed, to adjust the column widths and ensure that the user can only tab to fields that can be edited.

Этот метод автоматически вызывается всякий раз, когда изменяются настройки колонок, в том числе их ширина. Мы нашли то, что нам нужно! По аналогии с OnTopLeftChanged создадим в нашем гриде событие OnSetColumnAttr:

TexDBGrid = class(TDBGrid) private FOnTopLeftChanged, FOnSetColumnAttr : TNotifyEvent; ... protected Procedure SetColumnAttributes; override; public Procedure TopLeftChanged; override; ... published Property OnTopLeftChanged : TNotifyEvent read FOnTopLeftChanged write FOnTopLeftChanged; Property OnSetColumnAttr : TNotifyEvent read FOnSetColumnAttr write FOnSetColumnAttr; ... End; ... //-------------------------------------------------------------------------------------------------- procedure TexDBGrid.SetColumnAttributes; begin inherited; IF Assigned(FOnSetColumnAttr) Then FOnSetColumnAttr(Self); end;
Обработаем это событие для обоих гридов:

//-------------------------------------------------------------------------------------------------- // Так как определять ведомый грид приходится не один раз, правильно выделить это в отдельный метод Function TfExDBG.GetSlaveGrid( MasterGrid : TexDBGrid) : TexDBGrid; Begin // MasterGrid инициирует синхронизацию, он — ведущий грид // определяем "ведомый" грид IF MasterGrid.Name = 'TotalGrid' Then Result:=MainGrid Else Result:=TotalGrid; End; //---------------------------------------------------------------------------------------- Procedure TfExDBG.OnSetColumnAttr(Sender: TObject); Begin IF NOT SynchProccesed Then SynchronizeGrids( TexDBGrid(Sender) ,GetSlaveGrid(TexDBGrid(Sender)) ); End; //----------------------------------------------------------------------------------------
Ну а теперь, пробуйте! :о)

Для того, чтобы расслабиться перед следующим "броском", пристроим к нашему гриду несколько простых, но приятных бантиков :о)



Сложные заголовки


Изначально наш грид выглядит вот так:



Для того, чтобы добавить объединяющие заголовки для существующих, совершенно явно следует увеличить по высоте область заголовков грида.



И в нужном месте дорисовать самим объединяющую часть заголовка.



Реализация описанной методики в нашем наследнике TexDBGrid: Введем свойство, которое будет включать/выключать режим сложных заголовков.

TexDBGrid = class(TDBGrid) private FSubHeader : Boolean; // подзаголовки ... published Property SubHeader : Boolean read FSubHeader write SetSubHeader;
Именно это свойство будет регулировать высоту области заголовков.

... Const TITLE_SUBHEADER = 2; TITLE_DEFAULT = 1; ... //******************************************************* procedure TexDBGrid.CalcTitle; begin RowHeights[0] := 19 * FTitleLines ; end; //******************************************************* procedure TexDBGrid.SetSubHeader(const Value: Boolean); begin FSubHeader := Value; IF FSubHeader Then FTitleLines:=TITLE_SUBHEADER Else FTitleLines:=TITLE_DEFAULT; CalcTitle; end;

В метод TexDBGrid.DrawCell добавляем обработку

IF FSubHeader Then Begin // Рисуем объединяющий заголовок Header к мелким заголовкам Title DrawSubHeader(DataCol, Canvas); // Рисуем заголовки Title FRect:=ARect; FRect.Top:=RectHeight(ARect) div FTitleLines; DrawTitleCell(FRect,Columns[DataCol]); End Else DrawTitleCell(FRect,Columns[DataCol]);
Здесь рисование заголовка разбито на две процедуры: DrawSubHeader и DrawTitleCell. Где DrawTitleCell рисует в прямоугольнике 3D-окантовку, заливает его цветом FixedCols и вписывает текст. То есть имитирует обычный заголовок колонки. А вот на процедуре DrawSubHeader остановимся поподробнее.

Для того, чтобы нарисовать объединяющий заголовок для нескольких колонок, нужно получить прямоугольник (TRect), который объединяет эти колонки и текст, который следует писать в объединяющем заголовке. Для обеспечения гибкой настройки создадим два свойства:

published Property OnGetHeaderText : TOnGetHeaderText read FOnGetHeaderText write FOnGetHeaderText; Property OnGetHeaderRect : TOnGetHeaderRect read FOnGetHeaderRect write FOnGetHeaderRect;
С помощью этих свойств можно будет настраивать обработчики соответствующих событий.

Procedure DrawSubHeader(ACol : Integer; Canvas : TCanvas); Var HRect : TRect; Begin // Получаем прямоугольник, объединяющий несколько колонок, // для которых рисуем сложный заголовок HRect:=GetHeaderRect(ACol); // По высоте берем только часть прямоугольника // так как вторая часть — обычный заголовок HRect.Bottom:=RectHeight(HRect) div TITLE_SUBHEADER; Canvas.FillRect(HRect); // Вписываем текст, // который получаем методом GetHeaderText InflateRect(HRect,-1,-1); WriteText(Canvas, HRect, GetHeaderText(ACol) , taCenter); // Рисуем 3D-окантовку Paint3dRect(Canvas.Handle,HRect); End;
Внутри методов GetHeaderRect и GetHeaderText будут вызываться обработчики событий FOnGetHeaderRect и FOnGetHeaderText.

При этом, следует помнить, что в каждый момент могут быть видны не все колонки из объединенных в блок. Воспользуемся функцией TCustomDBGrid.CalcTitleRec, которая возвращает прямоугольник для определенной колонки и строки. Если в данный момент эта колонка не видна, то будет возвращен нулевой прямоугольник.

Function TexDBGrid.GetHeaderRect(ACol : Integer) : TRect; Var MasterCol : TColumn; Index,Shift , Count,i : Integer; Begin // Если в опциях отключен показ сетки, это нужно учесть при расчете // общего прямоугольника IF [dgColLines] * Options = [dgColLines] Then Shift:=1 Else Shift:=0; Index:=ACol; Count:=1; // получаем информацию для текущей колонки грида: // в какой объединяющий блок она входит // Index — с какой колонки начинается объединяющий блок // Count — сколько колонок он включает IF Assigned(FOnGetHeaderRect) Then FOnGetHeaderRect(ACol, Index, Count); IF Index+Count-1 > Columns.Count-1 Then Begin Index:=ACol; Count:=1; End; // В результате нужно получить прямоугольник, состоящий из // всех, включенных в объединенный блок колонок Result:=CalcTitleRect(Columns[Index],0,MasterCol); For i:=Index+1 To Index + Count -1 Do Result.Right:=Result.Right + RectWidth(CalcTitleRect(Columns[i] ,0,MasterCol)) + Shift; End;

И для примера покажем, как именно могут использоваться обработчики событий получения объединяющего прямоугольника и текста при использовании сложных заголовков:

Const GeoColumns = 3; ParamColumns = 2; ... //---------------------------------------------------------------------------------------- // Получить для текущей колонки информацию о том, в какое объеденение колонок она попадает //---------------------------------------------------------------------------------------- procedure TfExDBG.GetHeaderRect(ACol: Integer; var IndexStart, Count: Integer); begin IF ACol < GeoColumns Then Begin IndexStart:=0; Count:=GeoColumns; End Else Begin IndexStart:=GeoColumns; Count:=ParamColumns; End end; //---------------------------------------------------------------------------------------- // Получить для текущей колонки текст заголовка объеденени //---------------------------------------------------------------------------------------- procedure TfExDBG.GetHeaderText(ACol: Integer; var Text: String); begin IF ACol < GeoColumns Then Text:='География' Else Text:='Параметры'; end; //----------------------------------------------------------------------------------------

Предложенный способ просто один из возможных, он не позволяет настраивать параметры объединяющих заголовков в design-time, рассчитан на использование двухуровневых заголовков и предполагает наличие сложных заголовков у всех колонок грида.

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

Рассказать о реализации всех вариантов сложных заголовков не представляется возможным. Изучив наши примеры, Вы можете сами совершенствовать новый грид, по собственному усмотрению.



Выделение цветом текущей строки


А теперь снова вернемся к заголовкам и пойдем по дорожке, только что проложенной в самом начале статьи. Если мы умеем рисовать в заголовках, то мы можем очень многое, практически все :о)
На изображен грид со сложными заголовками. Разберем один из возможных способов достижения такого результата.



Вызываем разные меню для заголовков и области данных


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

procedure TexDBGrid.MouseToCell(X, Y: Integer; var ACol, ARow: Integer); Var Coord: TGridCoord; Begin Coord := MouseCoord(X, Y); ACol := Coord.X; ARow := Coord.Y; End;
И теперь обработаем событие OnMouseUp:

//---------------------------------------------------------------------------------------- procedure TfExDBG.GridMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); Var Row, Col : Integer; APoint : TPoint; Grid : TexDBGrid; begin Grid:=TexDBGrid(Sender); // Получим номер строки и столбца грида, над которыми произошел клик мышкой Grid.MouseToCell(X,Y,Col,Row); IF Button = mbRight Then // Если мышка не попала на незаполненную область грида IF (Col >= 0) AND (Row >=0 ) Then Begin // Нажатие правой кнопки мыши, проверяем какое меню требуется вызвать IF Row = 0 Then Grid.PopUpMenu:=pmTitle Else Grid.PopUpMenu:=pmData; // Получаем из координат мыши(относительно грида — клиентские координаты) // экранные координаты для всплывающего меню APoint := Grid.ClientToScreen(Point(X,Y)); Grid.PopUpMenu.Popup(APoint.X,APoint.Y); End; end; //--------------------------------------------------------------------------------------------------



Запрет перемещения колонок с разрешением менять их ширину


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

В опциях грида объединены запрет/разрешение на передвижение колонок и на изменение их ширины (dbColumnResize). Если запретить перемещать колонки, тогда нельзя будет менять их ширину. В нашем случае это неудачное сочетание будет крайне неудобно с точки зрения пользователя. Введем еще одно поле, которое будет отдельно запрещать перемещение колонок:

TexDBGrid = class(TDBGrid) private ... FAllowColumnMoved: Boolean; ... public Property AllowColumnMoved : Boolean read FAllowColumnMoved write SetAllowColumnMoved;
Изучив исходные коды DBGrids.pas, обратим внимание на метод BeginColumnDrag (см. help). Этот метод вызывается тогда, когда начинается перетаскивание колонок.

Переопределим его в нашем наследнике:

function TexDBGrid.BeginColumnDrag(var Origin, Destination: Integer; const MousePt: TPoint): Boolean; Begin Result:=FAllowColumnMoved; // Разрешить передвигать колонки только если это разрешено в настройках: AllowColumnMoved IF Result Then Result:= Inherited BeginColumnDrag(Origin,Destination,MousePt); End;
Так как мы контролируем непосредственно начало процесса перемещения, то возможность менять ширину колонок остается у пользователя.