Пятница, 17.05.2024
Королевство Delphi
Главное меню
Статьи
Наш опрос
Нравится раздел статьи?
Всего ответов: 68
Статистика
Онлайн всего: 1
Гостей: 1
Пользователей: 0
Форма входа
Главная » Статьи » Система » Текст

Автоматизация приложений MS® Office® для эффективного анализа результатов

Содержание:

  • Глава 1: Работа с MS Excel.
    • Часть 1: Создание, отображение и удаление экземпляра Excel.
    • Часть 2: Лучшее решение - шаблоны.
    • Часть 3: Создание или открытие книги.
    • Часть 4: Работа с листами и ячейками.
    • Часть 5: Передача данных разного типа.
    • Часть 6: Передача данных используя буфер обмена и DDE.
    • Часть 7: Пример обмена данными с Excel используя VCL и OLE.
  • Глава 2: Работа с MS Word.
    • Часть 1: Управление Word-ом через OLE.
    • Часть 2: Подсчет статистики обычного текста, сносок и колонтитулов в документах.
    • Часть 3: Открытие документа используя VCL.
    • Часть 4: Работа с таблицами.
    • Часть 5: Работа с текстом, рисунками и списками.

Автор статей: Евгений Старостин.


Глава 1. Работа с MS Excel.

Часть 1. Создание, отображение и удаление экземпляра Excel.

Собственно, цель этой статьи мне понятна - поделиться своим опытом с народом. Делюсь...

Итак, зачем нам, лучшим в мире программистам, нужен Excel, порождение "злого" гения Microsoft? Конечно, часто это лишнее - "юзать" Excel для отчетов. Напечатать "платежку" можно и в QReport-е. Но...

Есть заказчики, готовые отдать "кучищи" денег за то, что они будут знать все и всегда о своем предприятии. Да еще, чтоб это было красиво и со вкусом.

Приезжает один из моих заказчиков (немец - они повсюду! курорты Испании просто куплены ими - это знаю наверняка) на свое местное предприятие и начинает задавать интересные вопросы. Как трудились за время его отсутствия, сколько продукции выпустили, кому сколько отгрузили, в разных валютах, итого в USD и пр.? А я ему в ответ открываю отчет, неслабый такой, - сводная таблица по движению готовой продукции (посвященные знают, что это 40-ой счет в бухгалтерии). А в ней одних PageField-ов десяток. И на каждый его вопрос я начинаю отвечать не напрягаясь, потихоньку перетаскивая поля таблицы туда-сюда, фильтрую кое-что, строю диаграммы. Что, вы думаете, было потом? Он, как маленький ребенок, сидел за этой сводной таблицей несколько часов, все восхищался. И правильно, наши программисты круче ихних! Заодно и мы спокойно поработали (ему занятие нашлось). О деньгах тут вообще не говорим.

Потом я ему показал, как эту самую сводную таблицу в Сеть можно опубликовать. Сейчас просит, чтоб ему доступ из Германии сделали к этой табличке. Мы, конечно, рады стараться.

Я бы привел еще несколько примеров, но, думаю, читатели уже поняли меня. Excel - вещь практически незаменимая во всяческих анализах (не путать с поликлиникой). А для тех, кто не понял, я еще напишу. Отдельно.


Так как же с ним работать ?

А просто. Создал "Excel.Application", использовал его по назначению, "убил" и готово. Вот именно об этом я и попытаюсь написать здесь.

Важно! Параллельно с написанием статьи создавался демо-проект (точнее два - для Delphi 4 и 5), где вы сможете найти весь код примеров статьи. Проект для Delphi 4.0 использует импортированную Type Library из Excel 97. Здесь я использую ранее связывание, ибо CreateOLEObject отлично описал мой любимый классик в "Delphi 4 Unleashed" (мне ли с ним тягаться?). Кроме того, обращайтесь к комментариям в исходных текстах этого проекта. Местами там написано намного понятней, нежели здесь. Delphi 5 содержит более удобный механизм импорта библиотек типов с поддержкой событий и прекрасной генерацией ко-классов. Специально для счастливых обладателей Delphi 5 (я тоже им являюсь) я создал проект, но уже применительно к TexcelApplication (правда ли, что импортированный MS Office есть только в версии Enterprise?). Примеры кода я буду приводить сначала для Delphi 4, потом для Delphi 5. Заранее приношу прощения за дублирование информации в комментариях и в статье - писал сразу везде.

