Встpечаются два интернетчика:
- Жена меня нy совсем достала! Все вpемя шипит на меня!
- А может, она сконнектиться пытается?
Объединяя сказанное о сериализации, десериализации объектов и создании DTD соберем полноценный компонент для XML сериализации.
Компонент конвертирует компонент в XML и обратно в соответствии с published-интерфейсом класса компонента.
XML формируется в виде пар тегов с вложенными в них значениями. Атрибуты у тегов отсутствуют.
Тег верхнего уровня соответствует классу объекта. Вложенные теги соответствуют именам свойств. Для элементов коллекций контейнерный тег соответствует имени класса.
Вложенность тегов не ограничена и полностью повторяет published интерфейс класса заданного объекта.
Поддерживаются целые типы, типы с плавающей точкой, перечисления, наборы, строки, символы. вариантные типы, классовые типы, стоковые списки и коллекции.
property GenerateFormattedXML; // создавать форматированный XML код property ExcludeEmptyValues; // пропускать пустые значения свойств property ExcludeDefaultValues; // пропускать значения по умолчанию property OnGetXMLHeader; // позволяет указать свой XML заголовок
Ограничения:
В объекте допустимо использовать только одну коллекцию каждого типа. Для преодоления этого ограничения требуется некоторая доработка.
Наследники класса TStrings не могут иметь published свойств.
Процедурные типы не обрабатываются.
Для генерации DTD у объекта все свойства классовых типов, одноименные со свойствами агрегированных объектов, должны быть одного класса.
Предусловия:
Объект для (де)сериализации должен быть создан до вызова процедуры.
Дополнительно:
При загрузке из XML содержимое коллекций в объекте не очищается, что позволяет дозагружать данные из множества источников в один объект.
unit glXMLSerializer;
{
Globus Delphi VCL Extensions Library ' GLOBUS LIB '
Copyright (c) 2001 Chudin A.V, chudin@yandex.ru
glXMLSerializer Unit 08.2001 component TglXMLSerializer
}interfaceuses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, comctrls, TypInfo;
type
TOnGetXMLHeader = procedure Ошибка! Недопустимый объект гиперссылки.
(Sender: TObject; var Value: string) ofobject;
XMLSerializerException = class(Exception)
end;
TglXMLSerializer = class(TComponent)
private{ Private declarations }
Buffer: PChar;
BufferLength: DWORD;
TokenPtr: PChar;
OutStream: TStream;
FOnGetXMLHeader: TOnGetXMLHeader;
FGenerateFormattedXML: boolean;
FExcludeEmptyValues: boolean;
FExcludeDefaultValues: boolean;
FReplaceReservedSymbols: boolean;
procedure check(Expr: boolean; constmessage: string);
procedure WriteOutStream(Value: string);
protectedprocedure SerializeInternal(Component: TObject; Level: integer = 1);
procedure DeSerializeInternal(Component: TObject; const ComponentTagName:
string; ParentBlockEnd: PChar = nil);
procedure GenerateDTDInternal(Component: TObject; DTDList: TStrings;
Stream: TStream; const ComponentTagName: string);
procedure SetPropertyValue(Component: TObject; PropInfo: PPropInfo;
Value, ValueEnd: PChar; ParentBlockEnd: PChar);
public
tickCounter, tickCount: DWORD;
constructor Create(AOwner: TComponent); override;
{ Сериализация объекта в XML }procedure Serialize(Component: TObject; Stream: TStream);
{ Загрузка XML в объект }procedure DeSerialize(Component: TObject; Stream: TStream);
{ Генерация DTD }procedure GenerateDTD(Component: TObject; Stream: TStream);
publishedproperty GenerateFormattedXML: boolean
read FGenerateFormattedXML write FGenerateFormattedXML default true;
property ExcludeEmptyValues: boolean
read FExcludeEmptyValues write FExcludeEmptyValues;
property ExcludeDefaultValues: boolean
read FExcludeDefaultValues write FExcludeDefaultValues;
property ReplaceReservedSymbols: boolean
read FReplaceReservedSymbols write FReplaceReservedSymbols;
property OnGetXMLHeader: TOnGetXMLHeader
read FOnGetXMLHeader write FOnGetXMLHeader;
end;
procedureregister;
implementationuses dsgnintf, glUtils;
const
ORDINAL_TYPES = [tkInteger, tkChar, tkEnumeration, tkSet];
TAB: string = #9;
CR: string = #13#10;
procedureregister;
begin
RegisterComponents('Gl Components', [TglXMLSerializer]);
end;
constructor TglXMLSerializer.Create(AOwner: TComponent);
begininherited;
//...defaults
FGenerateFormattedXML := true;
end;
{ пишет строку в выходящий поток. Исп-ся при сериализации }procedure TglXMLSerializer.WriteOutStream(Value: string);
begin
OutStream.write(Pchar(Value)[0], Length(Value));
end;
{
Конвертирует компонент в XML-код в соответствии
с published интерфейсом класса объекта.
Вход:
Component - компонент для конвертации
Выход:
текст XML в поток Stream
}procedure TglXMLSerializer.Serialize(Component: TObject; Stream: TStream);
var
Result: string;
begin
TAB := IIF(GenerateFormattedXML, #9, '');
CR := IIF(GenerateFormattedXML, #13#10, '');
Result := '';
{ Получение XML заголовка }if Assigned(OnGetXMLHeader) then
OnGetXMLHeader(self, Result);
OutStream := Stream;
WriteOutStream( PChar(CR + '<' + Component.ClassName + '>') );
SerializeInternal(Component);
WriteOutStream( PChar(CR + '</' ? + Component.ClassNameend;
{
Внутренняя процедура конвертации объекта в XML
Вызывается из:
Serialize()
Вход:
Component - компонент для конвертации
Level - уровень вложенности тега для форматирования результата
Выход:
строка XML в выходной поток через метод WriteOutStream()
}procedure TglXMLSerializer.SerializeInternal(Component: TObject; Level: integer = 1);
var
PropInfo: PPropInfo;
TypeInf, PropTypeInf: PTypeInfo;
TypeData: PTypeData;
i, j: integer;
AName, PropName, sPropValue: string;
PropList: PPropList;
NumProps: word;
PropObject: TObject;
{ Добавляет открывающий тег с заданным именем }procedure addOpenTag(const Value: string);
begin
WriteOutStream(CR + DupStr(TAB, Level) + '<' + Value + '>');
inc(Level);
end;
{ Добавляет закрывающий тег с заданным именем }procedure addCloseTag(const Value: string; addBreak: boolean = false);
begin
dec(Level);
if addBreak then
WriteOutStream(CR + DupStr(TAB, Level));
WriteOutStream('</' ? + Valueend;
{ Добавляет значение в результирующую строку }procedure addValue(const Value: string);
begin
WriteOutStream(Value);
end;
begin// Result := '';{ Playing with RTTI }
TypeInf := Component.ClassInfo;
AName := TypeInf^.name;
TypeData := GetTypeData(TypeInf);
NumProps := TypeData^.PropCount;
GetMem(PropList, NumProps*sizeof(pointer));
try{ Получаем список свойств }
GetPropInfos(TypeInf, PropList);
for i := 0 to NumProps-1 dobegin
PropName := PropList^[i]^.name;
PropTypeInf := PropList^[i]^.PropType^;
PropInfo := PropList^[i];
{ Хочет ли свойство, чтобы его сохранили ? }ifnot IsStoredProp(Component, PropInfo) then
continue;
case PropTypeInf^.Kind of
tkInteger, tkChar, tkEnumeration, tkFloat, tkString, tkSet,
tkWChar, tkLString, tkWString, tkVariant:
begin{ Получение значения свойства }
sPropValue := GetPropValue(Component, PropName, true);
{ Проверяем на пустое значение и значение по умолчанию }if ExcludeEmptyValues and (sPropValue = '') then
continue;
if ExcludeDefaultValues and (PropTypeInf^.Kind in ORDINAL_TYPES)
and (sPropValue = IntToStr(PropInfo.default)) then
continue;
{ Замена спецсимволов }if FReplaceReservedSymbols thenbegin
sPropValue := StringReplace(sPropValue, '<', '%lt;', [rfReplaceAll]);
sPropValue := StringReplace(sPropValue, '>', '%gt;', [rfReplaceAll]);
sPropValue := StringReplace(sPropValue, '&', '%', [rfReplaceAll]);
end;
{ Перевод в XML }
addOpenTag(PropName);
addValue(sPropValue); { Добавляем значение свойства в результат }
addCloseTag(PropName);
end;
tkClass: { Для классовых типов рекурсивная обработка }begin
addOpenTag(PropName);
PropObject := GetObjectProp(Component, PropInfo);
if Assigned(PropObject)thenbegin{ Для дочерних свойств-классов - рекурсивный вызов }if (PropObject is TPersistent) then
SerializeInternal(PropObject, Level);
{ Индивидуальный подход к некоторым классам }if (PropObject is TStrings) then{ Текстовые списки }begin
WriteOutStream(TStrings(PropObject).CommaText);
endelseif (PropObject is TCollection) then{ Коллекции }begin
SerializeInternal(PropObject, Level);
for j := 0 to (PropObject as TCollection).Count-1 dobegin{ Контейнерный тег по имени класса }
addOpenTag(TCollection(PropObject).Items[j].ClassName);
SerializeInternal(TCollection(PropObject).Items[j], Level);
addCloseTag(TCollection(PropObject).Items[j].ClassName, true);
endend;
{ Здесь можно добавить обработку остальных классов: TTreeNodes, TListItems }end;
{ После обработки свойств закрываем тег объекта }
addCloseTag(PropName, true);
end;
end;
end;
finally
FreeMem(PropList, NumProps*sizeof(pointer));
end;
end;
{
Загружает в компонент данные из потока с XML-кодом.
Вход:
Component - компонент для конвертации
Stream - источник загрузки XML
Предусловия:
Объект Component должен быть создан до вызова процедуры
}procedure TglXMLSerializer.DeSerialize(Component: TObject; Stream: TStream);
begin
GetMem(Buffer, Stream.Size);
try{ Получаем данные из потока }
Stream.read(Buffer[0], Stream.Size + 1);
{ Устанавливаем текущий указатель чтения данных }
TokenPtr := Buffer;
BufferLength := Stream.Size-1;
{ Вызываем загрузчик }
DeSerializeInternal(Component, Component.ClassName);
finally
FreeMem(Buffer);
end;
end;
{
Рекурсивная процедура загрузки объекта их текстового буфера с XML
Вызывается из:
Serialize()
Вход:
Component - компонент для конвертации
ComponentTagName - имя XML тега объекта
ParentBlockEnd - указатель на конец XML описания родительского тега
}procedure TglXMLSerializer.DeSerializeInternal(Component: TObject;
const ComponentTagName: string; ParentBlockEnd: PChar = nil);
var
BlockStart, BlockEnd, TagStart, TagEnd: PChar;
TagName, TagValue, TagValueEnd: PChar;
TypeInf: PTypeInfo;
TypeData: PTypeData;
PropIndex: integer;
AName: string;
PropList: PPropList;
NumProps: word;
{ Поиск у объекта свойства с заданным именем }function FindProperty(TagName: PChar): integer;
var
i: integer;
begin
Result := -1;
for i := 0 to NumProps-1 doif CompareStr(PropList^[i]^.name, TagName) = 0 thenbegin
Result := i;
break;
end;
end;
procedure SkipSpaces(var TagEnd: PChar);
beginwhile TagEnd[0] <= #33 do
inc(TagEnd);
end;
function StrPos2(const Str1, Str2: PChar; Str2Len: DWORD): PChar; assembler;
asm
PUSH EDI
PUSH ESI
PUSH EBX
or EAX,EAX // Str1
JE @@2 // если строка Str1 пуста - на выходor EDX,EDX // Str2
JE @@2 // если строка Str2 пуста - на выход
MOV EBX,EAX
MOV EDI,EDX // установим смещение для SCASB - подстрока Str2xor AL,AL // обнулим AL
push ECX // длина строки
MOV ECX,0FFFFFFFFH // счетчик с запасом
REPNE SCASB // ищем конец подстроки Str2not ECX // инвертируем ECX - получаем длину строки+1
DEC ECX // в ECX - длина искомой подстроки Str2
JE @@2 // при нулевой длине - все на выход
MOV ESI,ECX // сохраняем длину подстроки в ESI
pop ECX
SUB ECX,ESI // ECX == разница длин строк : Str1 - Str2
JBE @@2 // если длина подсроки больше длине строки - выход
MOV EDI,EBX // EDI - начало строки Str1
LEA EBX,[ESI-1] // EBX - длина сравнения строк
@@1: MOV ESI,EDX // ESI - смещение строки Str2
LODSB // загужаем первый символ подстроки в AL
REPNE SCASB // ищем этот символ в строке EDI
JNE @@2 // если символ не обнаружен - на выход
MOV EAX,ECX // сохраним разницу длин строк
PUSH EDI // запомним текущее смещение поиска
MOV ECX,EBX
REPE CMPSB // побайтно сравниваем строки
POP EDI
MOV ECX,EAX
JNE @@1 // если строки различны - ищем следующее совпадение первого символа
LEA EAX,[EDI-1]
JMP @@3
@@2: xor EAX,EAX
@@3: POP EBX
POP ESI
POP EDI
end;
begin{ Playing with RTTI }
TypeInf := Component.ClassInfo;
AName := TypeInf^.name;
TypeData := GetTypeData(TypeInf);
NumProps := TypeData^.PropCount;
GetMem(PropList, NumProps*sizeof(pointer));
try
GetPropInfos(TypeInf, PropList);
{ ищем открывающий тег }
BlockStart := StrPos2(TokenPtr, PChar('<' + ComponentTagName + '>'), BufferLength);
check(BlockStart <> nil, 'Открывающий тег не найден: ' + '<' + ComponentTagName + '>');
inc(BlockStart, length(ComponentTagName) + 2);
{ ищем закрывающий тег }
BlockEnd := StrPos2(BlockStart, PChar('</' ? + ComponentTagName nil,
'Закрывающий тег не найден: ' + '<' + ComponentTagName + '>');
{ проверка на вхождение закр. тега в родительский тег }
check((ParentBlockEnd = nil)or(BlockEnd { XML парсер }while TagEnd dobegin{ быстрый поиск угловых скобок }asm
mov CL, '<'
mov EDX, Pointer(TagEnd)
dec EDX
@@1: inc EDX
mov AL, byte[EDX]
cmp AL, CL
jne @@1
mov TagStart, EDX
mov CL, '>'
@@2: inc EDX
mov AL, byte[EDX]
cmp AL, CL
jne @@2
mov TagEnd, EDX
end;
GetMem(TagName, TagEnd - TagStart + 1);
try{ TagName - имя тега }
StrLCopy(TagName, TagStart + 1, TagEnd - TagStart - 1);
{ TagEnd - закрывающий тег }{ поиск свойства, соответствующего тегу }
TagEnd := StrPos2(TagEnd, PChar('</' ? + TagName
PropIndex := FindProperty(TagName);
check(PropIndex <> -1, 'TglXMLSerializer.DeSerializeInternal: Uncknown property: ' +
TagName);
SetPropertyValue(Component, PropList^[PropIndex], TagValue, TagValueEnd, BlockEnd);
inc(TagEnd, length('</' ? + TagNamefinally
FreeMem(TagName);
end;
end;
finally
FreeMem(PropList, NumProps*sizeof(pointer));
end;
end;
{
Процедура инициализации свойства объекта
Вызывается из:
DeSerializeInternal()
Вход:
Component - инициализируемый объект
PropInfo - информация о типе для устанавливаемого свойства
Value - значение свойства
ParentBlockEnd - указатель на конец XML описания родительского тега
Используется для рекурсии
}procedure TglXMLSerializer.SetPropertyValue(Component: TObject;
PropInfo: PPropInfo; Value, ValueEnd: PChar; ParentBlockEnd: PChar);
var
PropTypeInf: PTypeInfo;
PropObject: TObject;
CollectionItem: TCollectionItem;
sValue: string;
charTmp: char;
begin
PropTypeInf := PropInfo.PropType^;
case PropTypeInf^.Kind of
tkInteger, tkChar, tkEnumeration, tkFloat, tkString, tkSet,
tkWChar, tkLString, tkWString, tkVariant:
begin{ имитируем zero terminated string }
charTmp := ValueEnd[0];
ValueEnd[0] := #0;
sValue := StrPas(Value);
ValueEnd[0] := charTmp;
{ Замена спецсимволов. Актуально только для XML,
сохраненного с помощью этого компонента }if FReplaceReservedSymbols thenbegin
sValue := StringReplace(sValue, '%lt;', '<', [rfReplaceAll]);
sValue := StringReplace(sValue, '%gt;', '>', [rfReplaceAll]);
sValue := StringReplace(sValue, '%', '&', [rfReplaceAll]);
end;
{ Для корректного преобразования парсером tkSet нужны угловые скобки }if PropTypeInf^.Kind = tkSet then
sValue := '[' + sValue + ']';
SetPropValue(Component, PropInfo^.name, sValue);
end;
tkClass:
begin
PropObject := GetObjectProp(Component, PropInfo);
if Assigned(PropObject)thenbegin{ Индивидуальный подход к некоторым классам }if (PropObject is TStrings) then{ Текстовые списки }begin
charTmp := ValueEnd[0];
ValueEnd[0] := #0;
sValue := StrPas(Value);
ValueEnd[0] := charTmp;
TStrings(PropObject).CommaText := sValue;
endelseif (PropObject is TCollection) then{ Коллекции }beginwhile true do{ Заранее не известно число элементов в коллекции }begin
CollectionItem := (PropObject as TCollection).Add;
try
DeSerializeInternal(CollectionItem, CollectionItem.ClassName,
ParentBlockEnd);
except{ Исключение, если очередной элемент не найден }
CollectionItem.Free;
break;
end;
end;
endelse{ Для остальных классов - рекурсивная обработка }
DeSerializeInternal(PropObject, PropInfo^.name, ParentBlockEnd);
end;
end;
end;
end;
{
Процедура генерации DTD для заданного объекта в
соответствии с published интерфейсом его класса.
Вход:
Component - объект
Выход:
текст DTD в поток Stream
}procedure TglXMLSerializer.GenerateDTD(Component: TObject; Stream: TStream);
var
DTDList: TStringList;
begin
DTDList := TStringList.Create;
try
GenerateDTDInternal(Component, DTDList, Stream, Component.ClassName);
finally
DTDList.Free;
end;
end;
{
Внутренняя рекурсивная процедура генерации DTD для заданного объекта.
Вход:
Component - объект
DTDList - список уже определенных элементов DTD
для предотвращения повторений.
Выход:
текст DTD в поток Stream
}procedure TglXMLSerializer.GenerateDTDInternal(Component: TObject; DTDList:
TStrings; Stream: TStream; const ComponentTagName: string);
var
PropInfo: PPropInfo;
TypeInf, PropTypeInf: PTypeInfo;
TypeData: PTypeData;
i: integer;
AName, PropName, TagContent: string;
PropList: PPropList;
NumProps: word;
PropObject: TObject;
const
PCDATA = '#PCDATA';
procedure addElement(const ElementName: string; Data: string);
var
s: string;
beginif DTDList.IndexOf(ElementName) <> -1 then
exit;
DTDList.Add(ElementName);
s := 'then Data := PCDATA;
s := s + '(' + Data + ')>'#13#10;
Stream.Write(PChar(s)[0], length(s));
end;
begin{ Playing with RTTI }
TypeInf := Component.ClassInfo;
AName := TypeInf^.name;
TypeData := GetTypeData(TypeInf);
NumProps := TypeData^.PropCount;
GetMem(PropList, NumProps*sizeof(pointer));
try{ Получаем список свойств }
GetPropInfos(TypeInf, PropList);
TagContent := '';
for i := 0 to NumProps-1 dobegin
PropName := PropList^[i]^.name;
PropTypeInf := PropList^[i]^.PropType^;
PropInfo := PropList^[i];
{ Пропустить не поддерживаемые типы }ifnot (PropTypeInf^.Kind in [tkDynArray, tkArray,
tkRecord, tkInterface, tkMethod]) thenbeginif TagContent <> '' then
TagContent := TagContent + '|';
TagContent := TagContent + PropName;
end;
case PropTypeInf^.Kind of
tkInteger, tkChar, tkFloat, tkString,
tkWChar, tkLString, tkWString, tkVariant, tkEnumeration, tkSet:
begin{ Перевод в DTD. Для данных типов модель содержания - #PCDATA }
addElement(PropName, PCDATA);
end;
{ код был бы полезен при использовании атрибутов
tkEnumeration:
begin
TypeData:= GetTypeData(GetTypeData(PropTypeInf)^.BaseType^);
s := '';
for j := TypeData^.MinValue to TypeData^.MaxValue do
begin
if s <> '' then s := s + '|';
s := s + GetEnumName(PropTypeInf, j);
end;
addElement(PropName, s);
end;
}
tkClass: { Для классовых типов рекурсивная обработка }begin
PropObject := GetObjectProp(Component, PropInfo);
if Assigned(PropObject)thenbegin{ Для дочерних свойств-классов - рекурсивный вызов }if (PropObject is TPersistent) then
GenerateDTDInternal(PropObject, DTDList, Stream, PropName);
end;
end;
end;
end;
{ Индивидуальный подход к некоторым классам }{ Для коллекций необходимо включить в модель содержания тип элемента }if (Component is TCollection) thenbeginif TagContent <> '' then
TagContent := TagContent + '|';
TagContent := TagContent + (Component as TCollection).ItemClass.ClassName + '*';
end;
{ Добавляем модель содержания для элемента }
addElement(ComponentTagName, TagContent);
finally
FreeMem(PropList, NumProps*sizeof(pointer));
end;
end;
procedure TglXMLSerializer.check(Expr: boolean; constmessage: string);
beginifnot Expr thenraise XMLSerializerException.Create ('XMLSerializerException'#13#10#13#10 + message);
end;
end.
//(PShortString(@(GetTypeData(GetTypeData (PropTypeInf)^.BaseType^).NameList)))//tickCount := GetTickCount();//inc(tickCounter, GetTickCount() - tickCount);
Эта часть кода - компонент Delphi для сериализации и десериализации объектов в формате XML. Он позволяет конвертировать объекты в XML-формат и обратно.
Интерфейс
Интерфейсный раздел определяет методы и свойства компонента TglXMLSerializer.
Serialize: конвертирует объект в XML-формат
DeSerialize: конвертирует XML-формат обратно в объект
GenerateDTD: генерирует DTD (Document Type Definition) для объекта
Имплементация
Имплементационный раздел содержит реальный код для методов и свойств, определенных в интерфейсном разделе.
Сериализация
Метод Serialize отвечает за конвертацию объекта в XML-формат. Он использует RTTI (Run-Time Type Information) для перебора свойств объекта и затем конвертирует каждое свойство в XML-формат с помощью рекурсивного подхода.
Для каждого свойства он проверяет, имеет ли оно значение, которое нужно сериализовать. Если так, он конвертирует это значение в строку и добавляет открывающий тег для свойства в поток вывода. Затем он рекурсивно вызывает себя, чтобы сериализовать любые вложенные объекты или свойства. Наконец, он добавляет закрывающий тег для свойства.
Десериализация
Метод DeSerialize отвечает за конвертацию XML-формата обратно в объект. Он использует RTTI для перебора свойств объекта и затем десериализирует каждое свойство из потока ввода с помощью рекурсивного подхода.
Для каждого свойства он проверяет, имеет ли оно значение, которое нужно десериализовать. Если так, он читает это значение из потока ввода, конвертирует его обратно в оригинальный тип (например, целое число или строку) и устанавливает значение свойства соответственно. Затем он рекурсивно вызывает себя, чтобы десериализовать любые вложенные объекты или свойства.
Генерация DTD
Метод GenerateDTD генерирует DTD для объекта. Он использует RTTI для перебора свойств объекта и затем добавляет каждое свойство в DTD с помощью рекурсивного подхода.
Для каждого свойства он проверяет, имеет ли оно значение, которое нужно включить в DTD. Если так, он добавляет имя свойства и его тип данных (например, #PCDATA для текста) в DTD. Затем он рекурсивно вызывает себя, чтобы генерировать DTD для любых вложенных объектов или свойств.
Обработка ошибок
Метод check - вспомогательная функция, которая выбрасывает исключение, если условие не выполняется.
В целом, этот компонент предоставляет гибкий и настраиваемый способ сериализации и десериализации объектов в XML-формат, а также генерации DTD для объекта. Он полезен при работе с XML-данными в приложениях Delphi.
Компонент для XML-сериализации реализация компонента для сериализации и десериализации объектов в XML-файл с возможностью форматирования результатов, генерации DTD и поддержкой различных типов
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.