Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2005年01月01日] [更新 2016年12月25日] Delphi サンプルプログラム集
200_TMemo への描画 ( テキストファイルの読み込み )
動作確認等 Windows 7 U64(SP1) + Delphi XE(UP1) Pro
200_MemoReadSpeed.zip [6,312 KB] 2016年12月25日版 (EXE 同梱)



  • 2013年06月26日
  • サンプルが 1つだけだったが、解説的な内容も含めてサンプルを機能別に分けた
  • 2016年12月25日
  • メモリやデータサイズの制限の記事を、別のページを新規作成して移動




テキストファイルの読み込みと TMemo への描画


文字列であるテキストを主にTMemo コントロールに表示するサンプルです。
テキストの表示には TRichEdit を使用することも多いと思いますが、記事そのものは、TRichEdit を利用する方にも参考になる内容となっています。

この記事を掲載したのは、2005年ですからだいぶ経ちます。その時のサンプルは 1 つだけだったのですが、2013年06月26日の更新では、関連するテストもしてみました。
ただし、テキストファイルからテキストを読み込んで描画することに関することだけで、テキストの編集や、キャレットの制御、文字列の操作関係のテストはありません。また、文字のコードページ関係のサンプルもありません。文字のコードページ関係については、[883_文字コードページとテキストファイルの保存と表示] 等を参考にしてください。


[備考]
TRichEdit でテキストファイルを扱う場合は、PlainText プロパティを True にします。
PlainText プロパティの値が False でも、プレーンテキストを読み込んで表示するだけであれば問題ありませんが、保存すると、リッチテキスト形式で保存されてしまいます。





01_テスト用テキストファイルの作成


テストには、テキストファイルが必要ですが、自身のハードディスクからテスト用のファイルを探すのは面倒です。そこで、テスト用のテキストファイルを作成するプログラムを作成しました。

このページのサンプルプログラムは、全て、このプログラムで作成したファイルを使用しています。計測時間は、筆者の環境での値です、一般的に、コンピュータ、あるいはハードディクスの性能や設定内容、表示するコントロールの表示領域等によっても値が異なります。
読み込んで表示する速度は行数だけでは決まりません。テキストファイルのサイズにもよります。

約 100 GB (ギガバイト - 行数は 1,510,000,000) のテスト用ファイルも作成してみましたが、152 分もかかってしまいました。2 GB の作成に 176 秒かかりましたから、その 52 倍かかったことになります。ほぼ比例しています。

なお、TStreamWriter, TStreamReader は Delphi 2009 で実装されたクラスです。文字コードの指定、バッファサイズの指定、行単位、文字単位の操作もできるクラスです。


図1
設計時画面
  • TSpinEdit は作成する行数の指定用
図2
実行例
  • 30,200,000 行のテキストファイル作成
  • サイズは約 2 GB (ギガバイト)
    ( SHIFT-JIS コードの場合 )
  • 約 176 秒かかった
  • 行数を 2 倍にすると 2 倍時間がかかる
図3
作成したテスト用ファイル群
  • 行数をファイル名にしている

リスト1
テスト用テキストファイルの作成
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, Spin;

