Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Создание САПР на базе продуктов Autodesk


Информационный Канал Subscribe.Ru


Информационная рассылка сайта [CadDev.Narod.Ru]

(с) 2004, Калугин Сергей Сергеевич

По просьбам активистов ...(продолжение)

Сегодняшняя и завтрашняя рассылки бпродолжат рассмотрение вопросов интеграции приложений, что относится к некторым подсистемам САПР.

В этом номере:


Компоненты ADO. Часть II

Дмитрий Ю. Сидоров

Простейшее приложение

  • cоздадим простейшее приложение, состоящее из одной таблицы.
  • cоздаем форму состоящую из трех компонент
  • TADOTable с палитры компонент ADO,
  • TDataSource с палитры компонент Data Access,
  • TDBGrid cпалитры компонент Data Controls.

Рис 7

  • cвязываем компоненты, устанавливая
  • свойство TDBGridDataSource на компонент TDataSource,
  • свойство DataSet компонента TDataSource на компонент TADOTable.

Теперь нам необходимо указать базу данных. Делается это в свойстве ConnectionString компонента TADOTable. При нажатии на кнопку “” появится редактор параметров соединения. Отметим радокнопку “Use data link file”, нажмите на кнопку “Browse…” и выберите в появившемся окне после файл линка к базе данных “\Program Files\Common Files\System\ole db\Data Links\DBDEMOS.UDL”. Этот линк указывает на базу в формате MS Access, входящую в поставку Delphi.

После этого в свойстве TableName компонента TADOTable выберем таблицу customer.

Активизируем компонента TADOTable, установив свойство Active в True.

Приложение можно запускать. Этот пример можно найти в директории Simple.

Обзор ADO

ADO основано на технологии COM. Все объекты и интерфейсы ADO являются интерфейсами и объектами COM.

Рис 8. Архитектура ADO

Интерфейс Connection

Объекты этого типа выполняют следующие функции:

  • Связь с сервером.
  • Управление транзакциями.
  • Получение информации о произошедших ошибках (свойство Errors).
  • Получение информации о схеме данных (таблицы, поля и так далее).

Рис 9. Схема взаимодействия в ADO основных COM интерфейсов

Интерфейсы Recordset и Field

Интерфейс Recordset (на нижнем уровне ADO это IRowset) является аналогом TDataSet в Delphi.

Поддерживает текущее положение и перемещение курсора, закладки (bookmarks), чтение, изменение и удаление записей и так далее. Значение полей и их типы доступны с помощью свойства Fields.

Интерфейс Field позволяет получать значение поля, его тип длину и так далее.

Интерфейсы Command и Parameter

Эти два типа позволяют работать с командами источника данных. Синтаксис команд для каждого из источников свой.

Интерфейс Property

Все объекты, кроме Parameter, имеют свойство Properties, которое позволяет получать и устанавливать параметры специфические для провайдера данных.

Библиотека довольно запутанная, многие функции дублированы в разных объектах. Например, Recordset можно создавать напрямую, методом Open, (причем предварительно создавать Connection не обязательно), можно получить как результат выполнения метода Command.Execute, либо после Connection.Execute задав команду без параметров.

Интерфейс Command инкапсулирован во все компоненты за исключением TADOConnection. Это сделано потому, что в ADO нет возможности получить данные не выполнив команду.

Интерфейс Recordset инкапсулирован в компоненты производные от TCustomADODataSet. Это компоненты TADODataSet, TADOTable, TADOQuery, TADOStoredProc.Получать данные из них возможно штатными средствами Delphi.

Возможно получение данных и при выполнении компонента TADOCommand. Метод этого компонента Execute возвращает тип _Recordset. После чего его можно, например, связать с компонентом TADODataSet следующим образом

ADODataSet1.RecordSet := ADOCommand1.Execute;

Компоненты TADOTable, TADOQuery и TADOStoredProc являются частными случаями команды, соответственно для таблицы, SQL запроса и хранимой процедуры.

Тип Connection инкапсулируется в компонент TADOConnection.

Когда вы выполняете команду предварительно не открывая соединение, оно все равно создается. Получить к нему доступ возможно через свойство Recordset. Привязать компонент TADOConnection к уже открытому соединению возможно через свойство ConnectionObject.

