Как уже все знают, в Delphi 2010 механизм RTTI претерпел значительные изменения, став еще более простым в применении. Я же постараюсь показать на примере, как можно, используя обновленный RTTI, включить в собственное приложение поддержку форматов файлов, основанных на XML.
В качестве объекта для экспериментов выберем SpreadsheetML.
SpreadsheetML - основанный на XML язык разметки, разработанный Microsoft для представления табличных данных в Excel. Используется во всех версиях MS Excel, начиная с MS Excel 2002. SpreadsheetML может быть использован когда:
Очевидно, что и сам файл, и отдельные его части можно представить в виде объектов, а задача сводится к их сериализации в нужный нам формат текстового файла. Итак, SML-файл должен содержать:
Атрибут TSMLTag соответствует узлу SpreadsheetML-документа, имеющему дочерние узлы, т.е. фактически соответствует объекту. Атрибут TSMLAttribute соответствует свойству объекта, значение которого должно записываться в виде значения XML-атрибута. И, наконец, атрибут TSMLTagValue будет соответствовать свойству объекта, значение которого запишется как значение XML-узла.
Дополнительно мы описали атрибуты TSMLTagNamespace для описания пространства имен XML-узла и TSMLEnumValue для хранения допустимых значений перечислимых типов. Теперь приступим к написанию кода классов, экземпляры которых и составят стройное здание SpreadsheetML-документа. Создадим класс, описывающий сам документ:
По приведенному коду видно, что корневым элементом документа будет XML-узел с именем Workbook, и для этого узла указаны 5 пространств имен. Сам документ является коллекцией объектов "Лист" (TSMLWorksheet). Значения свойств класса TSimpleSpreadsheet, отмеченных атрибутами-наследниками TCustomAttribute, записываются в файл.
"Сердцем" (или "мозгом", если угодно) является метод ObjectAsNode, который и представляет экземпляр класса TSimpleSpreadsheet в виде корневого узла документа, основываясь на RTTI и описанных нами выше атрибутах. Вызывая, если необходимо - рекурсивно, вложенную процедуру _SerializeObject, метод обходит все свойства экземпляра класса TSimpleSpreadsheet и "вложенных" в него объектов, составляющих документ, и создает иерархию XML-узлов, сохраняя значения свойств в эти узлы и их атрибуты.
Дальше открываем XML Spreadsheet Reference, и описываем классы, соответствующие остальным узлам документа, обязательно указывая необходимые RTTI-атрибуты:
Затем создаем стиль, который будет использоваться по умолчанию для всех ячеек (стиль обязательно должен называться "Default"):
Аналогично можно указать и еще несколько стилей. Стили могут наследовать свойства от других стилей. Для этого в свойстве Style.ParentStyle укажите имя стиля, от которого необходимо наследовать. Если указанное свойство пусто, то стиль наследует от стиля Default.
Теперь создадим рабочую книгу:
Заполняем ячейки
Не обязательно указывать стиль для каждой ячейки. Например, вам необходимо, чтобы все ячейки одной колонки использовали одинаковый стиль. В этом случае наш пример с заполнением ячеек станет таким:
Естественно, стиль column должен быть определен заранее. Теперь сохраняем полученный документ в файл
и удаляем объект.
И, под занавес, ссылки:
Dive into SpreadsheetML
XML Spreadsheet Reference
SpreadsheetML.pas (необходима библиотека NativeXML)
DatasetHelper.pas - демонстрация сохранения набора данных в файлы CSV и XML Spreadsheet
В качестве объекта для экспериментов выберем SpreadsheetML.
SpreadsheetML - основанный на XML язык разметки, разработанный Microsoft для представления табличных данных в Excel. Используется во всех версиях MS Excel, начиная с MS Excel 2002. SpreadsheetML может быть использован когда:
- ваше приложение должно обмениваться данными с мобильными устройствами;
- ваше приложение должно уметь создавать XML-документы для передачи на дльнейшую обработку другим приложениям, в том числе и работающих на других платформах;
- вам необходимо избегать использования COM;
- вам необходимо открывать файлы текстовым редактором для их последующего анализа.
<workbook xmlns:html="http://www.w3.org/TR/REC-html40" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="urn:schemas-microsoft-com:office:spreadsheet">
<documentproperties xmlns="urn:schemas-microsoft-com:office:office">
<author>Bill Gates</author>
<lastauthor>Bill Gates</lastauthor>
<company>Microsoft</company>
<version>12.00</version>
<created>2013-07-12T02:43:10Z</created>
</documentproperties>
<styles>
<style ss:id="Default" ss:name="Normal">
<alignment ss:Horizontal="Left" ss:Vertical="Center"/>
<font ss:FontName="Arial" ss:Color="#080000"/>
</style>
<style ss:id="s:title">
<alignment ss:Horizontal="Center"/>
<font ss:FontName="Arial" ss:Bold="1" ss:Color="#0000FF"/>
<interior ss:Color="#C0C0C0"/>
</style>
<style ss:id="s:cell">
<font ss:FontName="Arial" ss:Color="#080000" ss:Size="8"/>
</style>
<style ss:id="cell:GeneralNumber" ss:parent="s:cell">
<font ss:FontName="Arial" ss:Color="#080000" ss:Size="8"/>
<numberformat ss:Format="General Number"/>
</style>
<style ss:id="cell:Currency" ss:parent="s:cell">
<font ss:FontName="Arial" ss:Color="#080000" ss:Size="8"/>
<numberformat ss:Format="Currency"/>
</style>
</styles>
<worksheet ss:name="Page 1">
<table ss:expandedcolumncount="7" ss:expandedrowcount="2" x:fullcolumns="1" x:fullrows="1">
<column ss:autofitwidth="1" ss:index="1">
<column ss:autofitwidth="1" ss:index="2">
<column ss:autofitwidth="1" ss:index="3">
<column ss:autofitwidth="1" ss:index="4">
<column ss:autofitwidth="1" ss:index="5">
<column ss:autofitwidth="1" ss:index="6">
<column ss:autofitwidth="1" ss:index="7">
<row ss:autofitheight="0" ss:index="1">
<cell ss:index="1" ss:styleid="s:title">
<data ss:type="String">Column 1</data>
</cell>
<cell ss:index="2" ss:styleid="s:title">
<data ss:type="String">Column 2</data>
</cell>
<cell ss:index="3" ss:styleid="s:title">
<data ss:type="String">Column 3</data>
</cell>
<cell ss:index="4" ss:styleid="s:title">
<data ss:type="String">Column 4</data>
</cell>
<cell ss:index="5" ss:styleid="s:title">
<data ss:type="String">Column 5</data>
</cell>
<cell ss:index="6" ss:styleid="s:title">
<data ss:type="String">Column 6</data>
</cell>
<cell ss:index="7" ss:styleid="s:title">
<data ss:type="String">Column 7</data>
</cell>
</row>
<row ss:autofitheight="0" ss:index="2">
<cell ss:index="1" ss:styleid="s:cell">
<data ss:type="String">Cell Value</data>
</cell>
<cell ss:index="2" ss:styleid="s:cell">
<data ss:type="String">Cell Value</data>
</cell>
<cell ss:index="3" ss:styleid="s:cell">
<data ss:type="String">Cell Value</data>
</cell>
<cell ss:index="4" ss:styleid="cell:GeneralNumber">
<data ss:type="String">75</data>
</cell>
<cell ss:index="5" ss:styleid="s:cell">
<data ss:type="String">Cell Value</data>
</cell>
<cell ss:index="6" ss:styleid="s:cell">
<data ss:type="String">Cell Value</data>
</cell>
<cell ss:index="7" ss:styleid="cell:Currency">
<data ss:type="String">7300.00</data>
</cell>
</row>
</table>
</worksheet>
</workbook>
Очевидно, что и сам файл, и отдельные его части можно представить в виде объектов, а задача сводится к их сериализации в нужный нам формат текстового файла. Итак, SML-файл должен содержать:
- коллекцию стилей ячеек (коллекция объектов "Стиль");
- коллекцию листов (worksheets) рабочей книги (коллекция объектов "Лист").
TSMLTag = class(TCustomAttribute)
strict private
FName: string;
public
constructor Create(const AName: string); virtual;
///
/// Имя SpreadsheetML-тэга
///
property Name: string read FName write FName;
end;
TSMLAttribute = class(TSMLTag);
TSMLTagValue = class(TCustomAttribute);
TSMLEnumValue = class(TSMLTag);
TSMLTagNamespace = class(TSMLTag)
strict private
FSchemaUrl: string;
public
/// <summary>
/// Конструктор класса
/// </summary>
/// <param name="AName">
/// Имя пространства имен
/// </param>
/// <param name="ASchemaUrl">
/// Ссылка на DTD-опеределение пространства имен
/// </param>
constructor Create(const AName, ASchemaUrl: string); reintroduce;
/// <summary>
/// Ссылка на DTD-опеределение пространства имен
/// </summary>
property SchemaUrl: string read FSchemaUrl write FSchemaUrl;
end;
Атрибут TSMLTag соответствует узлу SpreadsheetML-документа, имеющему дочерние узлы, т.е. фактически соответствует объекту. Атрибут TSMLAttribute соответствует свойству объекта, значение которого должно записываться в виде значения XML-атрибута. И, наконец, атрибут TSMLTagValue будет соответствовать свойству объекта, значение которого запишется как значение XML-узла.
Дополнительно мы описали атрибуты TSMLTagNamespace для описания пространства имен XML-узла и TSMLEnumValue для хранения допустимых значений перечислимых типов. Теперь приступим к написанию кода классов, экземпляры которых и составят стройное здание SpreadsheetML-документа. Создадим класс, описывающий сам документ:
[TSMLTag('Workbook')]
[TSMLTagNamespace('xmlns', 'urn:schemas-microsoft-com:office:spreadsheet')]
[TSMLTagNamespace('xmlns:o', 'urn:schemas-microsoft-com:office:office')]
[TSMLTagNamespace('xmlns:x', 'urn:schemas-microsoft-com:office:excel')]
[TSMLTagNamespace('xmlns:ss', 'urn:schemas-microsoft-com:office:spreadsheet')]
[TSMLTagNamespace('xmlns:html', 'http://www.w3.org/TR/REC-html40')]
TSimpleSpreadsheet = class(TObjectList<TSMLWorksheet>)
strict private
...
private
...
protected
function ObjectAsNode(AXml: TNativeXml; AParent: TXmlNode; AObject: TObject;
AObjectClass: TClass): TXmlNode; virtual;
public
constructor Create; reintroduce;
destructor Destroy; override;
procedure SaveToFile(const AFilename: string); virtual;
procedure SaveToStream(AStream: TStream); virtual;
function NewStyle(const ID: string): TSMLStyle;
function NewWorksheet(const AName: string): TSMLWorksheet;
procedure DeleteWorksheet(const AName: string);
function WorksheetExists(const AName: string): Boolean;
[RefAttribute]
property DocumentProperties: TDocumentProperties read FDocProps;
[RefAttribute]
property Styles: TSMLStyles read FStyles;
property Worksheet[AName: string]: TSMLWorksheet read GetWorksheet;
property Style[ID: string]: TSMLStyle read GetStyle;
end;
По приведенному коду видно, что корневым элементом документа будет XML-узел с именем Workbook, и для этого узла указаны 5 пространств имен. Сам документ является коллекцией объектов "Лист" (TSMLWorksheet). Значения свойств класса TSimpleSpreadsheet, отмеченных атрибутами-наследниками TCustomAttribute, записываются в файл.
"Сердцем" (или "мозгом", если угодно) является метод ObjectAsNode, который и представляет экземпляр класса TSimpleSpreadsheet в виде корневого узла документа, основываясь на RTTI и описанных нами выше атрибутах. Вызывая, если необходимо - рекурсивно, вложенную процедуру _SerializeObject, метод обходит все свойства экземпляра класса TSimpleSpreadsheet и "вложенных" в него объектов, составляющих документ, и создает иерархию XML-узлов, сохраняя значения свойств в эти узлы и их атрибуты.
function TSimpleSpreadsheet.ObjectAsNode(AXml: TNativeXml; AParent: TXmlNode;
AObject: TObject; AObjectClass: TClass): TXmlNode;
var
RttiCtx: TRttiContext;
...
function _SerializeObject(ANextParent: TXmlNode; NextObject: TObject;
NextObjectClass: TClass): TXmlNode;
var
Value: TValue;
RttiType: TRttiType;
Method: TRttiMethod;
PropInfo: PPropInfo;
i, ListCount: Integer;
Tag, StrValue: string;
Attr: TCustomAttribute;
RttiProp: TRttiProperty;
EncodeFlags: TSMLChunkFlags;
PropNode: TXmlNode;
begin
if not Assigned(NextObject) then Exit(nil);
Tag := GetClassTagName(NextObjectClass);
if Tag = EmptyStr then Exit(nil);
if Assigned(ANextParent) then
Result := ANextParent.NodeNew(Tag)
else
Result := AXml.NodeNew(Tag);
EncodeFlags := [];
if NextObject is TCustomSpreadsheetChunk then
EncodeFlags := TCustomSpreadsheetChunk(NextObject).Flags;
RttiType := RttiCtx.GetType(NextObjectClass);
for Attr in RttiType.GetAttributes do
begin
if Attr is TSMLTagNamespace then
Result.AttributeAdd(TSMLTagNamespace(Attr).Name,
TSMLTagNamespace(Attr).SchemaUrl);
end;
for RttiProp in RttiType.GetProperties do
begin
StrValue := EmptyStr;
PropInfo := TRttiInstanceProperty(RttiProp).PropInfo;
for Attr in RttiProp.GetAttributes do
begin
Value := RttiProp.GetValue(NextObject);
if Value.IsObject then
_SerializeObject(Result, Value.AsObject, Value.TypeData^.ClassType)
else
begin
if System.TypInfo.IsStoredProp(NextObject, PropInfo) then
begin
StrValue := ValueToString(Value, EncodeFlags);
if Attr is TSMLAttribute then
begin
if StrValue <> EmptyStr then
Result.AttributeAdd(TSMLAttribute(Attr).Name, StrValue);
end
else
begin
if Attr is TSMLTag then
begin
if StrValue <> EmptyStr then
begin
PropNode := Result.NodeNew(TSMLTag(Attr).Name);
PropNode.Value := StrValue;
end;
end
else
begin
if Attr is TSMLTagValue then
Result.Value := StrValue;
end;
end;
end;
end;
end;
end;
Method := RttiType.GetMethod('ToArray');
if Assigned(Method) then
begin
Value := Method.Invoke(NextObject, []);
if Value.IsArray then
begin
ListCount := Value.GetArrayLength;
for i := 0 to ListCount - 1 do
_SerializeObject(Result, Value.GetArrayElement(i).AsObject,
Value.GetArrayElement(i).TypeData^.ClassType);
end;
end;
end;
begin
RttiCtx := TRttiContext.Create;
try
Result := _SerializeObject(AParent, AObject, AObjectClass);
finally
RttiCtx.Free;
end;
end;
Дальше открываем XML Spreadsheet Reference, и описываем классы, соответствующие остальным узлам документа, обязательно указывая необходимые RTTI-атрибуты:
- TDocumentProperties - свойства документа (автор, дата создания и т.п.);
- TSMLStyleAlignment - выравнивание текста внутри ячейки;
- TSMLBorder, TSMLBorders - границы ячейки;
- TSMLFont - шрифт текста в ячейке;
- TSMLInterior - заливка ячейки;
- TSMLNumberFormat - формат текста в ячейке;
- TSMLStyle - стиль ячейки (выравнивание и формат текста, шрифт, заливка, границы...);
- TSMLTableColumn, TSMLTableRow - колонка и строка таблицы;
- TSMLTableCell - ячейка таблицы;
- TSMLTable - таблица
- и, наконец, TSMLWorksheet - лист рабочей книги.
Sml := TSimpleSpreadsheet.Create();
Затем создаем стиль, который будет использоваться по умолчанию для всех ячеек (стиль обязательно должен называться "Default"):
Style := Sml.Styles.NewStyle('Default');
Style.Name := 'Normal';
Style.Alignment.Horizontal := TSMLTextAlignment.taLeft;
Style.Alignment.Vertical := TSMLTextAlignment.taCenter;
Аналогично можно указать и еще несколько стилей. Стили могут наследовать свойства от других стилей. Для этого в свойстве Style.ParentStyle укажите имя стиля, от которого необходимо наследовать. Если указанное свойство пусто, то стиль наследует от стиля Default.
Style := Sml.Styles.NewStyle('cell');
Style.ParentStyle := 'Default';
Style.Font.Bold := True;
Теперь создадим рабочую книгу:
Worksheet := Sml.NewWorksheet('Лист');
Заполняем ячейки
for Column := 1 to 3 do
begin
for Row := 1 to 10 do
begin
Cell := Worksheet.Cell[Row, Column];
Cell.Value := Format('%d:%d', [Row, Column]);
Cell.StyleID := 'cell';
end;
end;
Не обязательно указывать стиль для каждой ячейки. Например, вам необходимо, чтобы все ячейки одной колонки использовали одинаковый стиль. В этом случае наш пример с заполнением ячеек станет таким:
for Column := 1 to 3 do
begin
for Row := 1 to 10 do
begin
Cell := Worksheet.Cell[Row, Column];
Cell.Value := Format('%d:%d', [Row, Column]);
end;
end;
Col := Worksheet.Column[1];
Col.StyleID := 'column';
Естественно, стиль column должен быть определен заранее. Теперь сохраняем полученный документ в файл
Sml.SaveToFile('spreadsheet.xml');
и удаляем объект.
И, под занавес, ссылки:
Dive into SpreadsheetML
XML Spreadsheet Reference
SpreadsheetML.pas (необходима библиотека NativeXML)
DatasetHelper.pas - демонстрация сохранения набора данных в файлы CSV и XML Spreadsheet
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.