2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Delphi 向けの JSON 代替フォーマットを考える

2
Last updated at Posted at 2026-05-09

はじめに

JSON (JavaScript Object Notation) ってデータフォーマットありますよね?

JSON 代替フォーマットを作る

JSON って、JavaScript から使うには便利ですが、他言語から使うには結局パーサーを書く必要があってそれほど便利じゃありません。なんでかって、それは JSON の文法が JavaScript 由来だからって事に尽きます。

Delphi はコンパイラなので、ObjectPascal の文法に沿ったデータフォーマットを作ったとしても Eval() に食わせるような真似はできませんが、それでも何か楽できる方法はないものかと考えました。

See also:

あるんだな、それが。

Delphi 使いならピンと来たと思うのですが、実は構造化データを読み書きできる機構が Delphi には最初から備わっているのです。そのデータフォーマットとは...フォームファイルです。

例えば、このようなフォームがあったとします。

image.png

このフォームのフォームファイル (*.dfm) をテキストエディタで開いてみるとこのようになっています 1

Unit1.dfm
object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Form1'
  ClientHeight = 338
  ClientWidth = 438
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -12
  Font.Name = 'Segoe UI'
  Font.Style = []
  TextHeight = 15
  object Panel1: TPanel
    Left = 88
    Top = 104
    Width = 233
    Height = 137
    TabOrder = 0
    object ComboBox1: TComboBox
      Left = 16
      Top = 16
      Width = 145
      Height = 23
      TabOrder = 0
      Text = 'ComboBox1'
      Items.Strings = (
        'AAAAA'
        'BBBBB'
        'CCCCC')
    end
    object ComboBox2: TComboBox
      Left = 16
      Top = 45
      Width = 145
      Height = 23
      TabOrder = 1
      Text = 'ComboBox2'
      Items.Strings = (
        '11111'
        '22222'
        '33333')
    end
    object RadioButton1: TRadioButton
      Left = 16
      Top = 80
      Width = 113
      Height = 17
      Caption = 'RadioButton1'
      TabOrder = 2
    end
    object RadioButton2: TRadioButton
      Left = 16
      Top = 103
      Width = 113
      Height = 17
      Caption = 'RadioButton2'
      TabOrder = 3
    end
  end
  object Edit1: TEdit
    Left = 88
    Top = 48
    Width = 152
    Height = 23
    TabOrder = 1
    Text = 'Edit1'
  end
  object Button1: TButton
    Left = 246
    Top = 47
    Width = 75
    Height = 25
    Caption = 'Button1'
    TabOrder = 2
  end
  object Button2: TButton
    Left = 165
    Top = 256
    Width = 75
    Height = 25
    Caption = 'Button2'
    TabOrder = 3
  end
  object Button3: TButton
    Left = 246
    Top = 256
    Width = 75
    Height = 25
    Caption = 'Button3'
    TabOrder = 4
  end
end

Delphi はこのデータをリソースとして格納し、必要になった時に読み出してフォームを動的作成しています。フォームの読み書きには TReaderTWriiter が使われています。

image.png

つまり、このフォーム読み書きのためのシリアライズ機構を「もっと汎用的に使ってみようじゃないか?」というのが今回の趣旨となります。

なお、フォームファイルはアプリケーションフレームワークの種類によって拡張子が異なります。

フォームの種類 拡張子
VCL フォーム *.dfm
FireMonkey フォーム *.fmx
CLX フォーム *.xfm
.NET フォーム *.nfm

See also:

DFON (Delphi Form Object Notation)

このフォームファイルのフォーマットを汎用データフォーマットに使うものを DFON (Delphi Form Object Notation) と呼ぶ事にします。

DFON を簡単に使うためのユニット

DFON を簡単に扱うためのユニットを作ってみました。枝葉の部分が多いのでリストが少し長くなってしまいましたが、TDFON クラスだけでも充分に使えるかと思います。

DfonUtils.pas
unit DFONUtils;

interface

uses
  Classes, SysUtils;

type
  TDFON = class
  public
    class procedure LoadFromFile(const AFileName: string; ARoot: TComponent);
    class procedure LoadFromStream(AStream: TStream; ARoot: TComponent);
    class procedure SaveToFile(const AFileName: string; ARoot: TComponent);
    class procedure SaveToStream(AStream: TStream; ARoot: TComponent);
  end;

  TDFONItem = class(TPersistent)
  private
    FKey: string;
    FValue: string;
  published
    property Key: string read FKey write FKey;
    property Value: string read FValue write FValue;
  end;

  TDFONCollectionItem = class(TCollectionItem)
  private
    FKey: string;
    FValue: string;
  published
    property Key: string read FKey write FKey;
    property Value: string read FValue write FValue;
  end;

  TDFONCollection = class(TOwnedCollection)
  private
    function GetValue(const AKey: string): string;
    procedure SetValue(const AKey, AValue: string);
  public
    constructor Create(AOwner: TPersistent);
    function ContainsKey(const AKey: string): Boolean;
    procedure Delete(const AKey: string);
    property Values[const Key: string]: string read GetValue write SetValue;
  end;