Информацию о структуре базы данных можно получить с помощью метода OpenSchema компонента TADOConnection. Эта информация представлена как набор таблиц, как стандартизованных, так и специфических для провайдера. Таким способом можно узнать список таблиц, запросов, хранимых процедур и многое другое. Однако изменять структуру базы с помощью возвращаемых наборов данных невозможно.

Пример использования TADOConnection

В этом примере рассматривается работа с компонентом TADOConnection, SQL запросами с параметрами и трансакциями.

Создадим приложение из следующих компонентов

  • Connect типа TADOConnection
  • MasterSQL и DetailSQL типа TADODataSet
  • MasterDS и DetailDS типа TDataSource
  • MasterGrid и DetailGrid типа TDBGrid

Рис 10. Master-detail форма на этапе дизайна

Связываем MasterGrid, MasterDS,MasterSQL и DetailGrid,DetailDS,DetailSQL аналогично предыдущему примеру, за исключением того, что вместо типа TADOTable используется тип TADODataSet.

Привязываем Connect к базе данных. Для этого в редакторе свойства ConnectionString выбираем ту же базу данных, что и в предыдущем примере.

Для ввода SQL запросов необходимо отредактировать свойство CommandText компонентах MasterSQL и DetailSQL. После нажатия на кнопку “” появится редактор компонент, который выглядит следующим образом

Рис. 10

Кнопка “Add Table to SQL” добавляет в текст SQL запроса таблицу, выбранную в списке “Tables”, а “Add Field to SQL” поле таблицы, выбранное в списке “Fields”.

Запрос для MasterSQL

select VendorNo, VendorName, Country, City, State, Preferred
from vendors

Запрос в DetailSQL должен выбирать только те детали, поставщик которых является текущим в MasterSQL. Для этого установим свойство DataSource компонента DetailSQL в значение MasterDS.

Запрос для DetailSQL следующий:

select PartNo, OnOrder, OnHand, ListPrice, Description, Cost
from parts
where VendorNo = :VendorNo

:VendorNo в части whereпараметр запроса. Параметры при установленном DataSource берутся из него.

Активизируем MasterSQL и DetailSQL аналогично предыдущему примеру.

Приложение можно запускать. Этот пример можно найти в директории MasterDetail.

Пример использования параметров запроса

Теперь ограничим выборку поставщиков по значению поляState. Для этого добавим к форме следующие компоненты StateEdit типа TEdit c вкладки Standard, QueryButton типа TButton c вкладки Standard

Изменим запрос в MasterSQL на

select VendorNo, VendorName, Country, City, State, Preferred
from vendors
where State = :StateID

:StateID – параметр, вместо которого при выполнении подставляется значение.

Добавим так же обработчик события OnClick в QueryButton следующего содержания

procedure TForm1.QueryButtonClick(Sender: TObject);
begin
 MasterSQL.Active := False;
 DetailSQL.Active := False;
 MasterSQL.Parameters.ParamByName('StateID').Value := StateEdit.Text;
 MasterSQL.Active := True;
 DetailSQL.Active := True;

end;

Программа готова. Этот пример можно найти в директории Param.

Синхронизация данных клиента и сервера.

В ADO используются три метода синхронизации данных на клиенте и сервере.

  • c помощью метода Resync, который повторно считывает записи набора. Этот метод используется при выполнении метода Refresh Delphi.
  • повторный запрос методом Requery, который заново выполняет запрос на сервере. Выполнение этого метода то же самое, что и выполнение подряд закрытия и открытия набора данных.
  • уведомление сервером клиента в случае изменения данных.

Этих методы доступны во всех компонентах имеющих набор данных. Однако эти функции доступны не для всех баз данных.


По волнам интеграции… II

Евгений Старостин
Автор XL Report - http://www.afalinasoft.com/rus/

Вот и лето уже перевалило через середину. Странное у меня получилось лето. Заказчики требовали напряжения, жена требовала отдыха, а работа - самоотдачи. За всем за этим я успел сделать несколько курсовых работ. Несколько?! Боже. Господа Студенты, отчего Вы не можете самостоятельно искать ответы на свои многочисленные вопросы? Впрочем, может быть, я старею, забывая меж всеми своими ежедневными делами о том времени, когда сам был таким. Это дождь виноват. Барабанит частенько по сливам окон, портя настроение. Вот и статья, наверняка, из-за этого получится скучной, а, может быть, и вовсе нудной.

Так о чем это я? Ах да, об интеграции этого самого Excel-а с нашим любимым средством разработки. Прямо и не знаю с чего начать….