type
  TForm1 = class(TForm)
    Button1: TButton;
    SpinEdit1: TSpinEdit;
    LabeledEdit1: TLabeledEdit;
    procedure Button1Click(Sender: TObject);
    procedure FormShow(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses MMSystem;

{$R *.dfm}

//=============================================================================
//  フォーム表示開始時の処理
//=============================================================================
procedure TForm1.FormShow(Sender: TObject);
begin
  Caption := '行数指定でテキストファイル作成';

  SpinEdit1.Value := 15100;

  LabeledEdit1.Text := '../TextFiles/' + IntToStr(SpinEdit1.Value) + '.txt';
  LabeledEdit1.SetFocus;
  LabeledEdit1.SelStart := Length(LabeledEdit1.Text);
end;

//=============================================================================
//  テスト用テキストファイルの作成
//  先頭に行番号を付けた文字列を1行づつ追加していく
//
//  timeGetTimeの使用には、usesにMMSystemが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  FileName  : String;
  StartTime : DWORD;
  EndTime   : DWORD;
  SWriter   : TStreamWriter;
  StrText   : String;
  i         : Integer;
  StrFmt    : String;
  StrRes    : String;
begin
  StrText := '巨大テキストファイルです。行数を指定して作成しています';

  StartTime := TimeGetTime;
  FileName  := Trim(LabeledEdit1.Text);

  //TStreamWriterでファイル生成
  //第2引数をFalseにして新規作成モード
  //デフォルトではUTF-8になるのでSHIFT-JISの指定
  //文字コードによって作成されるファイルのサイズは異なる
  //TStreamWriterのCreateでは、自動的にOwnStreamが設定される
  SWriter := TStreamWriter.Create(FileName, False, TEncoding.GetEncoding(932));
  try
    for i := 0 to SpinEdit1.Value - 1 do begin
      StrFmt := FormatFloat('000,000,000', i + 1);
      SWriter.WriteLine(StrFmt + ' ' + StrText);
      Application.ProcessMessages;
    end;
  finally
    SWriter.Close;
    SWriter.Free;
  end;

  EndTime := timeGetTime;
  StrRes  := IntToStr(EndTime - StartTime) + ' ms' + sLineBreak + '終了しました';
  Application.MessageBox(PChar(StrRes), '情報');

  LabeledEdit1.SetFocus;
  LabeledEdit1.SelStart := Length(LabeledEdit1.Text);
end;

end.




02_テスト用の巨大バイナリファイルを瞬時に作成


テキストファイルではありませんが、バイナリファイルであれば一瞬で作成できます。ただし、中身は全て $00 で埋まったファイルとなります。
このようなファイルは、コピー等のファイル操作、ネットワークでの転送のダミーファイルとして、テストに利用できます。

こうして作成したバイナリファイルを操作するには、TBytesStream を使用すると便利です。



図4
設計時画面
図5
実行画面例
  • ファイルは、このプロジェクトのフォルダに出来る
図6
作成したファイル
  • 100 GB のバイナリファイルを作成
  • 一瞬でできる

リスト2
巨大バイナリファイルを瞬時に作成
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    LabeledEdit1: TLabeledEdit;
    LabeledEdit2: TLabeledEdit;
    procedure Button1Click(Sender: TObject);
    procedure FormShow(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム表示開始時の処理
//=============================================================================
procedure TForm1.FormShow(Sender: TObject);
begin
  Caption    := '巨大バイナリファイル作成';

  LabeledEdit1.Text := 'testfile.bin';
  LabeledEdit1.SetFocus;
  LabeledEdit1.SelStart := Length(LabeledEdit1.Text);

  LabeledEdit2.ImeMode     := imClose;
  LabeledEdit2.NumbersOnly := True;
  LabeledEdit2.Text        := '107374182400'; //100GB = 2^30 * 100
end;

//=============================================================================
//  FileCreate関数で指定サイズのバイナリファイルを作成
//  中身はすべて$00で埋まったファイル
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  FileHandle : NativeInt;
  FileSize   : Int64;
  Ch         : Char;
begin
  FileHandle := FileCreate(LabeledEdit1.Text);
  FileSize   := StrToInt64Def(LabeledEdit2.Text, 1);
  FileSeek(FileHandle, FileSize - 1, soFromBeginning);

  Ch := #$0000;
  FileWrite(FileHandle, Ch, 1);
  FileClose(FileHandle);

  MessageDlg(SysErrorMessage(GetLastError), mtInformation, [mbOK], 0);

  LabeledEdit1.SetFocus;
  LabeledEdit1.SelStart := Length(LabeledEdit1.Text);
end;

end.




03_TMemo への描画と BeginUpdate と EndUpdate の効果


テキストファイルからテキストを読み出して、TMemo へそのテキストを表示するもっとも基本的なコードです。
  • (1) TStringList の LoadFromFile メソッドでファイルからテキストを全て読み込む
  • (2) 読み込んだテキストを 1 行づつ TMemo の 各行に追加していく
という手順で行っています。1 行づつ文字列を TMemo にセットする状況を想定しています。
Button2 のクリックイベントでは、各行に文字列をセットする前に、BeginUpdate で描画を抑制し、全ての行に文字列をセットした後に EndUpdate で描画を更新します。こうすると描画が高速に行われます。このコードがないと、1 行追加する度に描画の更新が実行されます。

下の実行結果の数値を見ると、その違いが確認できます。


図7
設計時画面
  • Edit1 は Button1 の実行結果表示用
  • Edit2 は Button2 の実行結果表示用
図8
実行結果
  • 100 KB (1510 行) のテキストを読み込んだ結果
  • BeginUpdate と EndUpdate がないと 8.6 秒もかかっている

リスト3
BeginUpdate と EndUpdate の効果を確認するテスト
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ExtDlgs;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    Panel2: TPanel;
    Memo1: TMemo;
    Button1: TButton;
    Button2: TButton;
    Edit1: TEdit;
    Edit2: TEdit;
    OpenTextFileDialog1: TOpenTextFileDialog;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
    InitDir : string;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses MMSystem;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.ScrollBars := ssBoth;
  Memo1.WordWrap   := False;

  Edit1.ReadOnly   := True;
  Edit2.ReadOnly   := True;
end;

//=============================================================================
//  TStringListにテキストファイルを読み込み、それをTMemo.Lines(TStrings)に代入
//  1行づつ処理する
//  BeginUpdateとEndUpdateを使用しない場合
//
//  timeGetTimeの使用には、usesにMMSystemが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  StartTime : DWORD;
  EndTime   : DWORD;
  FileName  : String;
  SL        : TStringList;
  i         : Integer;
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);


  StartTime := TimeGetTime;
  FileName  := OpenTextFileDialog1.FileName;

  Edit1.Text := '';
  Edit1.Update;
  Memo1.Lines.Clear;
  Memo1.Update;

  SL := TStringList.Create;
  try
    SL.LoadFromFile(FileName);
    for i := 0 to SL.Count -1 do begin
      Memo1.Lines.Add(SL[i]);
      Application.ProcessMessages;
    end;
  finally
    FreeAndNil(SL);
  end;

  EndTime    := timeGetTime;
  Edit1.Text := IntToStr(EndTime - StartTime) + ' ms';
end;

//=============================================================================
//  TStringListにテキストファイルを読み込み、それをTMemo.Lines(TStrings)に代入
//  1行づつ処理する
//  BeginUpdateとEndUpdateを使用した場合
//
//  timeGetTimeの使用には、usesにMMSystemが必要
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  StartTime : DWORD;
  EndTime   : DWORD;
  FileName  : String;
  SL        : TStringList;
  i         : Integer;
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);


  StartTime := TimeGetTime;
  FileName  := OpenTextFileDialog1.FileName;

  Edit2.Text := '';
  Edit2.Update;
  Memo1.Lines.Clear;
  Memo1.Update;

  Memo1.Lines.BeginUpdate;
  SL := TStringList.Create;
  try
    SL.LoadFromFile(FileName);
    for i := 0 to SL.Count -1 do begin
      Memo1.Lines.Add(SL[i]);
      Application.ProcessMessages;
    end;
  finally
    FreeAndNil(SL);
  end;
  Memo1.Lines.EndUpdate;

  EndTime    := timeGetTime;
  Edit2.Text := IntToStr(EndTime - StartTime) + ' ms';
