Delphi入門

ファイルからデータを読み込むには?

TMemoを取りあえずは使って見よう!

次にテキストファイルを読み込むという 作業を行って行く訳ですが、すぐにできる訳ではありません。 答えを言ってしまえば読み込むのはプログラム1行で出来ます。 しかし、メカニズムを知っておかなければ後々困る事になるので色々な諸事項を押さえて下さい。

「TMemo」というコンポーネントがあるのは、 最初の方に少しだけ紹介しました。気付いてない方のためにもう一度言いますと、 これはWindows標準のメモ帳がありますが、あれと同じ役割を果たしてくれるコンポーネントです。 TMemoというのがありませんか?これを配置して見て下さい。

TMemo設置

このようになると思いますが、 ちょっと違いますね?スクロールバーがないと思いますが、これは設定でできます。 「オブジェクト インスペクタ」で「ScrollBars」というのがあると思います。ここで設定します。 「ssNone」とかあると思います。

ssNone
スクロールバーなし
ssVertical
縦方向のみ(Verticalは垂直)
ssHorizontal
横方向のみ(Horizonは水平)
ssBoth
両方(Bothは両者の意味)

これを色々変えてみて(色々という程ありませんが…) また、これを配置した地点で実行して見て下さい。 メモ帳のように文字が書けたと思います。 これがメモ帳の効果です。 TEdit の「TEdit」は一行だけ書けたのに対して、この「TMemo」は複数行書けます。

余談ですが、「Visual Basic」ではテキストボックスコントロールを貼り付けて、 「MultiLine」を「True」に設定しますので、多少乗り換える方は違和感を感じるかも知れませんね。

さて、何がいいたいのでしょうか(笑)。 テキストファイルをこの「TMemo」に読み込みたい訳です。 しかし、まずは、この「TMemo」に慣れて欲しいと思います。 実行時のテキストを編集したい場合は、「オブジェクト インスペクタ」で「Lines」という所で編集します。 また、折り返しをしたい時は、「WordWrap」という所がありますので、 そこを「True(右端で折り返す)」、「False(折り返さない)」という設定をします。 今は、この2つだけに重点を置いてやっていく事にします。 では、次の文章をカット&ペーストするなりして、Linesに追加して見て下さい。

one
two
three
four
five
six
seven
eight
nine
ten

今、しようとしている事は、 テキストファイルを読み込む前に、ここに書かれている行から各行を読み出してタイピングする!という事を考えます。 10問の問題がこれで作成できたわけですね。 まあ、早速やってみたいでしょうが、焦らずにじっくり見て行く事にしましょう。 これと今まで勉強して来た事をふまえて次のようなレイアウトをして見て下さい。

TMemoレイアウト例

というのを作って見て下さい。ちなみに「Label2」の「Caption」は何も記入しないで下さい。 ここには、左の「TMemo」から問題を読み取って表示させるようにします。 また、「Label1」の「Caption」は何でも構いません。適当に書いて下さい。 「TMemo」の「ScrollBars」はどうでも構いません。また、「WordWrap」は「False」にしておいて下さい。 理由は後で言います。。。

ちょっと「Start」ボタン(Button1)の上にカーソルが来たらカーソルを変えて見ましょう。 「Button1」の「Cursor」という所がありますので、自分の好きなカーソルにして見て下さい。 「crHandPoint」なんかが良いと思います。

しかし、ここで大きな壁があります。 問題を出題は構わないけど、タイプし終わって次の問題を出題するにはどうすればいいのかなぁという疑問が出てくると思います。 今は、時間を扱うのは難しいので、次のように考える事にします。

  • 画面に表示されている問題を打ち終わったら、「TMemo」の行を削除し、最後削除して、なくなったらそこで終了する事とする!

という風に考えればいいのではないでしょうか? つまり、一問目は、「one」と出題されます。これを打ち終わった瞬間、「TMemo」にある、「one」を削除します。 次に「two」を「Label2.Caption」に反映させて・・・・ と繰り返します。