Важно! В качестве примера я беру проект из предыдущей моей статьи и стану его понемногу расширять, отвечая на вопросы, появившиеся у специалистов разного профиля и кругозора. Эти вопросы получены мною из двух «источников»: как реакция на мою статью и, извините, из переписки по XL Report Support. Эти две вещи уж очень сильно пересекаются, поэтому я и обращаюсь к обоим источникам моего вдохновения. Я не буду последователен в своих рассуждениях, местами буду писать подробно, местами - кратко. Попросту, я опишу некоторые часто встречающиеся проблемы и решения этих проблем.

И еще! Я решил совсем опустить в своем пространном (как обычно) повествовании тонкости работы с Excel в Delphi 5.0, так как считаю, что работа с импортированной библиотекой типов принципиально одинакова и в версии 4, и в версии 5. Различен, разве что, только уровень импорта этой самой библиотеки. К тому же, я уже полностью «переехал» на Excel 2000, поэтому тестирую весь код, который приведен здесь, именно в нем. Итак, поехали.

Создание или открытие книги

Повторюсь, не смотря на то, что я уже писал об этом в предыдущей статье. В главной форме проекта-примера я объявил свойство IWorkbook. Оно будет содержать интерфейс книги, которую мы будем создавать и использовать. Естественно, в обработчике FormDestroy я его освобождаю.

property IWorkbook: Excel8TLB._Workbook read FIWorkbook;

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

if Assigned(IXLSApp) and (not Assigned(IWorkbook) ) then
   FIWorkbook := IXLSApp.Workbooks.Add(EmptyParam, 0);

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

Коллекция Workbooks содержит все открытые книги и предоставляет возможность кое-как управлять всем этим.

Боже, как убоги коллекции от Microsoft, и особенно поиск в них! Я отклонюсь, но это надо видеть. Вот пример поиска книги с заданным именем, приведенный как совет в MSDN Office Developer.

Public Function SheetExists(strSearchFor As String) As Boolean
SheetExists = False
For Each sht In ThisWorkbook.Worksheets
    If sht.Name = strSearchFor Then
        SheetExists = True
    End If
Next sht
End Function

Это вам не IndexOf писать. Сами ищите! А я так иделаю. Но, далее…

Метод Add этой коллекции (читай, метод интерфейса) позволяет добавить книгу к этой коллекции, пустую либо по шаблону. Первый параметр этого метода, Template (из справки по Excel VBA), может принимать имя файла с путем. Поэтому, выполнив код

if Assigned(IXLSApp) and (not Assigned(IWorkbook) ) then
  FIWorkbook := IXLSApp.Workbooks.Add(ExtractFilePath(ParamStr(0)) + 'Test.xls', 0);

вы получите книгу, идентичную файлу "Test.xls" с именем Test1.xls. Именно этим способом я создаю все свои отчеты, так как создаю их по заранее разработанным шаблонам. Естественно, что это шаблоны XL Report.

Если же необходимо просто открыть уже существующий файл, то используйте метод Open этой же коллекции:

if Assigned(IXLSApp) and (not Assigned(IWorkbook) ) then
  FIWorkbook := IXLSApp.Workbooks.Open(ExtractFilePath(ParamStr(0)) + "Test.xls', EmptyParam,
    EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam,
    EmptyParam, EmptyParam, EmptyParam, false, 0);

Понимаю, что в душе нормального программиста такой код вызовет отвращение. Как-то я даже получил гневное письмо о собственной ненормальности из-за того, что использую ранее связывание и кучу EmptyParam. Впрочем, я не сильно агрессивный человек (правда, только в переписке), и отвечать не стал. В конечном итоге, раннее связывание дает мне немного преимуществ, но я за него. Я не могу помнить все методы и их параметры из Excel Type Library, поэтому получаю их (только при раннем связывании, естественно) из подсказок редактора Delphi - продуманная вещь этот редактор. А чтобы не мучаться с написанием такого количества EmptyParam, можно написать и так (ответ на «гневное» письмо):

if Assigned(IXLSApp) and (not Assigned(IWorkbook) ) then
  IDispatch(FIWorkbook) := OLEVariant(IXLSApp.Workbooks).Open(
    FileName := ExtractFilePath(ParamStr(0)) + 'Test.xls');

Но, мы отклонились. Что же стоит за таким количеством параметров по умолчанию в методе Open? Да, много чего. Из этого «громадья» я использую лишь несколько вещей. Их я и опишу, а заинтересовавшихся остальными отсылаю к справке по Excel VBA. Вот объявление этого метода в импортированной библиотеке типов:

function Open(const Filename: WideString; UpdateLinks: OleVariant; ReadOnly: OleVariant;
              Format: OleVariant; Password: OleVariant; WriteResPassword: OleVariant;
              IgnoreReadOnlyRecommended: OleVariant; Origin: OleVariant;
              Delimiter: OleVariant; Editable: OleVariant; Notify: OleVariant;
              Converter: OleVariant; AddToMru: OleVariant; lcid: Integer): Workbook; safecall;

В FileName необходимо передать имя открываемого файла, желательно указав путь его нахождения. Иначе, этот файл Excel будет искать в каталоге по умолчанию. Чтобы файл был запомнен в списке последних открытых файлов, в AddToMru можно передать true. Иногда я знаю, что файл рекомендован только для чтения (не путать с «парольной» защитой книги). Тогда при открытии выдается соответствующее сообщение. Чтобы игнорировать его, можно передать в IgnoreReadOnlyRecommended true. Вот, пожалуй, и все мои скудные знания об этом методе. Впрочем, с помощью его мне приходилось открывать и файлы текстовых форматов с разделителями. Но тогда я обращался к чудесному «пишущему» плейеру VBA и записывал с его помощью макросы, затем правил их по необходимости и все отлично получалось. Этим же способом разрешать «всяческие» тонкие вопросы рекомендую и вам.

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

procedure TForm1.btnCreateBookClick(Sender: TObject);
var FullFileName: string;
begin
  FullFileName := ExtractFilePath(ParamStr(0)) + 'Test.xls';
  if Assigned(IXLSApp) and (not Assigned(IWorkbook) ) then
    try
      case rgWhatCreate.ItemIndex of
      // По шаблону
      0: FIWorkbook := IXLSApp.Workbooks.Add(FullFileName, 0);
      // Просто откроем
      1: FIWorkbook := IXLSApp.Workbooks.Open(FullFileName,
           EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam,
           EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, false, 0);
      // Пустая книга
      2: FIWorkbook := IXLSApp.Workbooks.Add(EmptyParam, 0);
      end;
    except
      raise Exception.Create('Не могу создать книгу!');
    end;
end;

Далее во всех примерах я подразумеваю, что вы всякий раз будете создавать новую книгу по шаблону с помощь кнопки "Create workbook". Книга-шаблон названа как прежде Test.xls и включена в проект. Все остальные примеры опираются именно на эту книгу и ее листы. В этой книге я подготовил кое-какие данные и поименованные области для последующих примеров работы с Excel. Для каждого примера кода я буду добавлять кнопку и, возможно, RadioGroup к ней с возможностью выбора варианта работы. Не судите меня строго за то, что главная и единственная форма проекта-примера получится громоздкой и некрасивой. Не это здесь главное. Итак, всегда создавайте по кнопке книгу. Далее нажимайте кнопку, на которую указывает конкретный пример кода, и наблюдайте. Буду рад, если кто-то из читателей создаст более приемлемый демонстрационный проект для этой статьи.

Работа с листами книги

Есть в VBA одна вещь, которая меня раздражает. Это ActiveSheet и ActiveWorkbook, а также возможность работы с Cells и Range без указания, к какому листу или книге они принадлежат. Одно время я боролся сам с собой, то применяя, то совсем отказываясь от подобных конструкций. Окончательно я отказался от этого лишь после обнаружения многочисленных ошибок в анализе «лога» моего Web-сервера, который я сделал на VBA. Благо, при работе в Delphi нет возможности написать Cells(x, y) = NewValue, подразумевая при этом какой-то неуловимый ActiveSheet. Поэтому прежде, чем работать с отдельными ячейками, я всегда получаю интерфейс на конкретный и вполне осязаемый лист книги. И делю это так:

var ISheet: Excel8TLB._Worksheet;

ISheet := IWorkbook.Worksheets.Item['Лист1'] as Excel8TLB._Worksheet;

Коллекция Worksheet подобна всем остальным коллекциям из Excel TLB. В ней вы можете удалять листы, вставлять новые, изменять их порядок. Но я практически никогда не делаю этого, поэтому всех нетерпеливых снова отсылаю к справке по Excel VBA.

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

Чтение данных из ячейки