end;

end.


読み出したテキスト全体、あるいは行単位での処理が必要ない場合は、次のように、LoadFromFile メソッドで、ファイルから直接テキストを読み込むと、高速に処理できます。


procedure TForm1.Button1Click(Sender: TObject);
var
  FileName : String;
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if not OpenTextFileDialog1.Execute then exit;


  FileName  := OpenTextFileDialog1.FileName;

  Memo1.Lines.Clear;
  Memo1.Lines.LoadFromFile(FileName);
end;




04_TStringList 等に全て読み込んでから描画


BeginUpdate と EndUpdate を使用しても、2 MB のテキストでは 9.5 秒程かかりました。
次は、以下の 3 つの方法で、テキストファイルを読み込み、読み込んだ全てのデータを 1 度に TMemo にセットしてみます。
  • TMemoryStream.LoadFromFile でストリームに読み込む
  • TStringList.LoadFromFile で TStringList に読み込む
  • TStreamReader.ReadToEnd で読み込む
1 度別のオブジェクトにテキストを読み込む方法は、読み出したテキスト全体に何かの処理を行う場合に利用できます。このサンプルでは使用していませんが、TStringStream というのもあります。
上のサンプルでは、2 MB のテキストの描画が 9.5 秒程度でしたが、3.7 秒程になりました。


