Публикации Разное

Формирование отчета в MSWord с использованием внешней DLL

26.12.2011
Витомский В.В.

1. Введение

В процессе разработки программ часто возникает необходимость создания различных типов отчетов. В данной статье я рассмотрю пример использования для этой задачи всемирно известный и установленный практически на каждом компьютере, где используется M$Windows, пакет Micro$oft Office, а конкретно программное формирование отчета в M$Word из файла базы данных в формате DBF с кодировкой CP866.

Нам понадобится:

  • среда Lazarus со стандартными компонентами;
  • библиотека msofficedll (взять можно тут или по SVN - svn checkout http://msofficedll.googlecode.com/svn/trunk/ msofficedll-read-only)
  • готовый пример можно взять тут

2. Постановка задачи

Наша программа должна открыть файл DBF формата Приватбанка, сформировать ведомость на получение зарплаты и сохранить её в файл.

Дополнительно пользователь может задать № ведомости и выбрать кодировку DBF. Последний номер ведомости и выбор кодировки сохраняется в настройках программы.

3. Создаем приложение

Запускаем среду Lazarus, создаем новый проект (меню Проект>Создать проект), на форме с палитры компонентов "Standard" размещаем три кнопки TButton, одну метку TLabel, два поля ввода TEdit, один чекбок TCheckBox и панель TPanel, переходим на вкладку "Dialogs" и размещаем диалог открытия файла TOpenDialog, затем на вкладке "Data Access" выбираем компоненты TDataSource и TDbf, и напоследок из вкладки "Data Controls" размещаем на компоненте Panel1 компонент TDbGrid.

Устанавливаем следующие свойства для компонентов:


Компонент и свойство Значение
Button1 Caption Открыть DBF
Button2 Caption Распечатать
Button2 Enabled False
Button3 Caption Выход
CheckBox1 Caption Кодировка DOS 866
Label1 Caption № списка
Panel1 Align alBottom
DbGrid1 Align alClient
DataSource1 Dataset Dbf1
DbGrid1 DataSource DataSource1
OpenDialog1 Filter Файлы DBF|*.dbf|Все файлы|*.*

В результате у нас должно получиться похоже как на рис. 1

Рис. 1.

Дальше приступаем к кодингу.

Первое, что нам нужно сделать - загрузка настроек программы из файла. Для этого мы в раздел использованых модулей добавляем модуль INIFiles, а в разделе глобальных переменных дописываем INI : TINIFile;

Далее через Инспектор обьектов на вкладке События для нашей формы делаем двойной клик напротив поля OnCreate и получаем заготовку обработчика в редакторе кода. Дописываем наш код :

 //изменение разделителя групп разрядов и разделителя целой и дробной части
oldDecSep:=DecimalSeparator;
DecimalSeparator:=',';
oldThSep:=ThousandSeparator;
ThousandSeparator:='.';

//очистить поле, куда записывается имя открытого файла
Edit1.Clear;
 
 //Задать стартовый каталог для выбора файла - каталог программы
OpenDialog1.InitialDir:=ExtractFilePath(Application.ExeName);

//открыть файл настроек
INI:=TIniFile.Create(ChangeFileExt(Application.ExeName,'.ini'));
CheckBox1.Checked:=INI.ReadBool('SECTION1','DOS CODEPAGE',true);
Edit2.Text:=INI.ReadString('SECTION2','LISTNUM','1');    

Тут необходимо некоторое пояснение. Я столкнулся с проблемой форматирования чисел - разряды вместо пробела отделяются знаком вопроса. Скорее всего, это глюк LCL, который можно "вылечить" либо глобальной устновкой через панель управления разделитель разрядов "." (точка), либо на время работы программы средствами FPC. Я выбрал второй вариант, когда программа при запуске запоминает "старый" разделитель разрядов и меняет его на нужный нам, а при завершении работы возвращает на место. Для этого я в разделе описания переменных добавил ещё две переменные:

oldDecSep: Char;
oldThSep : Char;

Следующим шагом у нас будет обработчик события OnDestroy формы. Добавляем следующий код:

 Dbf1.Active:=false;
INI.WriteBool('SECTION1','DOS CODEPAGE',CheckBox1.Checked);
INI.WriteString('SECTION2','LISTNUM',Edit2.Text);
INI.Destroy;
DecimalSeparator := oldDecSep;
ThousandSeparator := oldThSep;

Тут мы закрываем файл БД, сохраняем настройки программы, возвращаем разделители на место и освобождаем созданный обьект INI.

Еще один момент, который может вызвать дополнительные вопросы - правильное отображение текстовых полей БД на экране. Lazarus использует кодировку UTF8, поэтому для правильного отображения текста нам необходимо его "на лету" конвертировать. Так как у нас используется для отображения данных компонент DbGrid1, пишем обработчик события OnDrawColumnCell:

var
F: TField;
S: ShortString;
begin
f := Column.Field;
s := F.DisplayText;
if (F.FieldName = 'FAM') or (f.FieldName = 'NAME') or (f.FieldName='OT') then
begin
DBGrid1.Canvas.Brush.Color := clWindow;
DBGrid1.Canvas.Font.Color := clBlack;
DBGrid1.Canvas.FillRect(Rect);
if CheckBox1.Checked then DBGrid1.Canvas.TextOut(Rect.Left, Rect.Top, ConsoleToUTF8(s))
else DBGrid1.Canvas.TextOut(Rect.Left, Rect.Top, AnsiToUtf8(s));
end else if (f.FieldName='RLSUM') then
 begin
DBGrid1.Canvas.Brush.Color := clGreen;
DBGrid1.Canvas.Font.Color := clYellow;
DBGrid1.Canvas.FillRect(Rect);
DBGrid1.Canvas.TextOut(Rect.Left, Rect.Top, FloatToStrF(F.AsCurrency,ffCurrency,14,2)+' грн.');
end;
end;

В данном случае у нас заранее известно структуру таблицы БД, поэтому можно использовать имена полей. В этом же обработчике мы определяем выбор пользователем кодировки БД. Однако, если в процессе работы пользователь решит изменить кодировку, результата мы не увидим, если не напишем обработчик события OnChange для CheckBox1. Он у нас будет состоять всего лишь из одной строчки:

DBGrid1.Refresh;

Дальше будем писать обработчик OnClick кнопки Button1:

  if OpenDialog1.Execute then begin
    //если файл выбран, внести его имя в поле на форму
   Edit1.Text:=ExtractFileName(OpenDialog1.FileName);
  end;

  //открыть DBF
  if OpenDialog1.FileName<>'' then begin
Dbf1.FilePathFull:=ExtractFilePath(OpenDialog1.FileName);
   Dbf1.TableName:=Edit1.Text;
   Dbf1.Active:=true;
   Button2.Enabled:=true;//если файл открыт успешно, можно разблокировать кнопку для печати
end;

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

Пробуем откомпилровать, тихо вспоминаем чью-то мать :), исправляем ошибки, снова компилируем и наслаждаемся результатом.