Написав этот заголовок, я подумал о том, как часто я «беру» данные из книги. Это случается весьма редко, ибо Excel я использую как средство построения отчетов. То есть, намного чаще эти данные я туда передаю. Поэтому хотелось бы описать не столько чтение данных, сколько способы обращения к ячейкам. Я использую разные способы обращения к ячейкам от привычного в Excel Cells(x,y) до коллекции Names. Вот некоторые примеры:

procedure TForm1.btnReadDataClick(Sender: TObject);
var Value: OLEVariant;
    ISheet: Excel8TLB._Worksheet;
begin
  if Assigned(IWorkbook) then
    try
      ISheet := IWorkbook.Worksheets.Item['Лист1'] as Excel8TLB._Worksheet;
      try
        case rgWhatRead.ItemIndex of
          0: Value := ISheet.Cells.Item[2, 1].Value;
          1: Value := ISheet.Range['A2', EmptyParam].Value;
          2: Value := ISheet.Range['TestCell', EmptyParam].Value;
          3: Value := IWorkbook.Names.Item('TestCell', EmptyParam, 
               EmptyParam).RefersToRange.Value;
        end;
        ShowMessage(Value);
      finally
        ISheet := nil;
      end;
    except
      raise Exception.Create('Не могу прочитать данные!');
    end;
end;

На главную форму проекта я добавил кнопку, по которой можно прочитать данные из ячейки «А2» открытой книги, и RadioGroup к ней, чтобы выбрать способ получения этих данных. Из приведенного кода видна одна из «гнуснейших» моих привычек - освобождать все полученные интерфейсы явно (ISheet := nil). Я не могу побороть ее уже долгое время, поэтому прошу прощения у мастеров программирования на Delphi за то, что эта строчка здесь абсолютно лишняя.

Самый повторяющийся вопрос в моей почте, это вопрос о Cells. Почему-то многие уверены, что конструкция Cells[x, y].Value должна работать. В VBA это так. Но при раннем связывании это не соответствует истине. Свойство Cells объявлено у всех интерфейсов как

property Cells: Range read Get_Cells;

Отсюда видно, что это область (Range). И нет там никаких индексов, чтобы пробовать писать [x, y]. Один из корреспондентов мне написал, что он как-то обращался с этой проблемой к Наталье Елмановой, и она ему не помогла, написав, что «есть предположение, что кодогенератор Delphi их как-то не так "переваривает", генерируя XXX_TLB.pas, но полной уверенности нет». А дело в том, что «кодогенератор» правильно генерирует свойства _Defaul и Item (у многих интерфейсов в Excel Type Library есть такое свойство) у интерфейса Range. Вот только свойство _Default должно быть свойством по умолчанию. Поэтому стандартное объявление этих свойств

property _Default[RowIndex: OleVariant; ColumnIndex: OleVariant]: OleVariant dispid 0;
property Item[RowIndex: OleVariant; ColumnIndex: OleVariant]: OleVariant dispid 170;

можно исправить на такой вариант

property _Default[RowIndex: OleVariant; ColumnIndex: OleVariant]: OleVariant dispid 0; default;
property Item[RowIndex: OleVariant; ColumnIndex: OleVariant]: OleVariant dispid 170;

и смело писать Cells[x, y].Value.

Понятное дело, что это нехорошо - редактировать код, полученный автоматически из умного «кодогенератора» Delphi. Но «Там», ведь, тоже люди работают и ошибаются они не реже наших. Кстати, в импортированной Excel Type Library (независимо от версии Delphi - 4 или 5) некоторые свойства, имеющие dispid 0, почему-то все-таки объявлены как default. Почему?!

В приведенном выше примере кода я показал не только использование Cells. К ячейкам можно получить доступ и через свойство Range интерфейса Worksheet. Это свойство объявлено как

property Range[Cell1: OleVariant; Cell2: OleVariant]: Range read Get_Range;

В Cell1 / Cell2 можно передать ячейки (только в формате А1, RC вызовет исключение), описывающие границы области - левый верхний угол и правый нижний. Я же использовал только указание одной ячейки, мне необходимой. Где-то в Рунете я встретил предположение о том, что, если передать в оба параметра «A1», то в выбранный Range попадет вся колонка. Сначала я подумал, - «А почему не вся строка?!» Но, решил, все-таки проверить это предположение - в область попала одна ячейка.