図9
設計時画面
図10
実行結果例
  • 2 MB (30200 行) のファイルを表示
  • いずれの方法も大差ないと言える

リスト4
TStringList 等に全てを読み込んでから描画
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ExtDlgs;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    Panel2: TPanel;
    Memo1: TMemo;
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Edit1: TEdit;
    Edit2: TEdit;
    Edit3: TEdit;
    OpenTextFileDialog1: TOpenTextFileDialog;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
    InitDir : string;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses MMSystem;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.ScrollBars := ssBoth;
  Memo1.WordWrap   := False;

  Edit1.ReadOnly   := True;
  Edit2.ReadOnly   := True;
  Edit2.ReadOnly   := True;
end;

//=============================================================================
//  TMemoryStreamに全て読み込んで
//  TMemo.Lines.LoadFromStreamでそのストリームから読み込む
//  timeGetTimeの使用には、usesにMMSystemが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  StartTime : DWORD;
  EndTime   : DWORD;
  FileName  : String;
  AStream   : TMemoryStream;
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);


  StartTime := TimeGetTime;
  FileName  := OpenTextFileDialog1.FileName;

  Edit1.Text := '';
  Edit1.Update;
  Memo1.Lines.Clear;
  Memo1.Update;

  Memo1.Lines.BeginUpdate;
  AStream := TMemoryStream.Create;
  try
    AStream.LoadFromFile(FileName);
    Memo1.Lines.LoadFromStream(AStream);
  finally
    FreeAndNil(AStream);
  end;
  Memo1.Lines.EndUpdate;

  EndTime    := timeGetTime;
  Edit1.Text := IntToStr(EndTime - StartTime) + ' ms';
end;

//=============================================================================
//  TStringListに全て読み込んでTMemo.TextにTStringList.Textをセットする
//  timeGetTimeの使用には、usesにMMSystemが必要
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  StartTime : DWORD;
  EndTime   : DWORD;
  FileName  : String;
  SL        : TStringList;
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);


  StartTime := TimeGetTime;
  FileName  := OpenTextFileDialog1.FileName;

  Edit2.Text := '';
  Edit2.Update;
  Memo1.Lines.Clear;
  Memo1.Update;

  Memo1.Lines.BeginUpdate;
  SL := TStringList.Create;
  try
    SL.LoadFromFile(FileName);
    Memo1.Text := SL.Text;
  finally
    FreeAndNil(SL);
  end;
  Memo1.Lines.EndUpdate;

  EndTime    := timeGetTime;
  Edit2.Text := IntToStr(EndTime - StartTime) + ' ms';
end;

//=============================================================================
//  TStreamReaderを生成して、そのReadToEndでTMemo.Textにセット
//  Delphi 2010以降専用
//  timeGetTimeの使用には、usesにMMSystemが必要
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  StartTime : DWORD;
  EndTime   : DWORD;
  FileName  : String;
  SReader   : TStreamReader;
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);


  StartTime := TimeGetTime;
  FileName  := OpenTextFileDialog1.FileName;

  Edit3.Text := '';
  Edit3.Update;
  Memo1.Lines.Clear;
  Memo1.Update;

  Memo1.Lines.BeginUpdate;
  //ファイルの読み込み準備。文字コードはOSのデフォルト
  SReader := TStreamReader.Create(FileName, TEncoding.Default);
  SReader.OwnStream;
  try
    Memo1.Text := SReader.ReadToEnd;
  finally
    //SReader.OwnStream;を実行しない場合は、SReader.BaseStream.Free;も必要
    SReader.Close;
    SReader.Free;
  end;
  Memo1.Lines.EndUpdate;

  EndTime    := timeGetTime;
  Edit3.Text := IntToStr(EndTime - StartTime) + ' ms';
end;

end.




05_テキストを読み込むだけなら速いが表示には時間がかかる


ここで読み込みの速度を再確認してみます。
このサンプルは、TMemo.Lines プロパティと同じ TStrings 型の変数に、テキストファイルから読み込んで代入しています。100 MB のテキストファイルですが、1 秒程度しかかかっていません。
同じ TStrins でも、TMemo.Lines の TStrings は表示、描画を伴うため、非常に時間がかかることが分かります。

この TMemo.Lines にテキストを代入すると描画も制御してくれるという機能は便利です。描画コードを意識する必要が全くありません。それほど大量のテキストデータを扱わないのであれば、利用価値が十分にあると言えます。