Если файл БД открывается и отображается нормально, можем приступать к самой главной процедуре, ради которой я писал эту статью - распечатка данных. Для работы с M$Word нам понадобится подключить модули Variants и uofficedll. Подробно все функции работы с M$Word описаны в документации к библиотеке, так что сразу приведу текст обработчика события OnClick для кнопки Button2:

var W : Variant;
   sum: Currency;
   i  : Integer;

begin
 //обнуляем сумму
 sum := 0;

 //Создаем новый документ с полями слева 2 см, с других сторон по 1 см
 NewDocument(w,false);//если вместо false указать true, мы будем визуально наблюдать формирование документа
 PageMargins(2,1,1,1,w);

 //выравнивание по центру жирным шрифтом
 ParagraphAlign(wdAlignParagraphCenter,w);
 FontBold(true,w);

 AddText(Utf8ToAnsi('СПИСОК № '+Edit2.Text+#13),w);
 AddText(Utf8ToAnsi('для зачисления на карточные счета сотрудников организации'+#13),w);
 AddText(Utf8ToAnsi('аванса за декабрь 2011 г.'+#13),w);
 FontBold(false,w);
 ParagraphAlign(wdAlignParagraphLeft,w);

 //создаем таблицу 6 колонок 2 строки (остальные строки добавлятся в процессе формирования автоматически)
 //заполняем шапку
 CreateTable(6,2,w);
 AddText(Utf8ToAnsi('№ п/п'),w);
 SetColWidth(20,w);
 GotoRight(1,w);

 AddText(Utf8ToAnsi('Таб. №'),w);
 SetColWidth(30,w);
 GotoRight(1,w);

 AddText(Utf8ToAnsi('№ счета'),w);
 GotoRight(1,w);

 AddText(Utf8ToAnsi('Ф.И.О.'),w);
 SetColWidth(180,w);
 GotoRight(1,w);

 AddText(Utf8ToAnsi('Ид. код'),w);
 SetColWidth(70,w);
 GotoRight(1,w);

 AddText(Utf8ToAnsi('Сумма'),w);
 SetColWidth(90,w);
 GotoRight(1,w);

 //устанавливаем выборку только тех людей, у кого есть начисления

 Dbf1.Filter:='RLSUM>0';
 Dbf1.Filtered:=true;

 //идем в начало таблицы
 Dbf1.First;

 //выводим в цикле выбранные записи в таблицу
 for i:= 1 to Dbf1.ExactRecordCount do begin
  AddText(IntToStr(i)+'.',w);
  GotoRight(1,w);

  AddText(Dbf1.FieldByName('LSTBL').AsString,w);
  GotoRight(1,w);

  AddText(Dbf1.FieldByName('CARD_NO').AsString,w);
  GotoRight(1,w);

  if CheckBox1.Checked then AddText(Utf8ToAnsi(ConsoleToUTF8(Dbf1.FieldByName('FAM').AsString+' '+
                                                   Dbf1.FieldByName('NAME').AsString+' '+
                                                   Dbf1.FieldByName('OT').AsString)),w)

                      else AddText(Dbf1.FieldByName('FAM').AsString+' '+
                                   Dbf1.FieldByName('NAME').AsString+' '+
                                   Dbf1.FieldByName('OT').AsString,w);

  GotoRight(1,w);
  AddText(Dbf1.FieldByName('INN').AsString,w);
  GotoRight(1,w);
AddText(Utf8ToAnsi(FloatToStrF(Dbf1.FieldByName('RLSUM').AsCurrency,ffNumber,5,2)+' грн.'),w);
  sum:=sum+Dbf1.FieldByName('RLSUM').AsCurrency; //накопляем общую сумму
  GotoRight(1,w);
  Dbf1.Next;//переходим на следующую запись таблицы
  Application.ProcessMessages;//это необходимо для того, чтобы в процессе
//формирования документа не складывалось впечатление,что программа "зависла"
 end;

 //выводим итоги
  MergeCellsR(5,w);
  AddText(Utf8ToAnsi('ИТОГО:'+FloatToStrF(sum,ffNumber,10,2)+' грн.',w);
  ExitTable(w);

  //подписываем ведомость
  AddText(#13#13,w);
  AddTabPosition(1,w);
  AddTabPosition(12,w);
AddText(Utf8ToAnsi(#9+'Руководитель'+#9+'______________'+#13#13),w);
  AddText(Utf8ToAnsi(#9+'Главный бухгалтер'+#9+'______________'),w);
  InsertFooter(Utf8ToAnsi('Демонстрация формирования документа MS Word программно на основе файла DBF'),w);

  //сохраняем документ
SaveDocAs(Utf8ToAnsi(ExtractFilePath(Application.ExeName)+'список'+Edit2.Text+'.doc'),w);

  //закрываем ворд
  CloseWord(w);
  w:=Unassigned;

  //снимаем фильтрацию таблицы
  Dbf1.Filtered:=false;

  ShowMessage('Список сформирован и сохранен в папку программы с именем "'+'список'+Edit2.Text+'.doc"');
end;

Если при сохранении указать только имя файла, то файл будет сохранен в папку "Мои документы".
Внимание: если файл с указанным именем уже существует, он будет автоматически перезаписан!

Для логического завершения программы нам осталось только описать обработчик события OnClick кнопки Button3. Он будет состоять всего лишь из одной строки: Application.Terminate;

Если всё сделано правильно, в результате выполнения в папке программы должен появиться файл с именем "список1.doc".

Окно рабочей программы показано на рис. 2.

Рис. 2.

Актуальные версии
FPC3.2.2release
Lazarus3.2release
MSE5.10.0release
fpGUI1.4.1release
links