В Excel можно присваивать имена любым ячейкам и даже наборам ячеек. Это можно сделать, либо используя «комбо-бокс», который находится левее строки формул, либо пункт меню «Вставка\Имя\Присвоить». Ячейке «А2» я присвоил имя «TestCell» и, используя все то же свойство Range листа, получил значение ячейки по этому имени.

И последний вариант, без которого я не смог бы обойтись при создании всех своих отчетов, это использование коллекции Names книги. Не смотря на некоторую неуклюжесть кода, этот способ я использую довольно часто. Почему? Потому, что очень часто использую именованные ячейки и области, разбросанные по разным листам и даже книгам. Но останавливаться на нем смысла не вижу, оставляя благодарному читателю возможность обратиться непосредственно к первоисточнику - справке по Excel VBA.

Чтение данных из нескольких ячеек

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

procedure TForm1.btnReadArrayClick(Sender: TObject);
var Values: OLEVariant;
    ISheet: Excel8TLB._Worksheet;
    IRange: Excel8TLB.Range;
    i, j: integer;
begin
  if Assigned(IWorkbook) then
    try
      ISheet := IWorkbook.Worksheets.Item['Лист1'] as Excel8TLB._Worksheet;
      try
        IRange := ISheet.Range['TestRange2', EmptyParam];
        Values := VarArrayCreate([1, IRange.Rows.Count, 1, IRange.Columns.Count], varVariant);
        for i := 1 to IRange.Rows.Count do
          for j := 1 to IRange.Columns.Count do begin
            Values[i, j] := IRange.Item[i, j];
            ShowMessage( Values[i, j]);
          end;
      finally
        IRange := nil;
        ISheet := nil;
      end;
    except
      raise Exception.Create('Не могу прочитать данные в массив!');
    end;
end;

Я создал на форме кнопку, по которой из заранее подготовленной области с именем "TestRange2" все значения ячеек будут получены в вариантный массив Values. Вызов ShowMessage добавлен сюда только для контроля над процессом. Как видно, получить значения ячеек области достаточно просто. Вы создаете вариантный массив с количеством строк и колонок, равными размерам области, а затем, проходя по очереди все ячейки области, запоминаете их значения в массиве. Но в этом коде есть одна проблема. Чтение из ячеек можно организовать еще проще. Вот так:

procedure TForm1.btnReadArrayClick(Sender: TObject);
var Values: OLEVariant;
    ISheet: Excel8TLB._Worksheet;
    IRange: Excel8TLB.Range;
begin
  if Assigned(IWorkbook) then
    try
      ISheet := IWorkbook.Worksheets.Item['Лист1'] as Excel8TLB._Worksheet;
      try
        IRange := ISheet.Range['TestRange2', EmptyParam];
        Values := IRange.Value;
        for i := 1 to IRange.Rows.Count do
          for j := 1 to IRange.Columns.Count do begin
            ShowMessage( Values[i, j]);
          end;
      finally
        IRange := nil;
        ISheet := nil;
      end;
    except
      raise Exception.Create('Не могу прочитать данные в массив!');
    end;
end;

Дело в том, что строки Values := IRange.Value вполне достаточно. Свойство Value интерфейса Range в состоянии вернуть вариантный массив. Этот код, по моему мнению, более прост и производителен, особенно на больших объемах данных. Уберите отсюда циклы с ShowMessage и убедитесь в этом.

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

var Values: OLEVariant;
    ISheet: Excel8TLB._Worksheet;
    IRange: Excel8TLB.Range;
    i, j: integer;
begin
  if Assigned(IWorkbook) then
    try
      ISheet := IWorkbook.Worksheets.Item['Лист1'] as Excel8TLB._Worksheet;
      try
        IRange := ISheet.UsedRange[0];
        Values := IRange.Value;
      finally
        IRange := nil;
        ISheet := nil;
      end;
    except
      raise Exception.Create('Не могу прочитать данные в массив!');
    end;
end;

Здесь я использую свойство UsedRange листа. Это прямоугольная область, заключенная между «левой верхней непустой» и «правой нижней непустой» ячейками. (Кто-нибудь понял? Впрочем, в два часа ночи разве напишешь понятней!). Конечно, если в этой прямоугольной области будет много пустых ячеек, то массив получится с избыточными данными. Что бы убедиться в этом, попробуйте создать циклы с ShowMessage из предыдущего примера.

В комментариях проекта-примера вы найдете еще несколько интересных конструкций, которые мне приходится использовать для получения массивов со значениями ячеек. В качестве параметра в UsedRange я передаю 0. Это lcid, описанный в предыдущей статье.

