Delphi Starter Edition でメモ帳クローンを作る (後編)

# // 続き

これは [Delphi Advent Calendar 2017](https://qiita.com/advent-calendar/2017/delphi) 8 日目の記事です。[前編はこちら](https://qiita.com/ht_deko/private/bd854e8a22cf31b636b0)。


# コードを記述するよ！

## プログラムの構造

実際にコードを記述する前に、ソースコードのファイル構成を説明しておきます。Pascal の文法で記述できるファイルは基本的には 2 つあります。**プロジェクトファイル**である **.dpr** と **ユニットファイル** である **.pas** です。

「あれ？」と思われた方もいらっしゃるでしょう。最初にプロジェクトを保存した時のファイルは **.dproj** でしたよね？.dpr は古い Delphi ではプロジェクトファイルも兼ねていましたが、実際の所は**プログラムのファイル**です。.dproj は比較的新しい Delphi のプロジェクトファイルで、コンパイルオプション等を保持している XML ファイルです。C 言語でいう所の makefile に近いかもしれません。

実際の所 .dproj を削除しても .dpr を開けば .dproj は自動生成されます。但し、バージョン情報など、コンパイル用のオプションはすべて消えてしまいます。

では .dpr の中身を確認してみましょう。オブジェクトインスペクタで notepad_clone.exe となっている部分を右クリックして **[ソースの表示 (V)]** を選んでみます。 

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/a2efed6f-10a3-0718-d604-2aa5d29788b7.png)

エディタのタブが一つ増え、notepad_clone.dpr が開かれました。これがプログラムファイルです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/23a23e0a-f602-d904-3f38-6c0a4934e10f.png)

プログラムファイルは

```pascal:
program プログラム名;

uses
  // ユニットの依存関係のリスト

begin

end.
```

の様式になっています。begin と end の中にコードを記述します。

原始的な Pascal はソースコードの分割機能がなく、「Pascal はソースコードを分割できない」と揶揄する人もいますが、少なくとも Object Pascal においては正しくない発言です。Object Pascal では .dpr にすべてのコードを記述する事もできますが、基本的には機能毎にユニットに分割してプログラムを構成します。

ここで一旦〔Shift〕+〔Ctrl〕+〔S〕を押し、プロジェクトを保存してください。[ファイル | すべて保存] でも構いません。〔Ctrl〕+〔S〕での保存は現在のタブを保存する事を意味し、〔Shift〕+〔Ctrl〕+〔S〕はプロジェクトファイル中の編集したすべてのタブを保存する事を意味します。

[ファイル | 新規作成 | その他] を選びます。

![notepad_053.png](https://qiita-image-store.s3.amazonaws.com/0/21785/a069dea9-978c-f076-03c4-c70cccfa8787.png)

[Delphi プロジェクト] で**コンソールアプリケーション**を選び、[OK] ボタンを押します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/2134210a-4193-e268-0a9e-138500fe9899.png)

すると以下のようなコードが表示されると思います。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/b93035ed-39a5-aeec-d03a-4850d7229050.png)

プロジェクトマネージャで確認してもユニットファイルはありません。コンソールアプリケーションの場合、初期状態では単一のファイルでプログラムが構成されている事がわかります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/d1c85e7e-046b-c21c-b247-7208a19a1548.png)

さて、notepad_clone に戻りましょう。notepad_clone のプロジェクトを再び開くには、[ファイル | プロジェクトを開く...] または、[開きなおす] から開きます。コンソールアプリケーションを保存するか尋ねられますが、[いいえ] を押して破棄してください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/12b0ff34-beb4-8c76-9123-299e52fd10d6.png)

改めてユニットファイルを確認してみましょう。frmuMain のタブを選択し、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/5ef690ee-eaeb-3bfd-813b-0deddf05966b.png)

フォームデザイナが表示されていれば〔F12〕を押してコードエディタに切り替えます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/b20e2786-0bfe-a147-5430-c8b04ca40799.png)


ユニットファイルは

```pascal:
unit ユニット名;

interface

uses
  // ユニットの依存関係のリスト

// クラス、手続き、関数の宣言部


implementation

uses
  // ユニットの依存関係のリスト

// クラスのメソッド、手続き、関数の実装部


initialization

// ユニット初期化コード

finalization

// ユニット終了コード

end.
```

の様式になっています。省略できるセクションがあるため、違うように見えるかもしれません。

フォームのあるプログラム...つまり、今作成している **VCL フォームアプリケーション** では、フォームはユニットとして管理されます。ユニットは .pas で、フォームの定義は .dfm (**フォームファイル**) に保存されます。フォームファイルは独自のテキストファイルで、フォームデザイナの状態で〔Alt〕+〔F12〕を押すか、右クリックして [エディタで表示] を選ぶとエディタで編集できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/13c11efe-1bad-b888-29d4-ed45272737fe.png)