И еще. Эффективная работа с Excel-ом из Delphi-приложений немыслима без знания одной важной вещи. И имя ей - интерфейс. Мне, конечно, хотелось бы написать о принципах работы с интерфейсами здесь, в этой статье. Более того, я обещал сделать это самой Королеве. Но...

Мне ли (совсем еще не профессионалу - и это так!) пытаться сделать это лучше, чем классики этой области. Я честно признаюсь, что не смогу этого сделать быстро (в небольшом объеме) и качественно. Поэтому всякого, не знакомого еще с этой областью программирования, я с глубочайшими извинениями отсылаю к книге Чеппела "OLE Inside".

Достойную помощь (уже применительно к Delphi) может вам оказать "Delphi 4 Unleashed" Чарльза Калверта.


Создание экземпляра Excel.Application.

Модуль импортированной Excel TLB (неважно, для D4 или D5) содержит описания всех интерфейсов, которые правильные программисты из Microsoft решили выставить наружу. Там есть все необходимое: типы, константы и интерфейсы. Этого вполне достаточно для работы с Excel-ом из Delphi-приложения (во написал! а что еще нужно-то?). Я создаю Excel для последующего его использования с помощью такого кода:

Delphi 4.0

procedure TForm1.CreateExcel(NewInstance: boolean);
var IU: IUnknown;
isCreate: boolean;
begin
// FIXLSApp - private-поле у формы
// у меня в привычке добавлять букву I для всех интерфейсов
// понятно почему FI... ?
if not Assigned(FIXLSApp) then begin // а зачем создавать, если уже есть?
isCreate := NewInstance or
(not SUCCEEDED( GetActiveObject(CLASS_Application_, nil, IU) ) );
if isCreate then
FIXLSApp := CreateComObject(CLASS_Application_) as _Application
else
FIXLSApp := IU as _Application;
end;
end;

Этот достаточно простой код вы найдете практически во всех книгах, посвященных работе с интерфейсами. Как и везде, я напишу, что в результате выполнения этого кода создастся объект COM с CLSID-ом "{00024500-0000-0000-C000-000000000046}" (читайте и перечитывайте Калверта, это не только укрепляет сон!).

Delphi 5.0

procedure TForm1.CreateExcel(NewInstance: boolean);
begin
if not Assigned(IXLSApp) then begin
FIXLSApp := TExcelApplication.Create(Self);
if NewInstance then FIXLSApp.ConnectKind := ckNewInstance;
FIXLSApp.Connect;
end;
end;

В отличие от предыдущих версий, Delphi 5.0 предоставляет более удобный сервис при импорте библиотек типов. Большой шаг вперед - появление класса ToleServer с поддержкой событий. Теперь работа с существующими и создание новых OLE-серверов стала намного удобней. Как видите, не приходится обращаться к низкоуровневым функциям. Впрочем, в Delphi 4.0 тоже существовал этот класс, только не от Borland. Отличная библиотека была создана Бином Ли (Binh Ly) в COM Nodes - это Threading COM Library. С легкой руки Алексея Вуколова (специальное спасибо!) я использовал ее для построения масштабируемых COM-серверов в сервисах WinNT.

Обращу ваше внимание только на параметр NewInstance. Он позволяет создать новый процесс. Я часто задаю себе вопрос - "А нужен ли NewInstance?". Одна копия процесса, все ж, требует меньше памяти. Но еще чаще я думаю - "Боже, как хорошо я сделал, когда создал новый процесс!". Почему? Если вы не хотите потерять уже открытые, но еще не сохраненные книги, экспериментируя даже с моими примерами, создавайте новый процесс. Печальный опыт научил меня использовать GetActiveObject только в случае полной уверенности в коде, который будет выполняться после. Поэтому, мой вам совет, тестируйте свои приложения только с NewInstance. Или закрывайте важные книги пред этим. Excel - хитрая программа, бывает, улетает в неизвестность, ни слова не сказав. Это не вина Microsoft. Это неудачное расположение звезд.


Как показать Excel, если он, разумеется, создан ?