Кстати, об lcid. В прошлый раз меня подвела зрительная память. И в самом деле, «любимый классик» пишет, что туда можно смело передавать 0. Но другой, не менее любимый классик с этим не согласен и рекомендует передавать туда результат функции GetUserDefaultLCID. Думаю, последнее более правильно. А классики пусть сами разбираются (Канту и Калверт).

Есть еще несколько способов чтения данных из книги, которые, впрочем, я не в силах описать здесь. Один их таких способов, это использование DDE, самый быстрый и экономичный (по ресурсам) способ, который известен еще со времен Windows 3.1.

Поиск данных на листе

Нескольким моим корреспондентам я обещал в этой статье привести пример поиска данных в книге. Предлагаю поискать все ячейки, содержащие строку (или подстроку) "Text", и изменить цвет фона этих ячеек. Для этого я использовал методы Find и FindNext. На форму была добавлена кнопка, в обработчике которой появился следующий код:

procedure TForm1.btnFindClick(Sender: TObject);
var ISheet: Excel8TLB._Worksheet;
    IFirst, IRange: Excel8TLB.Range;
    FirstAddress, CurrentAddress: string;
    UsedRange: OLEVariant;
begin
  if Assigned(IWorkbook) then
    try
      ISheet := IWorkbook.Worksheets.Item['Лист1'] as Excel8TLB._Worksheet;
      try
        UsedRange := ISheet.UsedRange[0];
        IDispatch(IFirst) := UsedRange.Find(What:='Text', LookIn := xlValues,                                SearchDirection := xlNext);
        if Assigned(IFirst) then begin
          IRange := IFirst;
          FirstAddress := IFirst.Address[EmptyParam, EmptyParam, xlA1, EmptyParam, EmptyParam];
          repeat
            IRange.Interior.ColorIndex := 37;
            IDispatch(IRange) := UsedRange.FindNext(After := IRange);
            CurrentAddress := IRange.Address[EmptyParam, EmptyParam, xlA1,
                                             EmptyParam, EmptyParam];
          until FirstAddress = CurrentAddress;
        end;
      finally
        IRange := nil;
        IFirst := nil;
        ShowExcel;
      end;
    except
      raise Exception.Create('Не могу чего-то сделать!');
    end;
end;

Думаю, у каждого увидевшего этот код возникнет ощущение неудовлетворенности. Да, в выделенной красным строке никаким ранним связыванием и не пахнет. Более того, если вы попробуете вызвать метод Find с указанными параметрами, заменив остальные на EmptyParam, вы получите исключение. Есть места в Excel Type Library, работающие с ошибками. Я знаю достаточно этих мест. В таких случаях я использую приведенный здесь прием. Я проверяю работоспособность кода в редакторе VBA, а затем перехожу на позднее связывание. Так мне удалось обойти несколько серьезных, по моему мнению, ошибок в Excel TLB. Раннее связывание не должно быть догмой хотя бы из-за этого. Более того, перейдя полностью на ранее связывание, мы получим более компактный, а следовательно и читаемый код:

procedure TForm1.btnFindClick(Sender: TObject);
var ISheet: Excel8TLB._Worksheet;
    UsedRange, Range: OLEVariant;
    FirstAddress: string;
begin
  if Assigned(IWorkbook) then
    try
      ISheet := IWorkbook.Worksheets.Item['Лист1'] as Excel8TLB._Worksheet;
        UsedRange := ISheet.UsedRange[0];
        Range := UsedRange.Find(What:='Text', LookIn := xlValues, SearchDirection := xlNext);
        if not VarIsEmpty(Range) then begin
          FirstAddress := Range.Address;
          repeat
            Range.Interior.ColorIndex := 37;
            Range := UsedRange.FindNext(After := Range);
          until FirstAddress = Range.Address;
          ShowExcel;
        end;
    except
      raise Exception.Create('Не могу чего-то сделать!');
    end;
end;

 

Перемещение данных между листами

Несколько раз меня спросили о том, как перемещать данные между листами. Я бы сделал это вот так:

procedure TForm1.btnMoveDataClick(Sender: TObject);
var ISheetSrc, ISheetDst: Excel8TLB._Worksheet;
    IRangeSrc, IRangeDst: Excel8TLB.Range;