下手にいじるとフォームが壊れてしまうので、慣れてくるまでは手動で編集しない方がいいと思います。この状態でさらに〔Alt〕+〔F12〕を押すか、右クリックして [フォームとして表示] を選ぶとフォームデザイナに戻ります。

今度は [プロジェクト | オプション] を開き、[フォーム] を確認してみましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/b489678d-b9c0-dc6b-5827-f0c8406ec6f8.png)

このプロジェクト (プログラム) にはフォームが一つだけあり、これが**メインフォーム**として自動生成されている事が分かります。複数のフォームがあった場合、最初にすべてのフォームを自動生成してもいいのですが、メモリを消費してしまいます。最初からフォームを用意しておく必要がない場合には [使用可能フォーム] の方に移動しておき、使う時だけインスタンスを生成する方法を採ります。 

メインフォームの生成ロジックは .dpr にありましたよね。

```pascal:notepad_clone.dpr
program notepad_clone;

uses
  Vcl.Forms,
  frmuMain in 'frmuMain.pas' {Form1}; // frmuMain をこのプログラムで使用する

{$R *.res}

begin
  Application.Initialize;                // VCL フォームアプリケーションを初期化する
  Application.MainFormOnTaskbar := True; // オマジナイ
  Application.CreateForm(TForm1, Form1); // TForm 型の Form1 を生成する
  Application.Run;                       // VCL フォームアプリケーションを実行する
end.
```

 - **uses** 句では使用するユニットが列挙されています。
 - Form1 という TForm1 型の変数は frmuMain.pas 内で定義されています。

```pascal:frmuMain.pas
unit frmuMain;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, System.Actions,
  Vcl.ActnList, Vcl.Menus;

type
  TForm1 = class(TForm)
    (中略)
  end;

var
  Form1: TForm1; // <- コレ

implementation

{$R *.dfm}

end.
```

 - var は変数の宣言ブロックです。**interface** セクションにあるので、この変数は **uses** したプログラムまたはユニットから見る事ができます。**implementation** セクションに var ブロックを置くとここに置かれた変数は implementation セクション内のクラスのメソッド、手続き、関数からしか見えません。

VCL フォームアプリケーションでは、メインフォームが閉じられるとアプリケーションも終了するようになっています。なんとなく Delphi プロジェクトの構造が解ってきたでしょうか？

 - [プログラムとユニット（Delphi）(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%A8%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%EF%BC%88Delphi%EF%BC%89)

## コードを記述
ではコードを記述してみます。メモ帳の [ファイル (F)] のメニューで一番簡単な処理は [メモ帳の終了(X)] でしょう。アクションリストのコンポーネントエディタにある acExit をダブルクリックするか、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/3c3b0a96-7a5e-c275-6da4-e3f42dceee23.png)

オブジェクトインスペクタで acExit を選択し、[イベント] タブをクリック、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/c2453a09-5f89-892d-79fa-0b18746ce937.png)

OnExecute イベントの値部分をダブルクリックします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/2978530d-fcb3-2a59-dd5a-488790df27e5.png)

そうすると、ソースコードに**イベントハンドラ**用のコードが追加されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/17a305e8-b955-c16b-02f9-6e463ef0f1a0.png)

acExit の onExcute はメニューアイテムが選択された時に実行される**イベント**です。このイベントを処理するメソッドが**イベントハンドラ** acExitExecute() です。イベントハンドラの名前は自動で付けられますが、オブジェクトインスペクタから名前を変更する事も出来ます。

このイベントハンドラに何を書けばアプリケーションを終了させられるでしょうか？このフォームはメインフォームですから...そう、**メインフォームを閉じればアプリケーションを終了させる事ができます**。

TForm1 の自身を示す Self を用いて、Self.Close; と記述するか、

```pascal:
procedure TForm1.acExitExecute(Sender: TObject);
begin
  Self.Close;
end;
```

或いは単に Close; と書けばフォームは閉じます。

```pascal:
procedure TForm1.acExitExecute(Sender: TObject);
begin
  Close;
end;
```

Close メソッドは TForm1 が持つフォームを閉じるメソッドです。Delphi では引数のないメソッドの括弧を省略する事ができます。もちろん、Close(); と書いても構いません。

〔F9〕でコンパイルして実行してみましょう。今度は [メモ帳の終了(X)] がグレーアウトしていませんね。[メモ帳の終了(X)] を選択すると、メモ帳クローンが終了します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/3c963fbb-6d9d-a92b-433f-8a778f776f7d.png)