Вот здесь начинаются хитрости. Любой, читавший помощь по Excel VBA, скажет, что достаточно написать FIXLSApp.Visible := true. Не тут-то было. Я делаю так:

Delphi 4.0 / 5.0

procedure TForm1.ShowExcel;
begin
if Assigned(FIXLSApp) then begin // а если он не создан?
FIXLSApp.Visible[0] := true;
if FIXLSApp.WindowState[0] = TOLEEnum(xlMinimized) then
FIXLSApp.WindowState[0] := TOLEEnum(xlNormal);
FIXLSApp.ScreenUpdating[0] := true;
end;
end;

Зачем здесь условие на минимайз и какой-то ScreenUpdating? Давайте попробуем закомментировать эти строки, остаиви только Visible, запустить проект, создать Excel (кнопка CreateExcel), показать его (кнопка ShowExcel), минимизировать, вернуться в приложение и сделать снова ShowExcel. Да-да, Visible = true переводит фокус в минимизированный Excel, не восстанавливая размеры окна. Это ситуация, с которой я борюсь условием на xlMinimized. Но ScreenUpdating зачем?

Знающие люди говорят, что это свойство отвечает за перерисовку окон Excel. Это все равно, что DisableControls у TDataSet. Добавляет скорости, если в нем false. И это правда что, если выключить его во время длительных пересчетов, то быстрее пересчитается. Но мы, ведь, не выключали его. Зачем тогда эта строка?

Делаем так: комметируем эту строку, запускаем демо, CreateExcel, ShowExcel, закрываем его (можно кнопкой с крестиком в правом верхнем углу окна, кому нравится - через меню "Файл/Выход"). Знающие люди скажут, что Excel на самом деле не закрыт. Интерфейс мы не освободили, поэтому в TaskManager мы его и увидим. Итак, Excel по-прежнему у нас в руках. Мы имеем право сделать ему снова Show.

После такого действия у меня возникает ощущение, что я переплатил за свою видеокарту. Фокус в Excel-е, но я по-прежнему наблюдаю форму демо-проекта. Видимо, программисты из MS не рассчитывали на то, что кто-то закроет Excel, вызванный через создание Excel.Application, а потом захочет увидеть его снова. Но я-то захотел?!

Свойства Visible, WindowState и ScreenUpdating вызываются с каким-то непонятным индексом массива - 0. В модуле Excel TLB во многих свойствах и методах вы можете встретить параметр или индекс lcid. Не помню, у кого я это прочитал (Калверт или Канту), но с тех пор я туда передаю всегда 0. И все работает. LCID - это что-то насчет локализации. В MSDN написано "Indicates that the parameter is a locale ID (LCID)".


Спрячем Excel от посторонних глаз!

На свой процесс я всегда создаю один экземпляр Excel.Application. Уже пару лет все отчеты у меня - это отчеты Excel. Я написал несколько классов, которые мне очень помогают в этом. Сегодня у меня целая "отчетная" подсистема, зашитая в класс и обслуживающая непомерно большие запросы моих пользователей. В промежутках между работой с отчетами нет необходимости "мозолить глаза" лишним окном в TaskBar-е. Вот и прячу я этот Excel. Это очень просто и комментариев, думаю, не требует:

Delphi 4.0 / 5.0

procedure TForm1.HideExcel;
begin
if Assigned(FIXLSApp) then begin
FIXLSApp.Visible[0] := false;
end;
end;

Закроем Excel корректно!

Собственно говоря, при закрытии приложения Excel сам будет закрыт, если вы там не устели чего-нибудь отредактировать. И это правильно. Программисты Borland (Inprise до сих пор мне режет слух, да и некоторым в Inprise, судя по всему, тоже) позаботились об этом. Но я еще с Delphi 3 заимел дурную привычку освобождать все самостоятельно. Освобождать обычным присваиванием в nil (это касается проекта для D4). Труда это не составляет, да и проверка на Assigned удобна. Поэтому, и еще из кое-каких соображений, я делаю так:

Delphi 4.0