図11
実行結果例
  • 100 MB (1510000 行) のテキストファイルを読み込んだ結果

リスト5
TStrings にテキストファイルを読み込むだけのテスト
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ExtDlgs;

type
  TForm1 = class(TForm)
    Button1: TButton;
    OpenTextFileDialog1: TOpenTextFileDialog;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
    InitDir : string;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses MMSystem;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Caption := 'TStrinsへの読み込み';
  InitDir := '';
end;

//=============================================================================
//  TStringsにテキストファイルのテキストを読み込む
//
//  timeGetTimeの使用には、usesにMMSystemが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  FileName   : String;
  StartTime  : DWORD;
  EndTime    : DWORD;
  AStrings   : TStrings;
  StrRes     : String;
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);


  StartTime := TimeGetTime;
  FileName  := OpenTextFileDialog1.FileName;

  AStrings := TStringList.Create;
  try
    AStrings.LoadFromFile(FileName);
  finally
    FreeAndNil(AStrings);
  end;

  EndTime := timeGetTime;
  StrRes  := IntToStr(EndTime - StartTime) + ' ms';
  Application.MessageBox(PChar(StrRes), '情報');
end;

end.




06_既存コンポーネントの利用


ここまでのサンプルでは数メガバイト程度のテキストファイルであれば、読み込む時間がそれほどはかかりません。しかし、テキストのサイズが大きくなると、全てのテキストデータが表示されるまで時間がかかるようになります。

この問題を回避するには、テキストを保持するオブジェクトと、描画を実行する部分を別にしない限り無理があると思われます。これは仮想リストビューと同じような動作仕様となります。事実、多くのエディタ関係のコンポーネントでは、TCustomControl からの派生クラスとして、TMemo や TRichEdit と同様の機能を実装しています。
したがって、大きなサイズのテキストファイルを扱うには、自前でそのような機能のコンポーネントを作成するか、既存のコンポーネントを利用することになるでしょう。

この種のコンポーネントは多くありますが、ネット上でたまに話題になるのは、本多 勝彦さん作の TEditor, SynEdit, TMSSoftware の VCL Memo (TAdvMemo) といったところでしょうか。
下の図は、これらのコンポーネントを実際に動作させたものです。

TEditor コンポは、TEditor ver2.52 Unicdoe Edition Rel1.6 を使用しました。
SynEdit は SourceForge からダウンロードした Unicode 版の中の、TSynMemo を使用しています。
フォント以外はデフォルトのままフォームに配置しています。テキストファイルは、100 MB (1510000 行 - 151 万行) を使用しました。計測値は、実際にスクロールやテキスト入力等の操作が可能となるまでの時間です。



図12
TEditor コンポーネント
  • heditor_252U.dpk をインストール
  • ScrollBars を ssBoth に設定
  • 100 MB のテキストファイルを表示
図13
TAdvMemo コンポーネント
  • setup.exe を実行してインストール
  • 試用版は Copyright が表示される
  • 100 MB のテキストファイルを表示
図14
TSynMemo コンポーネント
  • SynEdit_RXE.dpk をビルドして
  • SynEdit_DXE.dpk をインストール
  • インストールの際にトラブルが発生した場合は、参考リンクの記事を参照
  • 100 MB のテキストファイルを表示

TEditor コンポのテストに使用したコードです。他のコンポーネントも、コンポーネント名が違うだけで全て同じコードを使用しています。このコードの実行には、uses に MMSystem が必要です。


procedure TForm1.Button1Click(Sender: TObject);
var
  StartTime : DWORD;
  EndTime   : DWORD;
  FileName  : String;
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);

  StartTime := TimeGetTime;
  FileName  := OpenTextFileDialog1.FileName;

  Edit1.Text := '';
  Edit1.Update;

  Editor1.Lines.Clear;
  Editor1.Update;

  Editor1.Font.Name := 'MS ゴシック';
  Editor1.Font.Size := 9;
  Editor1.Lines.LoadFromFile(FileName);
  Caption := Editor1.ToString;

  EndTime    := timeGetTime;
  Edit1.Text := IntToStr(EndTime - StartTime) + ' ms';
end;




07_巨大テキストファイルを扱うためのヒント