〔Alt〕,〔F〕,〔X〕と順に押しても [メモ帳の終了(X)] が実行される事を確認してください。

 - [イベント（Delphi）(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88)

コードエディタのショートカットも覚えておくといいでしょう。コーディングの効率が上がります。

 - [デフォルトのキーボード ショートカット (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E3%81%AE%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89_%E3%82%B7%E3%83%A7%E3%83%BC%E3%83%88%E3%82%AB%E3%83%83%E3%83%88)


## [開く(O)...] の実装
[開く(O)...] を実装してみます。実際にメモ帳で [開く(O)...] をやってみるとファイルオープンダイアログが表示され、ここでファイルを選ぶとそれが開かれるという処理です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/434fa650-17cc-a5ff-4b79-c751897afae5.png)

ところが...です。メモ帳のファイルオープンダイアログは特殊なもので、下に [文字コード(E)] というコンボボックスがあります。これと全く同じダイアログはありませんので TOpenTextFileDialog を使うことにします。TOpenTextFileDialog はツールパレットの [Dialogs] タブの中にあります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/36d5829b-64e3-8908-6e0d-51b05d05118d.png)

コンポーネント名は判っていてもどのタブにあるのか分らない場合には、ツールパレット上部の虫眼鏡アイコンのある検索ボックスに数文字入力するとコンポーネントを絞り込む事ができます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/14993abf-9336-be31-6048-38e7fa800fd7.png)

フォームデザイナに OpenTextFileDialog1 が貼り付けられました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/95cc20b5-eb51-0d3d-b504-ce24a522ecca.png)

この OpenTextFileDialog1 をダブルクリックすると実際のダイアログがプレビューできます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/d55fab3b-7c84-c9b0-5416-c260724ac746.png)

メモ帳のダイアログとはちょっと異なりますが、なんとかなりそうです。

### フィルタ
[ファイルの種類] をドロップダウンしても空白のままですが、実際のメモ帳には **テキスト文章(\*.txt)** と **すべてのファイル (\*.\*)** があります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/7fe946e6-9a36-6c1e-45dc-cd8eb6be2fcd.png)

これを指定するにはオブジェクトインスペクタで OpenTextFileDialog1 の Filter プロパティを設定します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/4f454e28-789c-43bd-1630-e50eb5f5874d.png)

Filter の値部分をダブルクリックするか、右側の […] ボタンを押すと、Filter プロパティのプロパティエディタが起動します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/8c559478-c501-96d2-e0d2-c7c74460dff9.png)

左の列にフィルタ名を、右の列にフィルタを入力します。具体的には以下のように入力します。

|フィルタ名|フィルタ|
|:-----------------|:------------------|
|テキスト文章 (\*.txt)|\*.txt|
|すべてのファイル (\*.\*)|\*.\*|

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/ae1e04c7-04c8-bada-40bf-31b8242a14f9.png)

[OK] ボタンを押して確定し、OpenTextFileDialog1 をダブルクリックしてダイアログをプレビューするとフィルターが指定されています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/fa6b2c27-88ba-1d0e-e3a3-a07fe1d9177d.png)

選択したフィルタによって FilterIndex プロパティの値が変化します。値はフィルタの順番です (0 オリジン)。

### エンコーディング (文字コード)
エンコーディングはイロイロと入っていますが...

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/1fd0fabd-3d0d-027e-042a-55d83a1d7ecf.png)

オリジナルのメモ帳はこんな感じです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/b5affd1c-c13b-4493-d240-a950fad60ddc.png)

メモ帳と同じにするには Encodings プロパティを編集します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/17ceb3b3-0317-2e06-cbb2-a60b614a6cf9.png)

Encodings の値部分をダブルクリックするか、右側の […] ボタンを押すと、Encodings プロパティのプロパティエディタが起動します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/c5b45cd1-1a57-fd21-8c85-aec68d3cb2ce.png)

これを以下のように書き換えます。

```text:
ANSI
Unicode
Unicode big endian
UTF-8
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/73d9e44c-c56a-38c9-1e6e-9fcbde193cbe.png)

[OK] ボタンを押して確定し、OpenTextFileDialog1 をダブルクリックしてダイアログをプレビューするとエンコーディングが指定されています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/8ba782f8-1b69-a836-b343-fc387819a293.png)

選択したエンコーディングによって EncodingIndex プロパティの値が変化します。値はエンコーディングの順番です (0 オリジン)。

### コード
コードは acOpen の onExecute イベントのイベントハンドラに書きます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/74fd16de-d801-4841-330c-03c33e2f4346.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/2d5b4051-7d79-7a4f-cfc5-cfe8232d5cd4.png)

ここに書くべき処理は以下の通りです。

1. OpenTextFileDialog1 を開く
2. ファイルが選択されて [OK] ボタンが押されたら (有効なファイル名が渡されたら)
3. ファイルを指定されたエンコーディングで Memo1 に読み込み

コーディングにフィルターは何も影響しませんが、エンコーディングは影響します。エンコーディングの **ANSI** とはデフォルトの ANISI コードページ (ACP) の事です。日本語版 Windows の場合、デフォルトの ANSI コードページは 932 (SHIFT_JIS) となります。

OpenTextFileDialog1 を開いて [OK] ボタンが押されて有効なファイル名が渡されたかどうかを判定するには以下のようなコードになります。

```pascal:frmuMain.pas
procedure TForm1.acOpenExecute(Sender: TObject);
begin
  if OpenTextFileDialog1.Execute then
    begin

    end;
