24 марта 2010

Delphi 2010: TDataSet.FreeBookmark – рудимент

   Часто бывает полезно отметить текущее положение курсора в DataSet'е так, чтобы позже можно было быстро возвратиться к этому месту. Delphi обеспечивает эту функциональную возможность с помощью закладок (Bookmark), для работы с которыми используются процедуры:
  • GetBookmark – устанавливает закладку на текущую запись;
  • BookmarkValid – проверяет, существует ли запись, на которую ссылается закладка;
  • GotoBookmark – позиционирует курсор на запись, на которую ссылается закладка;
  • FreeBookmark – освобождает системные ресурсы, используемые методом GetBookmark.
Думаю это знакомая многим конструкция:

Var
  q: TSDQuery;
  bm: TBookmark;
begin
  Try
    bm := q.GetBookmark; // делаем закладку
    // обрабатываем ds
  Finally
    If bm <> nil then
      begin
        If q.BookmarkValid(bm) then
          q.GotoBookmark(bm);
        q.FreeBookmark(bm);
      end;
  End;
end;

В Delphi 7 код FreeBookmark выглядит так:

procedure TDataSet.FreeBookmark(Bookmark: TBookmark);
begin
  FreeMem(Bookmark);
end;

В Delphi 2010 он – просто заглушка:

procedure TDataSet.FreeBookmark(Bookmark: TBookmark);
begin
  // No longer need to free bookmark since it's a TBytes now.
end;

   Т.е. FreeBookmark в Delphi 2010 – это уже пережиток прошлого и вызывать его больше не нужно. Таким образом, исходный код становится проще:

Var
  ds: TSDQuery;
  bm: TBookmark;
begin
  Try
    bm := q.GetBookmark; // делаем закладку
    // обрабатываем ds
  Finally
    If (bm <> nil) and q.BookmarkValid(bm) then
      q.GotoBookmark(bm);
  End;
end;

   P.S. А вот справочную систему Delphi 2010 подправить, как всегда, забыли. В ней есть раздел "DB.TDataSet.FreeBookmark" с описанием процедуры и рекомендацией ее использования. А раздел "Marking and Returning to Records" содержит строку "FreeBookmark frees the memory allocated for a specified bookmark when you no longer need it. You should also call DB.TDataSet.FreeBookmark before reusing an existing bookmark." и пример, где используется FreeBookmark :)

21 марта 2010

Разработчики Delphi собрали 26 000$ для помощи Гаити

   Anders Ohlsson, сотрудник Embarcadero Technologies (бывший сотрудник компании Borland), организовал на eBay благотворительный аукцион для помощи населению пострадавшего от землетрясения Гаити.
   На аукцион были выставлены старые вещи, связанные с компанией Borland. За 2 025$ был продан Compaq II – компьютер, на котором Anders Hejlsberg разрабатывал Turbo Pascal. За 99$ продали наклейку на бампер "Delphi Developers Do It Faster". С молотка ушли подписанные авторами книги "Delphi 2 Developer's Guide" (Steve Teixeira) и "Delphi Component Design" (Danny Thorpe), старые релизы продуктов в оригинальных упаковках, фирменные футболки компании, эксклюзивная спортивная куртка с большой буквой "B" и прочее. Обед с David Intersimone ("David I", вице-президент по связям с разработчиками и главный евангелист Embarcadero Technologies) оценили в 480$.
   Всего поклонниками компании Borland со всех континентов было куплено 177 предметов общей стоимостью 26 000$. Некто то, из Австрии, потратил на этом аукционе почти 4 200$!
   Собранные деньги будут переданы фонду "Clinton Bush Haiti Fund".
По информации Santa Cruz Sentinel

16 марта 2010

Delphi 2010: TValue - "тормоз"!