procedure TForm1.ReleaseExcel;
begin
if Assigned(FIXLSApp) then begin
if (FIXLSApp.Workbooks.Count > 0) and (not FIXLSApp.Visible[0]) then begin
FIXLSApp.WindowState[0] := TOLEEnum(xlMinimized);
FIXLSApp.Visible[0] := true;
Application.BringToFront;
end;
end;
FIXLSApp := nil;
end;

Ну вот, написал только про nil, а кода - на полстраницы. Опишу ситуацию.

Вы не запускали новый процесс, вы "законнектились" к уже существовавшему. В нем была открыта книга. Попробуйте: CreateExcel, ShowExcel, HideExcel (имеем право), ReleaseExcel. Если оставить только присваивание в nil, то существовавший процесс не будет выгружен (он же существовал до запуска нашего демо), но будет спрятан от пользователя с его открытой книгой.

Delphi 5.0

procedure TForm1.ReleaseExcel;
begin
if Assigned(IXLSApp) then begin
if (IXLSApp.Workbooks.Count > 0) and (not IXLSApp.Visible[0]) then begin
IXLSApp.WindowState[0] := TOLEEnum(xlMinimized);
IXLSApp.Visible[0] := true;
if not(csDestroying in ComponentState) then Self.SetFocus;
Application.BringToFront;
end;
end;
FreeAndNil(FIXLSApp);

Практически тот же код. Только в D5 вы работаете уже не с интерфейсом напрямую, а с экземпляром класса TexcelApplcation. Если посмотреть его предков, то можно увидеть, что это настоящий класс, освободить который просто необходимо. Поэтому вместо присваивания в nil там написано FreeAndNil (помните такую процедуру?).

Часть 2: Лучшее решение - шаблоны.

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

Delphi 4.0 / 5.0

function TForm1.AddWorkbook(const WorkbookName: string): 
Excel8TLB._Workbook;
begin
Result := nil;
if Assigned(FIXLSApp) and (trim(WorkbookName) <> '') then begin
Result := FIXLSApp.Workbooks.Add(WorkbookName, 0);
end;
end;

В этом коде нет ничего сложного. В принципе при работе с Excel я мало находил мест, где что-либо сделать было бы сложно. Чаще достаточно прочитать справку по VBA или записать макрос (благо, Microsoft встроила в Excel хороший пишущий player). После выполнения этого метода будет добавлена книга, близнец шаблона, с именем шаблона и порядковым номером (как "Книга1.xls" или "Книга228.xls"). Правда здесь есть одна тонкость. Эти "циферки" в имя книги Excel добавляет после поиска книг с таким же названием в каталоге по умолчанию. Я несколько раз наступал на грабли (больно!), когда пытался сохранять книги в другом каталоге и создавать новую - по этому же шаблону. К сожалению, не может эта "злобная" программа держать открытыми несколько книг с одинаковыми названиями, несмотря на то, что они лежат в разных каталогах.

Как я помечаю области, в которые необходимо разместить данные? В Excel существует возможность объединить ячейки в группу и поименовать эту группу. В терминах Microsoft это объект Range (область). Для своего проекта я создал тестовую книгу "Test.xls", в которой на листе "Лист1" разместил область "TestRange" (см. рисунок). Более того, для ячеек этой области я указал форматы вывода (Field4 - дата, Field3 - красный цвет шрифта). Я надеюсь, что после переноса тестовых данных форматы сохранятся.


Что есть шаблон без данных в нем?

Существует масса способов передать данные в Excel, начиная с DDE и заканчивая обычным присваиванием (типа Cell.Value := NewValue ). Конечно, максимальную скорость передачи данных можно получить, только используя DDE. Но я отказался от этого пути из-за некоторых ограничений и давно смущающего меня флажка в настройках Excel ("Игнорировать DDE-запросы"). Поэтому здесь я опишу менее эффективный, но работоспособный, путь решения этой проблемы. Итак, после нажатия кнопки CreateExcel имеем открытый шаблон с листом "Лист1" и областью с именем "TestRange". Для чистоты эксперимента (скорей из лени, великая вещь - собственная лень) я описал константный массив с тестовыми данными - TestDataArray. Именно эти данные я и передаю в ячейки области:

Delphi 4.0