end;
```
OpenTextFileDialog1 でダイアログを開くメソッドは Execute() で、[OK] ボタンが押されて有効なファイル名が渡されると戻り値として True を返します。ファイル名は FileName プロパティに格納されます。

Delphi の構造化文に関しては以下のドキュメントを参照してください。

 - [宣言と文（Delphi）(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E5%AE%A3%E8%A8%80%E3%81%A8%E6%96%87)
 - [改めて Delphi の構造化文 (VCL によるテクニック＆アルゴリズム)](http://ht-deko.com/tech058.html)

ファイルの読み込みですが、TMemo の Lines プロパティ (TString 型) には LoadFromFile() というファイルを読み込むメソッドがあります。

```pascal:
procedure LoadFromFile(const FileName: string); overload; virtual;
procedure LoadFromFile(const FileName: string; Encoding: TEncoding); overload; virtual;
```
 - [System.Classes.TStrings.LoadFromFile (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Classes.TStrings.LoadFromFile)

二番目の構文を使うとエンコーディングを指定して読み込めます。具体的には以下のようなコードになります。

```pascal:frmuMain.pas
procedure TForm1.acOpenExecute(Sender: TObject);
var
  Enc: TEncoding;
begin
  if OpenTextFileDialog1.Execute then
    begin
      // EncodingIndex によりエンコーディングを指定
      case OpenTextFileDialog1.EncodingIndex of
        1: Enc := TEncoding.Unicode;
        2: Enc := TEncoding.BigEndianUnicode;
        3: Enc := TEncoding.UTF8;
      else
        Enc := TEncoding.Default; // 0: ANSI
      end;
      // エンコーディングを指定して読み込み
      Memo1.Lines.LoadFromFile(OpenTextFileDialog1.FileName, Enc);
    end;
end;
```
ファイル保存の事を考えると、FileName と EncodingIndex はどこかに保存しておくと便利そうです。クラスの private な**フィールド**として定義しておきましょう。

```pascal:frmuMain.pas
(前略)
    N3: TMenuItem;
    OpenTextFileDialog1: TOpenTextFileDialog;
    procedure acExitExecute(Sender: TObject);
    procedure acOpenExecute(Sender: TObject);
  private
    { Private 宣言 }
    FFileName: String;       // 追加
    FEncodingIndex: Integer; // 追加
  public
    { Public 宣言 }
  end;
(後略)
```
OpenTextFileDialog1 で正しくファイルを選択された時のみ値を保存しておくことにします。

```pascal:frmuMain.pas
procedure TForm1.acOpenExecute(Sender: TObject);
var
  Enc: TEncoding;
begin
  if OpenTextFileDialog1.Execute then
    begin
      // 値を保存
      FFileName := OpenTextFileDialog1.FileName;
      FEncodingIndex := OpenTextFileDialog1.EncodingIndex;
      // EncodingIndex によりエンコーディングを指定
      case FEncodingIndex of                    // <- 変更
        1: Enc := TEncoding.Unicode;
        2: Enc := TEncoding.BigEndianUnicode;
        3: Enc := TEncoding.UTF8;
      else
        Enc := TEncoding.Default; // 0: ANSI
      end;
      // エンコーディングを指定して読み込み
      Memo1.Lines.LoadFromFile(FFileName, Enc); // <- 変更
    end;
end;
```
このままでは初期値が不定なので、フィールド (FFileName / FEncodingIndex) の値を初期値で初期化しておきます。初期化はフォームの作成時のイベント (onCreate) で行う事にしますので、オブジェクトインスペクタで Form1 を選び、[イベント] タブで onCreate の値の部分をダブルクリックしてイベントハンドラを作成します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/828e84fe-047d-d048-d762-0c0f2d3db12f.png)

FormCreate イベントハンドラでフィールドの値を初期化します。

```pascal:frmuMain.pas
procedure TForm1.FormCreate(Sender: TObject);
begin
  FFileName := '';
  FEncodingIndex := 0;