Перевод. Оригинал "TValue is very slow" (© TURBU Tech) дополнен моими тестами и комментариями.

   Справочная система Delphi 2010 описывает тип TValue, используемый модулем RTTI для хранения значений произвольных типов, как "облегченная версия типа Variant". Увидев это, я задался вопросом, насколько он легковеснее? Как быстро работает TValue?
   К счастью, среди известных мне новых возможностей Delphi 2010 – модуль диагностики (Diagnostics), который предоставляет нам объект TStopwatch – простой таймер позволяющий засечь время выполнения операций и тем самым облегчить написание простого теста скорости.
   Я ожидал, что скорость работы TValue будет сравнима со скоростью Variant, или возможно немного больше. Для проверки, я написал следующую программку:

program Project1;

{$APPTYPE CONSOLE}

uses
  SysUtils, rtti, diagnostics;

const
  HUNDRED_MILLION = 100000000;

procedure tryTValue;
var
  i: integer;
  j: TValue;
  value: integer;
begin
  for I := 1 to HUNDRED_MILLION do
    begin
      j := i;
      value := j.AsInteger;
    end;
end;

procedure tryVariants;
var
  i: integer;
  j: variant;
  value: integer;
begin
  for I := 1 to HUNDRED_MILLION do
    begin
      j := i;
      value := j;
    end;
end;

var
  stopwatch: TStopWatch;
begin
  try
    stopwatch := TStopWatch.StartNew;
    tryVariants;
    stopwatch.Stop;
    writeln('Variants: ', stopwatch.ElapsedMilliseconds);

    stopwatch := TStopWatch.StartNew;
    tryTValue;
    stopwatch.Stop;
    writeln('TValues: ', stopwatch.ElapsedMilliseconds);
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  readln;
end.

   Конечно, этот тест - не исчерпывающая проверка возможностей TValue, но результаты поучительны. Когда я запустил его на своем рабочем компьютере (высокопроизводительный ноутбук Alienware), тест Variant выполнился почти мгновенно, а тест TValue выполнялся так долго, что я решил, что он завис и остановил его.
   Затем я запустил тест снова и получил следующие результаты (в миллисекундах):
Variants: 717
TValues: 31131
   По крайней мере, для этой конкретной операции, TValue в 43.52167832167832 раза медленнее, чем Variant!

От меня.

   Вообще то 31131 поделить на 717 будет равно 43.41841004 ;)
   Что бы проверить результат, я запустил несколько тестов на своей домашней "dev machine".

Тест #1. Запустил исходный тест и получил такую же разницу скорости - в 43.17657992 раза:
Variants: 538
TValues: 23229.
Тест #2. Переставил местами вызов tryTValue и tryVariants и снова получил 43.376404494:
TValues: 23163
Variants: 534.
Тест #3. Закомментировав обратное присвоение TValue и Variant целой переменной ("value := j.AsInteger" и "value := j") я получил для TValue более "веселый" результат:
Variants: 535
TValues: 5168
Присвоение целого значения TValue медленнее присвоения целого значения Variant всего в 9.65981308 раза. А значит основное падение скорости вызвано AsInteger.

Тест #4. В процедуре tryTValue я заменил "value := j.AsInteger" на "value := j.AsOrdinal"
Variants: 536
TValues: 5862
В результате общее падение скорости всего в 10.93656716 раза!

Тест #5. AsOrdinal возвращает значение типа Int64, поэтому в процедуре tryTValue я заменил "value := j.AsInteger" на "value := j.AsInt64" и получил падение скорости в 54,25981308 раза!!!
Variants: 535
TValues: 29029
Вывод: "value := j.AsOrdinal" у TValue работает почти так же быстро, как и "value := j" для Variant. А методы AsInteger и AsInt64 – лучше не использовать. Но все равно, главный вывод: TValue – "тормоз"!

   Напоследок, я проверил с помощью функции SizeOf число байт, которые занимали переменные: переменная типа Variant занимала – 24 байта, а TValue – всего 16. Может в этом проявляется "облегченность" типа TValue? Тогда, храните числа в integer – они будут занимать 4 байта ;)

15 марта 2010