procedure TForm1.btnDataToBookClick(Sender: TObject);
var LaunchDir: string;
IWorkbook: Excel8TLB._Workbook;
ISheet: Excel8TLB._Worksheet;
IRange: Excel8TLB.Range;
NewValueArray, V: OLEVariant;
i: integer;
begin
if Assigned(IXLSApp) then begin
LaunchDir := ExtractFilePath( ParamStr(0) );
IWorkbook := AddWorkbook(LaunchDir + 'Test.xls');
try
ISheet := IWorkbook.Worksheets.Item['Лист1'] as Excel8TLB._Worksheet;
IRange := ISheet.Range['TestRange', EmptyParam];
NewValueArray := VarArrayCreate([0, 20, 1, 4], varVariant);
for i := 0 to 20 do begin
NewValueArray[i, 1] := TestDataArray[i].V1;
NewValueArray[i, 2] := TestDataArray[i].V2;
NewValueArray[i, 3] := TestDataArray[i].V3;
NewValueArray[i, 4] := date + i;
end;
IRange.Value := NewValueArray;
finally
IRange := nil;
ISheet := nil;
IWorkbook := nil;
end;
end;
end;

Delphi 5.0

procedure TForm1.btnDataClick(Sender: TObject);
type
var LaunchDir: string;
IWorkbook: Excel97.ExcelWorkbook;
ISheet: Excel97.ExcelWorksheet;
IRange: Excel97.Range;
NewValueArray, V: OLEVariant;
i: integer;
begin
if Assigned(IXLSApp) then begin
LaunchDir := ExtractFilePath( ParamStr(0) );
IWorkbook := AddWorkbook(LaunchDir + 'Test.xls');
try
ISheet := IWorkbook.Worksheets.Item['Лист1'] as Excel97.ExcelWorksheet;
IRange := ISheet.Range['TestRange', EmptyParam];
NewValueArray := VarArrayCreate([0, 20, 1, 4], varVariant);
for i := 0 to 20 do begin
NewValueArray[i, 1] := TestDataArray[i].V1;
NewValueArray[i, 2] := TestDataArray[i].V2;
NewValueArray[i, 3] := TestDataArray[i].V3;
NewValueArray[i, 4] := date + i;
end;
IRange.Value := NewValueArray;
finally
IRange := nil;
ISheet := nil;
IWorkbook := nil;
end;
end;
end;

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

Часть 3. Создание или открытие книги.

В качестве примера я беру проект из предыдущей моей статьи и стану его понемногу расширять, отвечая на вопросы, появившиеся у специалистов разного профиля и кругозора. Эти вопросы получены мною из двух "источников": как реакция на мою статью и, извините, из переписки по 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 к ней с возможностью выбора варианта работы. Не судите меня строго за то, что главная и единственная форма проекта-примера получится громоздкой и некрасивой. Не это здесь главное. Итак, всегда создавайте по кнопке книгу. Далее нажимайте кнопку, на которую указывает конкретный пример кода, и наблюдайте. Буду рад, если кто-то из читателей создаст более приемлемый демонстрационный проект для этой статьи.

Часть 4. Работа с листами и ячейками.

Есть в 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. Думаю, последнее более правильно. Однако В некоторых случаях, чаще в гремучей смеси Windows 2000 и Excel 2000, оба решения не проходили. Причем, выдавалось сообщение о попытке "использовать библиотеку старого формата:" и что-то еще. Так вот, вместо GetUserDefaultLCID я применяю теперь константу LOCALE_USER_DEFAULT. Более ничего объяснить не могу, так как до сих пор, проштудировав основательно MSDN, не разобрался, что же в таком случае хочет получить Microsoft в методы и свойства интерфейсов Excel, где одним из параметров требует lcid. Кто бы объяснил?..

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


Продолжение...

Получить ссылку на материал

Категория: Текст | Добавил: Барон (14.12.2011)
Просмотров: 1364 | Теги: Office | Рейтинг: 0.0/0
[ Пожертвования для сайта ] [ Пожаловаться на материал ]

Если вам помог материал сайта кликните по оплаченной рекламе размещенной в центре

Поиск
Категории раздела
ActiveX [10]
CORBA и COM [16]
Kol и MCK [23]
WinAPI [28]
Компоненты [27]
Работа с Bluetooth [4]
Железо [8]
Текст [18]
Разное [98]
Королевство Delphi © 2010-2024
Яндекс цитирования