end;
```
オリジナルのメモ帳は "ファイル名 - メモ帳" という形式でキャプションバーにファイルを表示するようになっており、ファイル名が決定していない場合には "無題" と表示されます。このキャプションを更新するメソッドを実装しましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/97c0148e-cdcc-17a5-cf41-de527af38f08.png)

メソッドは値を返す必要がないため**手続き (Procedure)** で実装します。値を返す必要がある場合は**関数 (Function)** で実装します。

引数もありませんので、宣言は以下のようになります。

```pascal:frmuMain.pas
(前略)
  private
    { Private 宣言 }
    FFileName: String;       
    FEncodingIndex: Integer; 
    procedure UpdateCaption; // 追加
  public
    { Public 宣言 }
  end;
(後略)
```

Delphi (Pascal) では戻り値の有無で手続きと関数が明示的に区別されています。文脈上、手続きと関数を区別しないでいい場所ではまとめて**ルーチン**と呼ばれます。クラスのメンバーなルーチンは**メソッド**と呼ばれますが、面倒くさいので全部ひっくるめてメソッドと呼ぶ事が多いです。

 - [手続きと関数（Delphi）(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E6%89%8B%E7%B6%9A%E3%81%8D%E3%81%A8%E9%96%A2%E6%95%B0%EF%BC%88Delphi%EF%BC%89)
 - [メソッド（Delphi）(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)

実装部は以下のように記述します。**implementation** セクションにならどこに書いても構いませんが、最後 (end. の前) に追加してみます。 

```pascal:frmuMain.pas
(前略)
implementation

(中略)

procedure TForm1.FormCreate(Sender: TObject);
begin
  // 値を初期化
  FFileName := '';
  FEncodingIndex := 0;
end;

procedure TForm1.UpdateCaption;
begin

end;

end.
```

1. ファイル名が空だったら "無題"
2. ファイル名が空でなかったらファイル名部分のみを取得 (ディレクトリ部分は無視)。
3. "ファイル名  - メモ帳クローン" というキャプションにする。

このロジックのために必要なコードの変更点は以下の通りです。

```pascal:frmuMain.pas
unit frmuMain;

interface

uses
  ..., System.IOUtils; // 末尾に System.IOUtils を追加

(中略)

implementation

(中略)

procedure TForm1.acOpenExecute(Sender: TObject);
var
  Enc: TEncoding;
begin
  if OpenTextFileDialog1.Execute then
    begin
      // 値を保存
      FFileName := OpenTextFileDialog1.FileName;
      FEncodingIndex := OpenTextFileDialog1.EncodingIndex;
      // キャプションを変更
      UpdateCaption; // <-------------------------------------------------------追加
      // EncodingIndex によりエンコーディングを指定
      case FEncodingIndex of
        1: Enc := TEncoding.Unicode;
        2: Enc := TEncoding.BigEndianUnicode;
        3: Enc := TEncoding.UTF8;
      else
        Enc := TEncoding.Default;
      end;
      // エンコーディングを指定して読み込み
      Memo1.Lines.LoadFromFile(FFileName, Enc);
    end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
 // 値を初期化
  FFileName := '';
  FEncodingIndex := 0;
  Memo1.Lines.Clear; // Memo1 の中身を消去する。
  // キャプションを変更
  UpdateCaption; // <-------------------------------------------------------追加
end;

procedure TForm1.UpdateCaption;
var
  Dmy: String;
begin
  if FFileName = '' then
    Dmy := '無題'
  else
    Dmy := TPath.GetFileName(FFileName);
  Self.Caption := Dmy + ' - メモ帳クローン';
end;