Использование полей и закладок при работе с MS Word из Delphi

   В предыдущей заметке "Поиск и замена текста в документе MS Word из Delphi" я рассказывал, как дорабатывал старый модуль, который генерирует клиентам компании письма в формате MS Word с помощью поиска и замены текста. Сдав модуль заказчикам, я в свободное от работы время, переделал его. Вместо поиска и замены использовал поля с переменными (DocVariable).
   В шаблон письма с помощью макроса добавил переменные

Sub AddFields()
  ThisDocument.Variables.Add "FIO", "FIO"
  ThisDocument.Variables.Add "ADDRESS", "ADDRESS"
  ...
End Sub

и расставил поля по тексту шаблона. В макросе у метода Add первый параметр – название переменной, а второй – ее значение. Я специально сделал их одинаковыми, чтобы пользователям было проще и нагляднее редактировать шаблоны.
   Затем внес изменения в методы модуля работающие с MS Word. Если опустить все детали и различную логику, то упрощенно работа с MS Word выглядит так:

Var
  wa: WordApplication;
  ovDotName, ovFileName: OleVariant;
  i: Integer;
  q: TSDQuery;
  ...
begin
  ...
  wa := CreateComObject(CLASS_WordApplication) as _Application;
  ovDotName := 'какой то шаблон.dot';
  wa.Documents.Add(ovDotName, EmptyParam, EmptyParam, EmptyParam);

  For i := 0 to q.FieldCount-1 do
    MSWordSetVariable(q.Fields[i].FieldName, q.Fields[i].AsString);
  ...
  ovFileName := 'письмо любимому клиенту.doc';
  wa.ActiveDocument.SaveAs(ovFileName, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam);
  ...

Где MSWordSetVariable – процедура, которая присваивает значение переменной в документе MS Word.

procedure MSWordSetVariable(ovVariable: OleVariant; const sValue: String);
begin
  If sValue = ''
    then wa.ActiveDocument.Variables.Item(ovVariable).Value := ' '
    else wa.ActiveDocument.Variables.Item(ovVariable).Value := sValue
end;

   Если переменной MS Word присвоить пустую строчку, то в поле выводится текст 'Ошибка! Переменная документа не указана.', поэтому вместо пустого значения я присваиваю ей пробел (так же делает и сам MS Word). Это связано со странной работой MS Word с коллекцией переменных. Если переменной присвоить пустую строку, то количество переменных в коллекции (Variables.Count) уменьшается на единицу, а попытка удалить переменную (Variables.Item(ovVariable).Delete) дает ошибку 'Объект был удален', т.е. присвоение пустой строки равносильно удалению. При этом после удаления переменной из коллекции переменных, присвоение ей непустого значения выполняется успешно и количество переменных в коллекции увеличивается на единицу, т.е. присвоение непустого значения равносильно вызову метода добавления переменной в коллекцию переменных.
   Продемонстрирую вышесказанное примером кода, по которому видно как изменяется количество переменных (iCount):

iCount := wa.ActiveDocument.Variables.Count; // iCount = 3
wa.ActiveDocument.Variables.Item(ovVariable).Value := '';
iCount := wa.ActiveDocument.Variables.Count; // iCount = 2
wa.ActiveDocument.Variables.Item(ovVariable).Value := 'не пусто';
iCount := wa.ActiveDocument.Variables.Count; // iCount = 3
wa.ActiveDocument.Variables.Item(ovVariable).Delete;
iCount := wa.ActiveDocument.Variables.Count; // iCount = 2
wa.ActiveDocument.Variables.Item(ovVariable).Value := 'не пусто';
iCount := wa.ActiveDocument.Variables.Count; // iCount = 3
wa.ActiveDocument.Variables.Item(ovVariable).Value := '';
iCount := wa.ActiveDocument.Variables.Count; // iCount = 2
wa.ActiveDocument.Variables.Item(ovVariable).Delete; // ошибка 'Объект был удален'

   Вот такая логика у индусов, писавших этот кусок MS Word.

   После присвоения значений всем переменным, осталось только дать команду полям обновиться новыми значениями переменных. Для этого в VBA у коллекции объектов полей (Fields) есть метод Update:

wa.ActiveDocument.Fields.Update

А для того, чтобы избежать дальнейшего обновления полей, избавимся от их связи с переменными:

wa.ActiveDocument.Fields.Unlink

   У каждого структурного элемента документа (заметки, колонтитула, сноски и т.д.) своя коллекция объектов полей, поэтому, если документ, как у меня, состоит из разных структурных элементов, то методы Update и Unlink необходимо вызвать для каждого из этих элементов. Для этого перебираем все элементы коллекции StoryRanges.

procedure MSWordUpdateStoryRanges;
  Var
    StoryRanges: Word2000.StoryRanges;
    StoryRange: Word2000.Range;
    iStoryIndex: integer;
Begin
  StoryRanges := wa.ActiveDocument.StoryRanges;
  For iStoryIndex := wdMainTextStory to wdFirstPageFooterStory do
    Try
      StoryRange := StoryRanges.Item(iStoryIndex);
      If StoryRange <> nil then
        begin
          StoryRange.Fields.Update;
          StoryRange.Fields.Unlink;

          While (StoryRange.NextStoryRange <> nil) do
            begin
              StoryRange := StoryRange.NextStoryRange;
              StoryRange.Fields.Update;
              StoryRange.Fields.Unlink;
            end;
          end;
    Except
      StoryRange := nil;
    End;
End;

   Т.к. в моем шаблоне есть только основной текст и одна заметка, то вместо процедуры, которая перебирает все элементы коллекции StoryRanges, я сделал процедуру, которая работает только с одним ее элементом:

procedure MSWordUpdateStoryRange(const iStoryIndex: integer);
  Var
    StoryRange: Word2000.Range;
begin
  Try
    StoryRange := wa.ActiveDocument.StoryRanges.Item(iStoryIndex);
    If StoryRange <> nil then
      begin
        StoryRange.Fields.Update;
        StoryRange.Fields.Unlink;
      end;
  Except
  End;
end;

И вызываю ее перед сохранением документа.

MSWordUpdateStoryRange(wdMainTextStory);
MSWordUpdateStoryRange(wdTextFrameStory);

   Все бы ничего, но часть текста передаваемого из программы в MS Word необходимо было выводить жирным шрифтом. А это через переменные не сделать :( В подобном случае "поиск и замену текста" можно заменить на "переход к закладке и вывод текста". Например, в шаблон вставляем закладку с именем 'писать текст сюда', а в программе пишем:

Var
  ovBookmarkName, ovWhat: OLEVariant;
begin
  ovWhat := wdGoToBookmark;
  ovBookmarkName := 'писать текст сюда';
  Try
    If wa.Selection.GoTo_(ovWhat, EmptyParam, EmptyParam, ovBookmarkName) <> nil then
      begin
        wa.Selection.TypeText('просто текст ');
        wa.Selection.Font.Bold := 1;
        wa.Selection.TypeText('жирный текст');
        wa.Selection.Font.Bold := 0;
        wa.Selection.TypeText(' просто текст');
      end;
  Except
  End;

   Вот и все :)
   Раннее связывание и использование полей/закладок дало существенный рост скорости генерации писем. Если на 500-х различных документах этого было почти не заметно, то при генерации 15 000 документов, прирост скорости составил 30% (специально проверил несколько раз на одних и тех же исходных данных).

P.S. При работе с ранним связыванием мне не нравится только одно – многие параметры в методах объявлены, как VAR (даже индекс элемента коллекции!), поэтому приходится заводить для них специальную переменную типа OleVariant.

09 марта 2010