そして、最後「ten」が出題されます。 これを打ち終わった地点で「TMemo」が1行しかなければその地点で終了!という事になりますよね。 これはいいでしょうか?行削除などのやり方は追々見て行きます。

TMemoをプログラム上から編集する

まずを「Start」を押すと、問題開始です。 「TMemo」のA行目に書かれている文字列を獲得したい時には、

Memo1.Lines[A-1]

とします。注意しなくてはならないのは、「-1」です。1行目は0として扱います。 この結果はString型(文字列型)です。適当に変数s を『var s : String ;』用意して、

s := Memo1.Lines[A-1]

とする事が出来ます。「Start」ボタンに試しに、

showmessage(Memo1.Lines[0]) ;

として見て下さい。「one」というメッセージが出るはずです。[ ]内には必ず整数値を入れます。 さて、これで試しが終わった所で問題を出題させて見ましょう! 「Start」ボタンには、初期化&最初の一題を表示させるというプログラムでいいのはお分かりでしょうか?

TForm1.Button1Click(Sender:TObject) ;
begin
   Memo1.Lines.Clear ;
   Memo1.Lines.Add('one') ;
   Memo1.Lines.Add('two') ;
   Memo1.Lines.Add('three') ;
   Memo1.Lines.Add('four') ;
   Memo1.Lines.Add('five') ;
   Memo1.Lines.Add('six') ;
   Memo1.Lines.Add('seven') ;
   Memo1.Lines.Add('eight') ;
   Memo1.Lines.Add('nine') ;
   Memo1.Lines.Add('ten') ;
   Label2.Caption := Memo1.Lines[0] ;
end ;

これからは、問題が打ち終わると、 どんどん「Memo1」の行を削除して行くわけですから、 初期化+最初の問題を「Label2」に設定するという作業を行っています。ここで、

Memo1.Lines.Clear ;

というのがあると思います。これは、「Memo1」の内容をすべてクリアする!という役割を持ちます。 今までは、「Label1.Caption」とかだけでしたね?しかし、今回は結構複雑です。次の例で考えて見て下さい。 例えば、ある集合「食べ物」があるとします。その中に「野菜」「果物」とか「肉」とかあります。 更に、「野菜」の中にも色々ありますね。 これらを、「Delphi」では「食べ物.野菜.有機野菜」のように、 「.(ドット)」を用いてどんどん絞って行くような感じに仕立てるわけです。 つまり、「Lines」は「Memo」の要素、「Add」は「Lines」の要素という事で対応しています。 とは言っても少し難しいでしょうか。次第に慣れてくると思います。

Memo1.Lines.Add('one') ;

というのがあります。これは、「Memo1」に「'one'」という行を追加するという意味になります。 つまり、このプログラムは、「Memo1」の内容をすべて消去して、初期化しているというプログラムになります。 しかし、なんか見栄え悪いと思う方もいらっしゃるでしょう。 これをコンパクトにする事もできます。コンパクトにする事はできますが、 慣れない内は最初の書き方で書いて下さい。以下、コンパクトにしたプログラムを書きます。

TForm1.Button1Click(Sender:TObject) ;
begin
   with Memo1.Lines do
   begin
      Clear ;
      Add('one') ;
      Add('two') ;
      Add('three') ;
      Add('four') ;
      Add('five') ;
      Add('six') ;
      Add('seven') ;
      Add('eight') ;
      Add('nine') ;
      Add('ten') ;
   end ;
   Label2.Caption := Memo1.Lines[0] ;
end ;

上記のように「with」で囲みます。 上記の例を参考にして下さい。しかし、慣れない内はこれを使うとミスる事が良くあるので注意して下さい。 というよりは慣れるまであまり使わない方が無難です。 理由は上達するに連れて良く分かるようになると思います。

TMemo内の行を削除する時の注意点

これで「Button1」は終了した訳ですね。 さて、次に考えなくてはならない事があります。 まず、出題された問題をタイプしたら先頭から消すというのは前回のプログラムをそのまま利用すればOKです。 以下のは、前回の練習からの抜粋です。ここで偶然「Label2」に問題となっていますし、変更する必要はないわけです。 しかし、今回は一題ではなく複数題です。どこを変更しなくてはならないかを考えて見ましょう。

procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
var s : String ;
begin
   //Form1のOnKeyPressイベントに書くプログラム
   KeyPreview := True ;
   s := Label2.Caption ;

   if Key = Copy(s, 1, 1) then
   begin
      Delete(s, 1, 1) ;
      Label2.Caption := s ;
   end ;

   if Label2.Caption = '' then
   begin
      showmessage('タイピング完了!') ;
      Button1.OnClick(Sender) ;
   end ;
end ;

さて、取消線の部分が必要ありませんね。 「Label2.Caption」が空になった地点で終わりとは限りません。「Label2.Caption」が空になり、 かつ「Memo」の行数が1行であれば終了という条件にならなくてはなりません。 そうでない時には、次の問題を出題させるという事をしなければなりません。 それと、言ってませんでしたが、「Memo1」のA行目を消すには次のようにします。

Memo1.Lines.Delete(A-1) ;

ここで、[ ]としては行けません。( )とします。つまり、一番先頭の行を消すときには、

Memo1.Lines.Delete(0) ;

とします。また、行が存在しないのに、

Memo1.Lines.Delete(100) ;

とかするとエラーになります(と言っても警告ウィンドウが出るだけで止まる事はありません)のでご安心下さい。 また、これを使う際に特に注意したいのが、複数行削除する時です。 例えば、3行目と5行目を削除したい時には、

Memo1.Lines.Delete(2) ;
Memo1.Lines.Delete(4) ;

とやりがちですが、これはしてはいけません! 何故なら、プログラムは上から順番に実行され、最初に3行目を削除した地点で、 自動的に、5行目に書かれていたものは4行目になるからです。この場合は、

Memo1.Lines.Delete(4) ;
Memo1.Lines.Delete(2) ;

としなくてはなりません。削除する時は末尾から消していく。 これはプログラム界では定石です。 後々また説明して行きます。また、現在の「Memo」の行数を調べるのにはどのようにしたら良いのでしょうか? これも簡単で、非常に分かり安いです。

Memo1.Lines.Count ;

とします。英単語も「Count」と非常に分かり安いと思いませんか?「Delphi」はこのように、 英単語の意味からだいたいその役割を予測する事も慣れれば出来るようになるのでこれは素晴らしいと思います。 これで現在の行数をチェックできます。結果は、整数値なので、 もし、どこかにボタンを作ってそこに何行が表示させるプログラムを書くのであれば、

ShowMessge('全部で、'+IntToStr(Memo1.Lines.Count)+'行です!') ;

のようにすればいいのはお分かりでしょうか。段々慣れてきたと思いますので、後は回数を重ねるだけです。 頑張って下さい。慣れればこんなのどうって事はありません。 また、「Count」の場合は、「-1」させません。10行あるならば、きちんと10と返しますので、 そこら辺混同しないように注意して下さい。

以上をふまえてサンプルプログラムを作ってみる

さて、どのようなプログラムを書いたらいいのでしょうか。 考えて見る事にしましょう。もし、現在の行数が一行であるならば…というのは、、、

if Memo1.Lines.Count = 1 then

とすればいいですね。これで、「もし、Memo1の行数が1行であるならば・・・」という判定を行っている事になります。 これをふまえて、プログラムを何分かかっても構いませんので自分で考えて見て下さい。 複雑になっても構いません。答えは隠してあるので分かるも分からなくても参照して見て下さい。

procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
var s : string ;
begin
   KeyPreview := True ;
   s := Label2.Caption ;

  if Key = Copy(s,1,1) then
  begin
    Delete(s,1,1) ;
    Label2.Caption := s ;
  end ;

  if Label2.Caption = '' then
  begin
    if Memo1.Lines.Count = 1 then
    begin
      showmessage('タイピング完了!') ;
    end else
    begin
      Memo1.Lines.Delete(0) ;
      Label2.Caption := Memo1.Lines[0] ;
    end ;
  end ;
end;
end ;

という風になるのはお分かりでしょうか?「Lanel2.Caption」がなって、かつ「Memo」が一行ならば… 終了させて、そうでないならば、「Memo」から読み出しという操作を行っています。また、次のように書いた方は惜しいです。

procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
var s : string ;
begin
   KeyPreview := True ;
   s := Label2.Caption ;

  if Key = Copy(s,1,1) then
  begin
    Delete(s,1,1) ;
    Label2.Caption := s ;
  end ;

  if (Label2.Caption = '') and (Memo1.Lines.Count = 1) then
  begin
    if Memo1.Lines.Count = 1 then
    begin
      showmessage('タイピング完了!') ;
    end else
    begin
      Memo1.Lines.Delete(0) ;
      Label2.Caption := Memo1.Lines[0] ;
    end ;
  end ;
end;
end ;

一見、よさそうでスマートに見えるプログラムです。 致命的な誤りがあります。どこがいけないかお分かりでしょうか? 「and」を使う事によって、プログラムを短くしている…という所までは正しいです。 しかし、「else」以下の判定をよく考えて見て下さい。「Label2.Caption」が「空」であり、 「Memo1.Lines.Count」が「1」であれば、「タイピング完了!」というのはOKですね、 それの反対「else」はどうなるでしょうか?

  • 「Label2.Caption」が「空」ではない、或いは「Memo1.Lines.Count」が「1」ではない

ここで、「and」や「or」を使った場合に、 「else」では非常に気を配る必要があります。 大学の授業や、高校時代に集合などを習った時にポチッと習った方もいらっしゃると思いますが、 「ド・モルガンの法則」というのを覚えてないでしょうか?(普通は覚えてないと思いますけど) 次のような法則が成り立つので、これは覚えておいて下さい。

ド・モルガン(De Morgan)の法則
  1. A and B = A or B
  2. A or B = A and B

このような法則があります。Aの逆は、Aとします。 これは非常に重要です。数学が苦手な方もこれは覚えるしかないので覚えて下さい。これを使いますと、

if ( Label2.Caption = '') or (Memo1.Lines.Count = 1) then ... else ...

「else以下」にはどのような条件下となるか考えて見て下さい。 上記の法則から、「Label2.Caption」が空ではない、かつ「Memo1.Lines.Count」が「1」ではない!という条件になった時に処理がされる事になりますね。「かつ」「或いは」が切り替わる訳ですね。 ですから、もし、これを「and」を使って判定するのであれば、次のようにしなくてはなりません!

procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
var s : string ;
begin
   KeyPreview := True ;
   s := Label2.Caption ;

  if Key = Copy(s,1,1) then
  begin
    Delete(s,1,1) ;
    Label2.Caption := s ;
  end ;

  if (Label2.Caption = '') and (Memo1.Lines.Count = 1) then
  begin
     showmessage('タイピング完了!') ;
  end else if Label2.Caption = '' then
  begin
     Memo1.Lines.Delete(0) ;
     Label2.Caption := Memo1.Lines[0] ;
  end ;
end ;

と、更に判定をすれば良い事になりますね。しかし、これは最初からはちょっと高度なので、 少々プログラムが長くなっても自分の頭で整理できる範囲でプログラムを書いた方が良いと思いますし、 ミスも少なくると思います。と言うわけで、一番最初に書いたプログラムをオススメします。実行して見て下さい。 うまく動きましたか?ここで、先程の誤ったプログラムを書いた場合、 なんか挙動不審な結果となる事も確かめて見て下さい。

えッ!?データを読み込むのってこんなに簡単なの?

前回で峠は越えたという所です。 応用させて点数を入れたり、減らしたりするプログラムも書けると思います。自分の好きなようにして見て下さい。 これを応用する事で色々なアプリケーションを作る事ができると思います。 「Simple Typing」の基礎もこんな感じですので、 後はプログラムの知識とコンポーネントの役割をどんどん覚えていけば時期にタイピングソフトは作れるようになるはずです。

しかし、今回の場合はタイピングを作りたい一心の方が結構いらっしゃると思いますので、 結構ハイスピードで基礎もかじる程度で飛ばしている所あります。 もし、それの埋め合わせとしての講座も開設致しますので、また後で参考にして見て下さい。 今回のアプリケーションはハッキリ言って作るのに3分かかってません(笑)。 慣れると、こんなのは数分で作れるようになります。