implementation

{ TDFON }

class procedure TDFON.LoadFromFile(const AFileName: string; ARoot: TComponent);
var
  Bin, Txt: TMemoryStream;
  Reader: TReader;
begin
  Txt := TMemoryStream.Create;
  Bin := TMemoryStream.Create;
  try
    Txt.LoadFromFile(AFileName);
    Txt.Position := 0;
    ObjectTextToBinary(Txt, Bin);
    Bin.Position := 0;
    Reader := TReader.Create(Bin, 4096);
    try
      Reader.ReadRootComponent(ARoot);
    finally
      Reader.Free;
    end;
  finally
    Bin.Free;
    Txt.Free;
  end;
end;

class procedure TDFON.SaveToFile(const AFileName: string; ARoot: TComponent);
var
  Bin, Txt: TMemoryStream;
  Writer: TWriter;
begin
  Bin := TMemoryStream.Create;
  Txt := TMemoryStream.Create;
  try
    Writer := TWriter.Create(Bin, 4096);
    try
      Writer.WriteRootComponent(ARoot);
    finally
      Writer.Free;
    end;
    Bin.Position := 0;
    ObjectBinaryToText(Bin, Txt);
    Txt.SaveToFile(AFileName);
  finally
    Txt.Free;
    Bin.Free;
  end;
end;

class procedure TDFON.LoadFromStream(AStream: TStream; ARoot: TComponent);
var
  Bin, Txt: TMemoryStream;
  Reader: TReader;
begin
  Txt := TMemoryStream.Create;
  Bin := TMemoryStream.Create;
  try
    Txt.CopyFrom(AStream, AStream.Size);
    Txt.Position := 0;
    ObjectTextToBinary(Txt, Bin);
    Bin.Position := 0;
    Reader := TReader.Create(Bin, 4096);
    try
      Reader.ReadRootComponent(ARoot);
    finally
      Reader.Free;
    end;
  finally
    Bin.Free;
    Txt.Free;
  end;
end;

class procedure TDFON.SaveToStream(AStream: TStream; ARoot: TComponent);
var
  Bin, Txt: TMemoryStream;
  Writer: TWriter;
begin
  Bin := TMemoryStream.Create;
  Txt := TMemoryStream.Create;
  try
    Writer := TWriter.Create(Bin, 4096);
    try
      Writer.WriteRootComponent(ARoot);
    finally
      Writer.Free;
    end;
    Bin.Position := 0;
    ObjectBinaryToText(Bin, Txt);
    Txt.Position := 0;
    AStream.CopyFrom(Txt, Txt.Size);
  finally
    Txt.Free;
    Bin.Free;
  end;
end;

{ TDFONCollection }

constructor TDFONCollection.Create(AOwner: TPersistent);
begin
  inherited Create(AOwner, TDFONCollectionItem);
end;

function TDFONCollection.ContainsKey(const AKey: string): Boolean;
var
  i: Integer;
begin
  for i:=0 to Count-1 do
    if SameText(TDFONCollectionItem(Items[i]).Key, AKey) then
    begin
      Result := True;
      Exit;
    end;  
  Result := False;
end;

procedure TDFONCollection.Delete(const AKey: string);
var
  i: Integer;
begin
  for i:=0 to Count-1 do
    if SameText(TDFONCollectionItem(Items[i]).Key, AKey) then
    begin
      Items[i].Free;
      Exit;
    end;
end;

function TDFONCollection.GetValue(const AKey: string): string;
var
  i: Integer;
  Item: TDFONCollectionItem;
begin
  for i:=0 to Self.Count-1 do
  begin
    Item := TDFONCollectionItem(Self.Items[i]);
    if SameText(Item.Key, AKey) then
    begin
      Result := Item.Value;
      Exit;
    end;
  end;
  Result := '';
end;

procedure TDFONCollection.SetValue(const AKey, AValue: string);
var
  i: Integer;
  Item: TDFONCollectionItem;
begin
  for i:=0 to Self.Count-1 do
  begin
    Item := TDFONCollectionItem(Self.Items[i]);
    if SameText(Item.Key, AKey) then
    begin
      Item.Value := AValue;
      Exit;
    end;
  end;
  Item := Self.Add as TDFONCollectionItem;
  Item.Key := AKey;
  Item.Value := AValue;
end;

end.

インライン変数宣言などを使っていないので、そこそこ古い Delphi でも動作するハズです 2

使い方

単純な使い方は次のようになります。

フォームには 1 つのエディットボックスと 3 つのボタンを配置してあります。

image.png

コントロール 説明
Edit1 値の表示
btnSetValue Edit1 の値を Config.Value に設定
btnRead Config.dfon から値を読み込み、Edit1 に表示
btnWrite Config.Value を Config.dfon へ書き込み
Unit1.pas
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
  Vcl.StdCtrls, System.IOUtils, DFONUtils;