begin
  if Assigned(IWorkbook) then
    try
      ISheetSrc := IWorkbook.Worksheets.Item['Лист1'] as Excel8TLB._Worksheet;
      ISheetDst := IWorkbook.Worksheets.Add(EmptyParam, ISheetSrc, 1, EmptyParam, 0) as _Worksheet;
      IRangeSrc := ISheetSrc.Range['TestRange2', EmptyParam];
      IRangeDst := ISheetDst.Range['D4', EmptyParam];
      IRangeSrc.Copy(IRangeDst);
    finally
      IRangeDst := nil;
      IRangeSrc := nil;
      ISheetDst := nil;
      ISheetSrc := nil;
    end;
end;

Метод Copy интерфейса Range принимает в качестве параметра любой другой Range. Причем, совсем не важно, совпадают ли размеры источника и получателя, так как данные копируются начиная с левой верхней ячейки получателя в количестве, определенном размером источника. (О, завернул!) Для затравки хотелось бы показать код, который выполняет ту же задачу, но через буфер обмена (а вдруг в Word вставлять будем):

procedure TForm1.btnMoveDataClick(Sender: TObject);
var ISheetSrc, ISheetDst: Excel8TLB._Worksheet;
    IRangeSrc, IRangeDst: Excel8TLB.Range;
begin
  if Assigned(IWorkbook) then
    try
      ISheetSrc := IWorkbook.Worksheets.Item['Лист1'] as Excel8TLB._Worksheet;
      ISheetDst := IWorkbook.Worksheets.Add(EmptyParam, ISheetSrc, 1, EmptyParam, 0) as _Worksheet;
      IRangeSrc := ISheetSrc.Range['TestRange2', EmptyParam];
      IRangeDst := ISheetDst.Range[ 'D4', EmptyParam];
      IRangeSrc.Copy(EmptyParam); // так кладем в Clipboard
      ISheetDst.Paste(IRangeDst, EmptyParam, 0); // а вот так достаем оттуда
    finally
      IRangeDst := nil;
      IRangeSrc := nil;
      ISheetDst := nil;
      ISheetSrc := nil;
    end;
end;

 

Форматирование ячеек

Нет ничего проще в Excel, чем форматирование ячеек. Я специально не стану освещать эту тему. Вряд ли мне по силам составить конкуренцию лучшему помощнику в этом вопросе - пункту меню Excel "Сервис\Макрос\Начать запись…" А приведенный выше материал, надеюсь, создаст благодатную почву для изучения этого вопроса.

ИтогО

Во многих примерах я ссылался на объявление свойств и методов из Excel Type Library. Таким же образом я поступаю всегда, прежде чем использовать что-либо оттуда. Читаем же мы исходные тексты VCL! Понятно, что там, в Excel TLB, вы увидите только объявления этих самых свойств и методов, но этого часто вполне достаточно. К тому же я часто отправлял читателя к справке по VBA. Именно благодаря справке по VBA я создал многое из того, что работает сейчас у меня и моих заказчиков. К сожалению, книг, посвященных этой проблеме, не так много. Так что, читайте справку, «пользуйте» пишущий плейер и поглядывайте в импортированную библиотеку типов.

Нет, это еще не конец!

Писать подобные статьи достаточно сложно (я только сейчас это понял, столкнувшись с этим). Это не запись в блокноте и не записка на клавиатуре коллеги. Одно время я преподавал студентам. Это занятие, оказывается, намного проще. Я никогда не готовился к лекциям, никогда не имел даже малейшего намека на их план, никогда не имел конспекта. У меня было общее направление и память о прошлом занятии. Здесь есть и то, и другое. Но, честное слово, я снова буду тянуть из себя строка за строкой следующую статью о том, как передавать данные в Excel. Вы не заметили, что об этом здесь ни слова?..

Помню, как лет шесть назад я с раздражением ответил одному заказчику, что «Экселов» не пишу. Тогда они только подошли к идее построения большой информационной системы и работали в этом самом «Экселе», выписывая все документы и делая там же отчеты. Естественно, информация в их сети структурирована не была и была похожа на «свалку» из файлов. Сейчас я снова работаю на этого заказчика. Это уже большое предприятие и новая информационная система (за шесть лет многое изменилось). А насчет «Экселов» я не напоминаю. Все, что здесь печатается, все, с помощью чего анализируются какие-то данные, все, что можно представить диаграммой, живущей во времени, - все это в нем, родном. В Microsoft Excel…

Проект - SampleExcel.zip (133 K)


http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу


В избранное