32 ビットのアプリでは、100 MB 程度までのテキストファイルであれば、TMemo や TRichEdit に読み込んで表示できます。

メモリに格納できないような巨大テキストファイルは、分割して読み出すしか方法はありません。
この時、分割したテキストファイルの位置等を管理するための情報を、どこかに、例えばレコード型の配列等に記憶しておきます。スクロール等の操作で、必要になったら、その情報に基づいてファイルから読み出して表示することになるでしょう。
場合によっては、作業用、管理用のファイルが別に必要になるかも知れません。以下は、そのような場合に参考になるかも知れないテストコードです。

「巨大テキストファイル」の「巨大」がどの程度のサイズを意味するのかは分かりませんが、筆者自身は、巨大テキストファイルの中身を全て表示するという必要性は感じていません。
ギガバイト級のテキストファイルを扱うことはありますが、それらはシステム関係やアプリケーションのログファイルです。これらのログファイルの場合、必要な部分は決まっています。大抵は最終行からとなります。必要と思われる部分だけを取り出して表示、そこになければ、次の行からある程度の行数のテキストを表示して確認するという作業を行うことができます。

ログファイルのようなテキストファイルの場合、格納する形式、つまり文字列の書式が一定であれば、インデックス用のファイルを別に用意しておけば、後の検索には便利かも知れません。
あるいは、データベース化ということも考えられます。



[備考]
64 ビットのアプリは、1 つのプロセスが使用可能なメモリは、8 TB (8 テラバイト= 8,000 GB) です。したがって、事実上、TMemo 等に読み込めるデータのサイズは、システムで使用可能なメモリ量によって決まることになりますが、実際にシステムに搭載可能なメモリ量は、Windows のバージョンによって上限があります。





08_ストリームの指定位置から改行位置を知る


少し時間がかかってもいいのであれば、TextFile オブジェクトを使用して、指定行数目からのテキストを取り出すことができます。

テキストファイルの、指定バイト値の位置から、あるいは、指定行数目から、その位置にどんな意味があるかということは別問題として、次のように、指定した位置から改行文字を調べることによって、それ以降のデータを行の文字列として取得することができます。



図15
設計時画面
図16
実行画面例
  • 10 GB のテキストファイルでテスト

リスト6
ストリームの指定位置から改行位置を知る
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ExtDlgs;

type
  TForm1 = class(TForm)
    OpenTextFileDialog1: TOpenTextFileDialog;
    Panel1: TPanel;
    Button1: TButton;
    Button2: TButton;
    LabeledEdit1: TLabeledEdit;
    LabeledEdit2: TLabeledEdit;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
    InitDir : string;
    FileName : String;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Caption := '指定位置から改行文字を検索';

  LabeledEdit1.Text     := '';
  LabeledEdit2.ReadOnly := True;
  LabeledEdit2.Text     := '';

  FileName := '';
  Memo1.ScrollBars := ssBoth;
  Memo1.WordWrap   := False;
end;

//=============================================================================
//  対象のファイルの選択
//  そのファイルのTStreamReaderを生成して、ファイルのサイズを取得して表示
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  SReader : TStreamReader;
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);


  FileName := OpenTextFileDialog1.FileName;
  //TStreamReaderの生成。文字コードはOSのデフォルト
  SReader := TStreamReader.Create(FileName, TEncoding.Default);
  try
    SReader.OwnStream;

    //テキストファイルのサイズ
    LabeledEdit1.Text := IntToStr(SReader.BaseStream.Size);
  finally
    //SReader.OwnStream;を実行しない場合は、SReader.BaseStream.Free;も必要
    SReader.Close;
    FreeAndNil(SReader);
  end;

  Memo1.Lines.Clear;
  LabeledEdit1.SetFocus;
  LabeledEdit1.SelStart := Length(LabeledEdit1.Text);
end;

//=============================================================================
//  改行文字の次の行の先頭位置の検出
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  SReader  : TStreamReader;
  Ch       : Char;
  MaxLines : Integer;
  i        : Integer;