type
  TConfig = class(TComponent)
  private
    FValue: Integer;
  published
    property Value: Integer read FValue write FValue;
  end;

  TForm1 = class(TForm)
    btnRead: TButton;
    btnWrite: TButton;
    btnSetValue: TButton;
    Edit1: TEdit;
    procedure btnSetValueClick(Sender: TObject);
    procedure btnWriteClick(Sender: TObject);
    procedure btnReadClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
    Config: TConfig;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

const
  DFON_NAME = 'Config.dfon';

procedure TForm1.FormCreate(Sender: TObject);
begin
  Config := TConfig.Create(Self);
end;

procedure TForm1.btnSetValueClick(Sender: TObject);
begin
  // Config.Value に値をセット
  Config.Value := StrToIntDef(Edit1.Text, 0);
end;

procedure TForm1.btnReadClick(Sender: TObject);
begin
  // 'Config.dfon' から値を読み込み
  if TFile.Exists(DFON_NAME) then
    TDFON.LoadFromFile(DFON_NAME, Config) // dson ファイルがあれば読み込む
  else
    Config.Value := 0;                    // なければ 0

  // Edit1 に値をセット
  Edit1.Text := Config.Value.ToString;
end;

procedure TForm1.btnWriteClick(Sender: TObject);
begin
  // 'Config.dfon' に値を書き込み
  TDFON.SaveToFile(DFON_NAME, Config);
end;

end.

値 50 をセットして書き込んだ時の Config.dfon は次のようになっています。

object TConfig
  Value = 50
end

DFON のデータ読み書きにはいくつかの制約があります。

  • 元々フォームファイルのために作られているので、データを保持するクラスは TComponent から派生していなくてはならない
  • 公開されたプロパティを RTTI を利用して読み書きするので、プロパティの可視性は published でなくてはならない
  • TComponent なプロパティは参照になってしまい、思ったような形で DFON に保存されない
  • データを保持するクラスにネストして TComponent 派生クラスを使うと (子コンポーネント)、LoadFromFile() の際に新しくインスタンスが作られる (保存した子コンポーネントのコピーが作られてしまう)

使い方 (その2)

データを保持するクラスに TDFONItem 型のプロパティを持たせ、

  TConfig = class(TComponent)
  private
    FValue: Integer;
    FItem2: TDFONItem;
    FItem1: TDFONItem;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property Item1: TDFONItem read FItem1 write FItem1;
    property Item2: TDFONItem read FItem2 write FItem2;
    property Value: Integer read FValue write FValue;
  end;

...

constructor TConfig.Create(AOwner: TComponent);
begin
  inherited;
  FItem1 := TDFONItem.Create;
  FItem2 := TDFONItem.Create;
end;

destructor TConfig.Destroy;
begin
  FItem2.Free;
  FItem1.Free;
  inherited;
end;

このように書き込むと、

  Config.Value := 100;
  Config.Item1.Value := 'abc';
  Config.Item2.Value := 'def';

  // 'Config.dfon' に値を書き込み
  TDFON.SaveToFile(DFON_NAME, Config);

config.dson は次のように吐かれます。

config.dson
object TConfig
  Item1.Value = 'abc'
  Item2.Value = 'def'
  Value = 100
end

使い方 (その3)

データを保持するクラスに TDFONCollection 型のプロパティを持たせ、

type
  TConfig = class(TComponent)
  private
    FValue: Integer;
    FItems: TDFONCollection;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property Items: TDFONCollection read FItems write FItems;
    property Value: Integer read FValue write FValue;
  end;

...

constructor TConfig.Create(AOwner: TComponent);
begin
  inherited;
  FItems := TDFONCollection.Create(Self);
end;

destructor TConfig.Destroy;
begin
  FItems.Free;
  inherited;
end;

このように書き込むと、

  Config.Value := 100;
  Config.Items.Clear;
  with TDFONCollectionItem(Config.Items.Add) do
  begin
    Key := 'Item1';
    Value := 'abc';
  end;
  with TDFONCollectionItem(Config.Items.Add) do
  begin
    Key := 'Item2';
    Value := 'def';
  end;
  Config.Items.Values['Item2'] := 'xyz';

  // 'Config.dfon' に値を書き込み
  TDFON.SaveToFile(DFON_NAME, Config);

config.dson は次のように吐かれます。

object TConfig
  Items = <
    item
      Key = 'Item1'
      Value = 'abc'
    end
    item
      Key = 'Item2'
      Value = 'xyz'
    end>
  Value = 100
end

おわりに

もちろん、真面目に構造化データを扱うなら TJsonSerializer 3 を使うのが正解だと思います。

DFON は「Delphi の仕組みだけでどこまで遊べるか?」という事に主眼を置いた実験的フォーマットなので、実用性よりも Delphi らしさを楽しむ 事に価値があると思っています。

もし、Delphi だけで完結するジョークフォーマットとしての DFON を楽しんで頂けたのなら幸いです m(_ _)m

See also:

  1. Delphi 4 以前はバイナリ形式の DFM しか扱えません。

  2. DFONUtils.pas 自体は Delphi 7 でもコンパイル可能でした。

  3. Delphi 10.2 Tokyo 以降。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?