Поиск и замена текста в документе MS Word из Delphi

   Попросили меня доработать старый модуль, который генерирует клиентам компании письма в формате MS Word. Пользователи создают шаблоны, в которых расставляют названия полей, заключенные в служебные символы (например, #CONTRACT_NUM#, #FIO#, #ADDRESS#...). Программа по заданным критериям выбирает информацию о клиентах из базы и генерирует письма, находя в тексте шаблонов названия полей и заменяя их фактическими значениями.
   Если опустить все детали и различную логику, то упрощенно это выглядит так:

Var
  MSWord: OleVariant;
  i: Integer;
  q: TSDQuery;
  ...
begin
  ...
  MSWord := CreateOleObject('Word.Application');
  MSWord.Documents.Add('какой то шаблон.dot');
  For i := 0 to q.FieldCount-1 do
    MSWord.Selection.Find.Execute(FindText := '#' + q.Fields[i].FieldName + '#', ReplaceWith := q.Fields[i].AsString);
  ...
  MSWord.ActiveDocument.SaveAs('письмо любимому клиенту.doc');
  ...

   Первое, с чем я столкнулся, это то, что длина значения параметра ReplaceWith не должна превышать 255 символов. Но это я обошел легко, заменив "поиск и замену текста" на "поиск и вывод текста":

If MSWord.Selection.Find.Execute(FindText := '#' + q.Fields[i].FieldName + '#') then
  MSWord.Selection.TypeText(q.Fields[i].AsString);

   Вторая задачка оказалась сложнее. Внизу листа в фиксированное место необходимо было вывести фамилию и адрес получателя. Сначала я думал поместить его в нижний колонтитул, но оказалось, что письмо может быть на двух листах. Тогда ничего не оставалось, как использовать объект "заметка". Вставил "заметку". Красота! Документ генерируется, текст сдвигается, заметка остается на месте... Но радость была недолгой, т.к. поля #FIO# и #ADDRESS#, помещенные в заметку, так и остались незамененными :(
   Оказалось, что MSWord.Selection.Find.Execute ищет текст только в основной части документа, а в документе, состоящем из разных структурных элементов (заметок, колонтитулов, сносок и т.д.), поиск необходимо производить отдельно в каждом из этих элементов. Все эти структурные элементы документа являются элементами коллекции StoryRanges. Т.к. дело было к ночи, а модуль должен был быть готов к утру, я не стал разбираться, как работать со StoryRanges через OLE из Delphi, и просто добавил в тестовый шаблон письма макрос на VBA, в котором перебираются все структурные элементы активного документа, в которых ведется поиск:

Sub ReplaceText(sFindText As String, sReplaceText As String)
  Dim rngStory As Range
  For Each rngStory In ActiveDocument.StoryRanges
    With rngStory.Find
      .Text = sFindText
      .Replacement.Text = sReplaceText
      .Wrap = wdFindContinue
      .Execute Replace:=wdReplaceAll
    End With
  Next rngStory
End Sub

А в программе я только написал вызов макроса:

MSWord.Application.Run('ReplaceText', '#FIO#', 'Иванов Иван Иванович');

Работает как часы :)

P.S. После окончания генерации документа, если MS Word вам больше не нужен, то не забывайте закрывать его и высвобождать память

MSWord.Quit;
MSWord := UnAssigned;

P.P.S. Для подобной задачи генерации писем в формат MS Word больше подходит не поиск и замена текста, использование полей с переменными (DocVariable) и закладок (Bookmark). А как это сделать, я расскажу в следующий раз.

04 марта 2010

Delphi 2010: Сносим назойливый Code Formatter

   В далекие школьные годы у нас в школе стоял компьютер ДВК-2М. По сравнению с общераспространенными БК - это было чудо техники, с нормальным монитором и даже с винчестером. На нем я писал свои первые программки. Однажды, когда я писал очередной "шедевр", в соседний кабинет вошла уборщица и включила свет. Этот щелчок включателя я запомнил надолго. При включении света, компьютер моргнул экраном и начал перезагружаться, унеся с собой больше часа моей работы. Это послужило для меня уроком. С тех пор, я регулярно сохраняюсь и, раз в несколько минут, мои руки автоматически жмут Ctrl+S.
   Когда я начал писать свою первую программу на Delphi 2010, я случайно промахнулся и нажал Ctrl+D вместо Ctrl+S. Каково было мое изумление, когда я заметил, что код программы стал выглядеть совсем не так, как я привык его форматировать за свои 14 лет работы с Delphi.