begin
  if FileName = '' then exit;

  //TStreamReaderの生成。文字コードはOSのデフォルト
  SReader := TStreamReader.Create(FileName, TEncoding.Default);
  try
    SReader.OwnStream;

    SReader.BaseStream.Position := StrToInt64Def(LabeledEdit1.Text, 0);

    Ch := #0;
    while (SReader.BaseStream.Read(Ch, 1) = 1) and (Ch <> #13) do begin
      Application.ProcessMessages;
    end;
    if Ch = #13 then begin
      //次に読み出したのが#10でなければ1つ戻す
      if (SReader.BaseStream.Read(Ch, 1) = 1) and (Ch <> #10) then begin
        SReader.BaseStream.Seek(-1, soCurrent);
      end;
    end;

    //BaseStream.Position-2が先頭位置の値
    //Edit1にその値を表示
    LabeledEdit2.Text := IntToStr(SReader.BaseStream.Position - 2);

    //検出した行からMaxLines分の文字列を取得して表示
    MaxLines := 5;
    Memo1.Lines.Clear;
    Memo1.Lines.BeginUpdate;
    for i := 0 to MaxLines - 1 do begin
      if SReader.EndOfStream then break;
      Memo1.Lines.Add(SReader.ReadLine);
    end;
    Memo1.Lines.EndUpdate;

  finally
    SReader.Close;
    FreeAndNil(SReader);
  end;

  LabeledEdit1.SetFocus;
  LabeledEdit1.SelStart := Length(LabeledEdit1.Text);
end;

end.




09_テキストファイルの最下行から指定行数分のテキストを取り出す


テキストファイルの最下行から、指定行数の文字列を取り出すテストです。
改行位置から前の行だけを取得するメソッドはありません。そこで、テキストファイル内を 1 文字づつファイルの先頭方向に、改行文字を検査して取得しています。
ストリームの位置をファイルの最後に移動 (seek) するだけなので、ファイルのサイズは関係ありませんが、折角 100 GB のテキストファイルを作成したので、そのファイルでテストしてみました。


図17
設計時画面
図18
実行結果例 1
  • 100 GB バイトのファイルでテスト
  • 最下行は 15 億 1000 万行目
  • 昇順に TMemo に表示
図19
実行結果例 2
  • 100 GB バイトのファイルでテスト
  • 最下行は 15 億 1000 万行目
  • 検索順の降順に TMemo に表示

リスト7
テキストファイルの最下行から指定行数を取り出す
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ExtDlgs;

type
  TForm1 = class(TForm)
    OpenTextFileDialog1: TOpenTextFileDialog;
    Memo1: TMemo;
    Panel1: TPanel;
    Button1: TButton;
    Button2: TButton;
    CheckBox1: TCheckBox;
    LabeledEdit1: TLabeledEdit;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
    InitDir    : string;
    FileName   : String;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Caption := 'ファイルの最下行から指定行数を取り出す';

  LabeledEdit1.NumbersOnly := True;
  LabeledEdit1.ImeMode     := imClose;
  LabeledEdit1.Text        := '3';

  FileName := '';
  Memo1.ScrollBars := ssBoth;
  Memo1.WordWrap   := False;

  Memo1.ScrollBars := ssBoth;
  Memo1.WordWrap   := False;
end;

//=============================================================================
//  対象のファイルの選択
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);


  FileName := OpenTextFileDialog1.FileName;

  Memo1.Lines.Clear;
  LabeledEdit1.SetFocus;
  LabeledEdit1.SelStart := Length(LabeledEdit1.Text);
end;

//=============================================================================
//  テキストファイルの最後から、指定行数だけを取り出す
//  改行文字をテキストファイルの最後から検索していく
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  SReader      : TStreamReader;
  iMaxLines    : Integer;
  iLineCount   : Int64;
  FilePosition : Int64;
  FileOffset   : Int64;
  i            : Integer;
  Ch           : AnsiChar;
begin
  if FileName = '' then exit;
  if StrToInt(LabeledEdit1.Text) = 0 then exit;

  //TStreamReaderの生成。文字コードはOSのデフォルト
  SReader := TStreamReader.Create(FileName, TEncoding.Default);
  try
    SReader.OwnStream;
    Application.ProcessMessages;

    Memo1.Lines.Clear;
    Memo1.Update;
    Memo1.Lines.BeginUpdate;

    Ch   := #0;
    iMaxLines  := StrToIntDef(LabeledEdit1.Text, 1);
    iLineCount := 0;
    //最初の検索位置(CRLFを除いた分から)
    FileOffset := SReader.BaseStream.Seek(-3, soEnd);

    while True do begin
      if (SReader.BaseStream.Read(Ch, 1) = 1) and (Ch = #13) then begin
        if (SReader.BaseStream.Read(Ch, 1) = 1) and (Ch = #10) then begin
          Inc(iLineCount);
          if CheckBox1.Checked then begin
            Memo1.Lines.Add(SReader.ReadLine);
            //バッファのクリア
            //これがないと前に実行したSReader.ReadlLineの次の行が取得されてしまう
            SReader.DiscardBufferedData;
          end;
          if iLineCount = iMaxLines then Break;
        end;
      end;

      //FileOffsetは検索位置
      //1つづつ前に移動して改行文字を検索していく
      FileOffset   := FileOffset - 1;
      FilePosition := SReader.BaseStream.Seek(FileOffset, soBeginning);

      //ファイルの先頭位置に来た時の処理
      if FilePosition <= 0 then begin
        if iLineCount < iMaxLines then begin
          iLineCount := iLineCount + 1;
          if CheckBox1.Checked then begin
            Memo1.Lines.Add(SReader.ReadLine);
          end;
        end;
        break;
      end;
    end;

    //iLineCount行分のテキストを表示
    if not CheckBox1.Checked then begin
      for i := 0 to iLineCount - 1 do begin
        Memo1.Lines.Add(SReader.ReadLine);
      end;
    end;
    Memo1.Lines.EndUpdate;

  finally
    SReader.Close;
    FreeAndNil(SReader);
  end;

  LabeledEdit1.SetFocus;
  LabeledEdit1.SelStart := Length(LabeledEdit1.Text);
end;

end.




10_改行文字の扱いと sLineBreak 定数


上の 2 つのサンプルでは、#13 (CR) の次が #10 (LF) であった場合に改行文字と判定しています。
下図のように、ShowMessage 等のダイアログでは #13 だけでも改行しますが、TMemo や TRichEdit 等のコントロールにテキストを表示する場合は改行しません。

改行文字は、AdjustLineBreaks 関数で制御できます。



図20
#13 だけでは改行しない
  • ダイアログ関係では改行表示する

procedure TForm1.Button2Click(Sender: TObject);
var
  S : String;
begin
  S := '改行文字による' + #13 + '複数行の表示';
  Memo1.Text := S;
  Application.MessageBox(PChar(S), '結果');
end;


また、#10#13 でも改行しません。これも ShowMessage 等のダイアログでは改行表示します。


図21
#10#13 では改行しない
  • ダイアログ関係では改行表示する


procedure TForm1.Button2Click(Sender: TObject);
var
  S : String;
begin
  S := '改行文字による' + #10#13 + '複数行の表示';
  Memo1.Text := S;
  Application.MessageBox(PChar(S), '結果');
end;


改行文字には、sLineBreak 定数を使用すると便利です。sLineBreak の System.pas 内での定義は次のようになっています。この定義文は、Delphi 2010, XE の場合です。Delhpi 2009 以前には MACOS 部分の定義はありません。Delphi 5 以前には sLineBreak 定数の定義はありません。


const
   sLineBreak = {$IFDEF LINUX} AnsiChar(#10) {$ENDIF}
      {$IFDEF MSWINDOWS} AnsiString(#13#10) {$ENDIF}
      {$IFDEF MACOS} AnsiChar(#10) {$ENDIF};


Delphi XE2, XE3, XE4, XE5 での改行文字の定義は次のようになっています。


const
   sLineBreak = {$IFDEF POSIX}} AnsiChar(#10) {$ENDIF}
      {$IFDEF MSWINDOWS} AnsiString(#13#10) {$ENDIF}


改行文字ではありませんが、SysUtils.pas 内で、パスやドライブ区切りの文字定数が次のように定義されています。PathSep は環境文字列のセパレータ等に使われている文字です。
この定義文は、Delphi 6, 7, 2007, 2009, 2010, XE, XE2, XE3, XE4, XE5 のものです。


const
  PathDelim  = {$IFDEF MSWINDOWS} '\'; {$ELSE} '/'; {$ENDIF}
  DriveDelim = {$IFDEF MSWINDOWS} ':'; {$ELSE} '';  {$ENDIF}
  PathSep    = {$IFDEF MSWINDOWS} ';'; {$ELSE} ':'; {$ENDIF}