end.
```
TPath.GetFileName はフルパスファイル名からファイル名だけを取り出すメソッドです。TPath は**構造体 (record)** ですが、パス名を操作するメソッドを備えています。TPath は System.IOUtils **名前空間**で定義されており、利用するには uses に名前空間を追加する必要があります。

 - [Delphi での名前空間の使い方 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/Delphi_%E3%81%A7%E3%81%AE%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)
 - [System 名前空間 (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System)
 - [Vcl 名前空間 (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/Vcl)

TPath にどんなメンバーがあるのかは、TPath. と入力してそのままちょっと待ってみましょう。**コード補完**機能により、プロパティ / メソッド / イベントの一覧が表示されます。 その後、項目を選択して Enter キーを押すかマウスでクリックするとコードに追加できます。一覧を強制的に表示するには 〔Ctrl〕+〔Space〕を押します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/95f5aca7-3464-5e8c-e09e-faa095b1dd0e.png)

また、TPath という単語にカーソルを置いて〔F1〕キーを押すとヘルプを開く事ができます。ここで [メソッド] をクリックすると...

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/7ee42bd0-ef82-e18e-92fb-5e5f7998a7da.png)

TPath が持つメソッドの一覧が表示されます。メソッド名をクリックするとそのメソッドの詳細が表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/345cbbf5-7b4f-302b-544a-a232180740cc.png)


さて、話はそれましたが〔F9〕を押してメモ帳クローンを実行し、[ファイル | 開く(O)...] で任意のファイルを開いてみましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/676595d9-9eda-fb51-3de4-56cd0d75806f.png)

マウスホイールでスクロールできない？それはスクロールバーを表示していないからです。Memo1 の ScrollBars プロパティを ssBoth に設定しましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/3ddcfd25-4e58-d82c-f7f7-6d2629954bc9.png)

今度はマウスホイールでスクロールできるはずです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/43fa6257-30ef-d75b-d56c-4d6a5a92fe11.png)

〔Alt〕,〔F〕,〔O〕と順に押しても [開く(O)\.\.\.] が実行される事を確認してください。〔Ctrl〕+〔O〕でも実行されます。

## [上書き保存(S)] の実装
上書き保存の実装は簡単です。

### コード
TMemo の Lines プロパティ (TString 型) には SaveToFile() というファイルを保存するメソッドがあります。

```pascal:
procedure SaveToFile(const FileName: string); overload; virtual;
procedure SaveToFile(const FileName: string; Encoding: TEncoding); overload; virtual;
```
 - [System.Classes.TStrings.SaveToFile (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Classes.TStrings.SaveToFile)


二番目の構文を使うとエンコーディングを指定して保存できますので、acSave アクションの onExecute イベントに対応する acSaveExecute イベントハンドラに処理を記述します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/8ef317ae-4066-c987-4689-81a16058f57d.png)

```pascal:frmuMain.pas
procedure TForm1.acSaveExecute(Sender: TObject);
var
  Enc: TEncoding;
begin
  // EncodingIndex によりエンコーディングを指定
  case FEncodingIndex of
    1: Enc := TEncoding.Unicode;
    2: Enc := TEncoding.BigEndianUnicode;
    3: Enc := TEncoding.UTF8;
  else
    Enc := TEncoding.Default;
  end;
  // エンコーディングを指定して保存
  Memo1.Lines.SaveToFile(FFileName, Enc);
end;
```
しかしながらこのロジックでは新規作成時であっても...つまりファイル名が未定 (空) であってもファイルを保存しようとしてしまいます。これではエラーになってしまうでしょう。よって、FFileName が空かどうかを判定し、空なら [名前を付けて保存(A)...] のロジックに飛ばすようにしましょう。[名前を付けて保存(A)...] はまだ実装していないので、とりあえず何もしない事にします。

```pascal:frmuMain.pas
procedure TForm1.acSaveExecute(Sender: TObject);
var
  Enc: TEncoding;
begin
  if (FFileName = '') then
    begin
      // 名前を付けて保存 (未実装)
    end
  else
    begin
      // EncodingIndex によりエンコーディングを指定
      case FEncodingIndex of
        1: Enc := TEncoding.Unicode;
        2: Enc := TEncoding.BigEndianUnicode;
        3: Enc := TEncoding.UTF8;
      else
        Enc := TEncoding.Default;
      end;
      // エンコーディングを指定して保存
      Memo1.Lines.SaveToFile(FFileName, Enc);
    end;
end;
```

ついでにこのファイル保存ロジックはメソッドにしてしまいましょう。

```pascal:frmuMain.pas
(前略)
  private
    { Private 宣言 }
    FFileName: String;
    FEncodingIndex: Integer;
    procedure SaveFile;        // <- 追加
    procedure UpdateCaption;
  public
    { Public 宣言 }
  end;

(中略)

procedure TForm1.acSaveExecute(Sender: TObject);
begin
  if (FFileName = '') then
    begin
      // 名前を付けて保存 (未実装)
    end
  else
    begin
      // 上書き保存
      SaveFile;
    end;
end;

procedure TForm1.SaveFile;
var
  Enc: TEncoding;
begin
  // EncodingIndex によりエンコーディングを指定
  case FEncodingIndex of
    1: Enc := TEncoding.Unicode;
    2: Enc := TEncoding.BigEndianUnicode;
    3: Enc := TEncoding.UTF8;
  else
    Enc := TEncoding.Default;
  end;
  // エンコーディングを指定して保存
  Memo1.Lines.SaveToFile(FFileName, Enc);