しかし、これをC言語とかで作ったらどうでしょう?きっと時間のかかる事でしょう。 「Visual Basic」でも慣れるとこのくらいで作れるかも知れませんね。コンパイル速度は別として… コンパイル一瞬だったのにお気づきでしょうか。 何故、「Delphi」がマイナーになっているかそろそろ疑問に感じ始めて頂ければ嬉しいです。 個人的には、Borland社とMicrosoft社の商売の巧さの違いと見ているのですが(笑)。

ではでは、次は、テキストファイルを読み込むというメインの話を進めて行きたいと思います。 テキストファイルを読み込むのには、正式には、いろいろしなくてはならないのですが、簡単な書き方があります。本当に簡単です(^^;)。

Memo1.Lines.LoadFromFile('c:\sample.txt') ;

のようにします。たった一行です!これで、Memo1に「c:\sample.txt」の内容を読み込んでくれます。 勿論、拡張子は「txt」に限りません。テキスト形式で扱えるものでしたら何でも構いません。 只、ビットマップとかも読み込めてしまいますが、そこらへんは常識の範疇で行って下さいね(笑)。 逆にセーブする時は、

Memo1.Lines.SaveToFile('c:\sample.txt') ;

これで、保存してくれます。もし、ファイルが存在しなければ自動的に生成してくれます。 何となく単語も分かりやすいです。 「LoadFromFile」(ファイルからロードする)、「SaveToFile」(ファイルへセーブする) というので分かりやすく覚えやすいと思いますし、「.(ドット)」を打ってしばらくしたら候補が出てくるので、 それで入力しても良いでしょう。試しに、「c:\sample.txt」に、適当に、タイピングさせたい英単語を並べて見て下さい。 そして、「Start」ボタンに、

procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
var s : string ;
begin
   //Button1のプログラム(追加部分は色を変更しております。)
   Memo1.Lines.LoadFromFile('c:\sample.txt') ;
   Label2.Caption := Memo1.Lines[0] ;
end ;

とでもしてみましょう。勿論、「c:\sample.txt」が見つからなければ実行時に「ファイルが見つかりません!」 とメッセージが出てきますので、正確なパスを入力して見て下さい。別にデスクトップにあるならそこでも構いません。 自分の好きなディレクトリにあるテキストファイルを読み出して見て下さい。 また、テキストの内容が何もないと、次の行で1行目を参照しているので、1行目がないという事になり、 「リストの範囲が越えています」などのエラーが表示されますので注意して下さい。 ここでは、テキストに最低限1行以上あるという事を仮定して話を進めています。

さて、実行してみて下さい。きちんと読み込めていましたか? それで、どんどんタイピングもできるようになってますね?タイピングの部分のプログラムは全く変更の必要がありません。 「Memo1」から読み込んでタイプするという事は基本的には変わってないからです。

ただ、10問で取りやめたい!とかいうのも出てくると思いますが、 そういう発展的な事もプログラム経験者であれば、もうこの地点でできる方もいらっしゃると思いますが、 もう少し基礎を慣れて見ましょう。でも、固定されたファイルから読み込むよりは、やっぱり、 ファイルダイアログを出して、問題集を選べるようにして見たいですよね?

実はこれもすごい簡単なのデス。やはり数行でできてしまいます。 コンポーネントはちょっと違う所で「Dialogs」というのがあると思います。 ここに、何やら「ファイルを開く」アイコンマークが入った「TOpenDialog」というコンポーネントがありますので、 例のごとくフォーム上に配置して見て下さい。そうすると、次のようになりましたね?また、レイアウト次のように少し変えて見て下さい。 「Memo1」の内容は空にしておきます。別に今までのままでも構いませんけど、一応…(^^;)

LoadfromFileの使い方

TOpenDialog」は、一番最初にちょこっとお話しましたが、 「非ビジュアルコンポーネント」と呼ばれるものです。ボタンやメモのように視覚的に見えてきるものではなく、 何と言ったら良いか分かりませんが、実行時にある特定の作業を行う時に、 使用するためのコンポーネントです…と言っても意味分かりましたか?(汗) ちょっと説明しづらいですが… 時期に理解できる時は来ますので大丈夫です。

ファイル・ダイアログ

それで、百聞は一見に如かず… 次のプログラムを「Button2」、つまり「Load」ボタンに書いて下さい。

procedure TForm1.Button2.Click(Sender: TObject; var Key: Char);
begin
   if OpenDialog1.Execute then
   begin
      Memo1.Lines.LoadFromFile(OpenDialog1.FileName) ;
   end ;
end ;

これで、ファイルオープンダイアログが出てきて、 ファイルが読み込めるようになります。試しに実行して見て下さい。OpenDialog1.FileNameというものは、 オープンしたファイルのフルパスを返します。また、「if」文の条件式では、もし条件式が「if ○○ = True then ...」 という形だった場合には、「if ○○ then ...」と省略する事ができます。 ですからこの場合も、本当は、「Opendialog1」が実行されたら…というので、

if OpenDialog1.Execute = True then

というのが省略されているのです。別にこれでも構いません。 これは、ファイルオープンダイアログが出てきて、「OK」(ファイルを開いた)時に起こる処理を書きます。 しかし、実際に試して見ると、何か変ではないですか?

そうなんです、よく通常のソフトで見かける拡張子とかがありませんね。 それを設定するには、「OpenDialog1」の「オブジェクト インスペクタ」を見て下さい。 「Filter」というのがあると思います。ここをダブルクリックして見て下さい。書き方の例は以下に示します。

ファイルフィルタの編集

のように記述して下さい。「*.*」の「*」は御存知の方もいると思いますが「ワイルドカード」と呼ばれるものです。 任意の文字列を表します。また、複数の拡張子を設定したい時には、フィルタの所に、

*.txt;  *.htm;  *.log;

このように、つなげて行って下さい。これで変更した後に、試しに実行してみてまた確認して見て下さい。 うまく行きましたか?設定したフィルタによって、Windowsが自動的に、後はファイルの表示を行ってくれます。 そして、「OpenDialog1」の「オブジェクト インスペクタ」の所に「Options」というのがあります。 ここをクリック(「+」の所)すると、いろいろ何やら出てきます。

これは、「読み取り専用ファイルを表示しない」とか 「隠しファイルを表示しない」とかいろいろ設定できます。中でも、「ofFileMustExist」というのがありませんか? これは「ファイルが存在するかどうかを確認する」というものです。 これは初期値は「False」になっていると思いますが、これを「True」にして下さい。 これにより、存在しないファイルを指定した場合でも、エラーを回避する事ができます。

もし「Delphi」のバージョンが低いとこれはないかも知れません。そういう場合には、 以下のようなプログラムを付け加える必要があります。ファイルが存在するかどうか調べる関数です。 使い方は簡単なので、「Delphi」のバージョンが高い方ももしかしたらの時のために覚えて下さい。

procedure TForm1.Button2.Click(Sender: TObject; var Key: Char);
begin
   if OpenDialog1.Execute then
   begin
      if FileExists(OpenDialog1.FileName) = True then
         Memo1.Lines.LoadFromFile(OpenDialog1.FileName) 
            else showmessage('ファイルが存在しません!') ;
   end ;
end ;

「= True」は先程言ったように省略可能で記述しなくてもOKです。 指定のファイルが存在するかどうかは「FileExists」という関数を使います。「Exist」というのは「存在する」という英単語ですね。 「FileExists(ファイルパス)」で、もし、ファイルが存在すれば、「True」を、存在しなければ「False」を返す関数です。 これは結構、ファイル操作をするときには、頻繁に出てくる事があるので押さえて置きましょう!

さて、これで完成しました!お疲れさまです。 まずは色々使って見て慣れて下さい。次は少し難関である時間を取り扱ってみたいと思います。 また、少し難しい話をしすぎたかも知れませんので、 基礎に戻ってもっと簡単な例をいくつか示して進めて行きたいと思います