end;
(後略)
```
〔Alt〕,〔F〕,〔S〕と順に押しても [上書き保存(S)] が実行される事を確認してください。〔Ctrl〕+〔S〕でも実行されます。


## [名前を付けて保存(A)...] の実装
[開く(O)] の逆です。ファイル保存ダイアログを出し、指定されたファイル名でファイルを保存します。ファイル保存ダイアログ用のコンポーネントとして TSaveTextFileDialog を使います。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/cde2cd82-cc3b-bfb8-e969-4bba229f5069.png)

ツールパレットで絞り込んで探し、フォームデザイナに貼っておきます。

### フィルターとエンコーディング

こちらも TOpenTextFileDialog と同様にフィルターとエンコーディングを処理しておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/e2825c40-3a62-db8d-a91e-3df22ec6cff3.png)

手順は TOpenTextFileDialog と同じなので省略します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/b589758f-4bbd-f524-41bc-0d71fb4e28db.png)

TSaveTextFileDialog では DefaultExt プロパティに txt を指定しておきます。これはファイル名として拡張子を指定しなかった時に追加される拡張子です。例えば "ABC" という名前で保存すると、"ABC.txt" という具合に拡張子を自動で追加します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/789d6454-6d6c-8812-e491-1286905e9346.png)

そして Options プロパティの ofOverwritePrompt フラグを True にします。これはファイルが既に存在する時に上書き確認のダイアログを出してくれるようになるフラグです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/377d6eaf-cb97-70f3-1101-723fc63b3e59.png)

### コード
acSaveAs アクションの onExecute イベントに対応する acSaveAsExecute イベントハンドラに処理を記述します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/b3ec3c12-2c8b-f451-d358-8dfb61abe35e.png)

先程、[上書き保存(S)] の実装の時にファイルを保存するメソッドは作ったので、ここで行う処理は...

1. SaveTextFileDialog1 を開く
2. ファイルが選択されて [OK] ボタンが押されたら (有効なファイル名が渡されたら)
3. Memo1 の内容を指定されたエンコーディングでファイルに保存

このようになります。

```pascal:frmuMain.pas
procedure TForm1.acSaveAsExecute(Sender: TObject);
begin
  if SaveTextFileDialog1.Execute then
    begin
      // 値を保存
      FFileName := SaveTextFileDialog1.FileName;
      FEncodingIndex := SaveTextFileDialog1.EncodingIndex;
      // キャプションを変更
      UpdateCaption;
      // ファイルへ保存
      SaveFile;
    end;
end;

procedure TForm1.acSaveExecute(Sender: TObject);
begin
  if (FFileName = '') then
    begin
      // 名前を付けて保存
      acSaveAs.Execute;          // <- 追加
    end
  else
    begin
      // 上書き保存
      SaveFile;
    end;
end;
```
アクション acSaveAs は、Execute() メソッドで実行する事ができます。実行すると OnExecute イベントのイベントハンドラが実行されます。

〔Alt〕,〔F〕,〔A〕と順に押しても [名前を付けて保存(A)...] が実行される事を確認してください。

## [新規(N)] の実装
FormCreate イベントハンドラ内の初期化処理を実行すればよさそうです。

### コード
acNew アクションの onExecute イベントに対応する acNewExecute イベントハンドラに処理を記述します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/81dcdfc7-775a-f335-e643-c75d1648bb2f.png)

FormCreate での処理を Init() メソッドとして外に出し、FormCreate と acNewExecute イベントハンドラ内でそれぞれ実行します。

```pascal:frmuMain.pas
(前略)
  private
    { Private 宣言 }
    FFileName: String;
    FEncodingIndex: Integer;
    procedure Init;                  // <- 追加
    procedure SaveFile;
    procedure UpdateCaption;
  public
    { Public 宣言 }
  end;

(中略)

procedure TForm1.FormCreate(Sender: TObject);
begin
  Init;
end;

procedure TForm1.acNewExecute(Sender: TObject);
begin
  Init;
end;

procedure TForm1.Init;
begin
 // 値を初期化
  FFileName := '';
  FEncodingIndex := 0;
  Memo1.Lines.Clear; // Memo1 の中身を消去する。
  // キャプションを変更
  UpdateCaption;
end;

(後略)
```
〔Alt〕,〔F〕,〔N〕と順に押しても [新規(N)] が実行される事を確認してください。

#  おわりに } end.
いかがでしたか？Delphi でメモ帳クローンを作るのは難しくないという事がおわかり頂けたかと思います。もちろん、実装していない機能はまだまだあり、メモ帳は見た目ほど簡単なアプリケーションではないという事も認識できたかと思います。

テキストエディタとしては最低限の機能しか実装していないので、残りの部分については次の記事 (来週です！) でやろうと思っております。今回の分の全ソースコードを掲載しておきますので、興味がある方は残りの部分を実装してみてください。

ソースコード中の TMenuItem の定義部分、F1 とか N1 とかは IDE が自動で付けた名前ですので、名前が異なるかもしれませんが気にしないでください。

```pascal:frmuMain.pas
unit frmuMain;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, System.Actions,
  Vcl.ActnList, Vcl.Menus, Vcl.ExtDlgs, System.IOUtils;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    MainMenu1: TMainMenu;
    ActionList1: TActionList;
    acNew: TAction;
    acOpen: TAction;
    acSave: TAction;
    acSaveAs: TAction;
    acPageSetup: TAction;
    acPrint: TAction;
    acExit: TAction;
    F1: TMenuItem;
    N1: TMenuItem;
    O1: TMenuItem;
    S1: TMenuItem;
    A1: TMenuItem;
    U1: TMenuItem;
    P1: TMenuItem;
    X1: TMenuItem;
    N2: TMenuItem;
    N3: TMenuItem;
    OpenTextFileDialog1: TOpenTextFileDialog;
    SaveTextFileDialog1: TSaveTextFileDialog;
    procedure FormCreate(Sender: TObject);
    procedure acExitExecute(Sender: TObject);
    procedure acOpenExecute(Sender: TObject);
    procedure acSaveExecute(Sender: TObject);
    procedure acSaveAsExecute(Sender: TObject);
    procedure acNewExecute(Sender: TObject);
  private
    { Private 宣言 }
    FFileName: String;
    FEncodingIndex: Integer;
    procedure Init;
    procedure SaveFile;
    procedure UpdateCaption;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
// フォーム作成時
begin
  Init;
end;

// [FILE] Actions

procedure TForm1.acNewExecute(Sender: TObject);
// Action: 新規(N)
begin
  Init;
end;

procedure TForm1.acOpenExecute(Sender: TObject);
// Action: 開く(O)...
var
  Enc: TEncoding;
begin
  if OpenTextFileDialog1.Execute then
    begin
      // 値を保存
      FFileName := OpenTextFileDialog1.FileName;
      FEncodingIndex := OpenTextFileDialog1.EncodingIndex;
      // キャプションを変更
      UpdateCaption;
      // EncodingIndex によりエンコーディングを指定
      case FEncodingIndex of
        1: Enc := TEncoding.Unicode;
        2: Enc := TEncoding.BigEndianUnicode;
        3: Enc := TEncoding.UTF8;
      else
        Enc := TEncoding.Default;
      end;
      // エンコーディングを指定して読み込み
      Memo1.Lines.LoadFromFile(FFileName, Enc);
    end;
end;

procedure TForm1.acSaveExecute(Sender: TObject);
// Action: 上書き保存(S)
begin
  if (FFileName = '') then
    begin
      // 名前を付けて保存
      acSaveAs.Execute;
    end
  else
    begin
      // 上書き保存
      SaveFile;
    end;
end;

procedure TForm1.acSaveAsExecute(Sender: TObject);
// Action: 名前を付けて保存(A)...
begin
  if SaveTextFileDialog1.Execute then
    begin
      // 値を保存
      FFileName := SaveTextFileDialog1.FileName;
      FEncodingIndex := SaveTextFileDialog1.EncodingIndex;
      // キャプションを変更
      UpdateCaption;
      // ファイルへ保存
      SaveFile;
    end;
end;

procedure TForm1.acExitExecute(Sender: TObject);
// Action: メモ帳の終了(X)
begin
  Self.Close;
end;

// メソッド

procedure TForm1.Init;
// エディタの初期化
begin
 // 値を初期化
  FFileName := '';
  FEncodingIndex := 0;
  Memo1.Lines.Clear; // Memo1 の中身を消去する。
  // キャプションを変更
  UpdateCaption;
end;

procedure TForm1.SaveFile;
// Memo の内容をファイルに保存
var
  Enc: TEncoding;
begin
  // EncodingIndex によりエンコーディングを指定
  case FEncodingIndex of
    1: Enc := TEncoding.Unicode;
    2: Enc := TEncoding.BigEndianUnicode;
    3: Enc := TEncoding.UTF8;
  else
    Enc := TEncoding.Default;
  end;
  // エンコーディングを指定して保存
  Memo1.Lines.SaveToFile(FFileName, Enc);
end;

procedure TForm1.UpdateCaption;
// キャプション (ファイル名) の更新
var
  Dmy: String;
begin
  if FFileName = '' then
    Dmy := '無題'
  else
    Dmy := TPath.GetFileName(FFileName);
  Self.Caption := Dmy + ' - メモ帳クローン';
end;

end.
```

余談:
この記事を書くのに Windows 8.1 を使っているのは何故かというと、**Delphi は他のグレードの Delphi と共存できない**からです。Windows 10 の実機には Delphi (正確には RAD Studio) 10.2 Tokyo Professional Edition がインストールされており、Starter Edition がインストールできそうな余ってた VM は Windows 8.1 だけだったのです。 

P.S.
{ } は Object Pascal でのブロックコメントです。何の事？って右上にあるリスト (インデックス) の事です :sweat_smile:

**[=> to be continued.](https://qiita.com/ht_deko/items/525cd67aaba16681c3ba)**