個人の目次へ - 全体の目次へ
前の章へ - 次の章へ

第三章 システムの説明

この章では、ページ毎にシステムの説明をする。その前に、ページについて説明する。

3-0 ページについて

このシステムは操作毎に別のフォームを使わず、タブによるページ分けを行っている。ページ表示を利用することで「同時に表示できない」という問題があるが、スペースを有効に利用できるようになる。タブを使用してページ分けを行うには、PageControlコンポーネントTabControlコンポーネントを使う。プログラムからページを操作する必要がある場合には、PageControl コンポーネントの方が扱いやすい。このプログラムではページ管理を行うので、 PageControl コンポーネントを利用する。

ページを作成するには、コンポーネントをフォームに貼り付け、右クリックからページの新規追加を行う。

プログラム内でアクティブページ(表示しているページ)をプログラムから切り替えるには、ActivePageIndex プロパティを利用する。

// 3ページ目をアクティブページにする。 PageControl1 . ActivePageIndex := 2;

3-1 メール作成ページ

「メール作成」ページは、メールの作成・送信を主としたページである。

このプログラムにおける送受信は、データベースと並んで重要なウェイトを占めている。送受信を行う前に「アカウント設定」ページで、設定しておく必要がある。設定をせずに送受信を行った場合、ダイアログを表示してアクティブページを「アカウント設定」ページにする。

「受信」ボタンを押すことで、現在何通のメールを受信しているのかも分かるようにしてある。ソフトの性質上、受信機能は直接必要になることはないが、送信サーバ(SMTPサーバ)の設定次第ではPOP before SMTPによる認証が必要になる場合がある。POP before SMTPとは、電子メールを送信する前に、受信作業を要求される物である。利用しているSMTPサーバによってはこの機能が必要になる場合があるので、受信サーバ(POPサーバ)へアクセスする機能を実装した。またPOP before SMTPでは、受信サーバへアクセスし正規ユーザであることを認証してもらえば、すぐに接続を切っても良い。しかし受信サーバに接続したのだから「何通受信したのか」を分かるようにし、「何通の受信があるのか」をフォーム下にあるStatusBar コンポーネントに表示する。StatusBar は、主にソフトの情報を表示する時に使われるコンポーネントである。StatusBar は、このプログラムのように、パネルを複数に区切って表示させることも可能である。

// StatsuBar2つ目のパネルに、[あかさたな]と表示させる。     StatusBar1 . Panels[1] . Text := 'あかさたな';

次はサーバへアクセスする部分の解説である。SMTP サーバにアクセスするためにはClientSocketコンポーネントか、NMSMTPコンポーネントのいずれかを使用する。ここでは NMSMTP コンポーネントを利用した場合について書く。送信に必要なプロパティを解説する。

プロパティ名解説
Charset文字コードの設定。日本語を扱う場合、iso-2022-jpと設定する。
Host接続する送信サーバのホスト名か、IPアドレス。
Portポート番号の設定。SMTPは、通常25
PostMessageメール本文に必要なデータ。
TimeOut強制切断されるまでの時間(ミリ秒)。
UserIDユーザID。接続に必要な場合がある。

PostMessageは、さらに以下のプロパティを持つ。

プロパティ名解説
Bodyメール本文。
Date送信日時。未記入時は、現在の時刻。
FromAddress送信者のメールアドレス。
FromName送信者の名前。
LocalProgram送信者の利用した MUA(メールソフト)の名前。
ReplyTo送信者のメールアドレス。返信に用いられる。
Subjectメールの題名。
ToAddress送信者のメールアドレス。通常はここを用いる。
ToBlindCarbonCopy送信先のメールアドレス。この欄に記述されたメールアドレスは、他の送信先には分からなくなる。
ToCarbonCopy送信先のメールアドレス。カーボンコピー(写し)。

接続処理を行う前に、これらのプロパティにデータをセットする必要がある。ユーザの未記入などでデータをセットできない場合は、処理を途中で打ちきり、接続しないようにする。必要なデータが揃っているなら、SMTP サーバへ接続する。NMSMTP コンポーネントの Connect メソッドを呼び出す。接続できればメールを送信する。送信に成功すれば、SMTP サーバから切断する。また、接続や送信に失敗した時は、失敗したと表示すべきなので、それも実装する。

以下、プログラムから必要な部分を抜粋する。

procedure TMainForm.ConnectSMTPButtonClick(Sender: TObject); begin // 必要なデータがあるかどうかをチェックする処理を行っておく。 // SMTP サーバへ接続する NMSMTP1 . Connect(); end; // ---------------------------------------------------------------------------------- procedure TMainForm.NMSMTP1Connect(Sender: TObject); begin // 接続できたら送信する NMSMTP1 . SendMail(); end; // ---------------------------------------------------------------------------------- procedure TMainForm.NMSMTP1Failure(Sender: TObject); begin // メールの送信に失敗したら、メッセージを表示して切断 ShowMessage('送信に失敗しました。'); NMSMTP1Disconnect(); end; // ---------------------------------------------------------------------------------- procedure TMainForm.NMSMTP1Disconnect(Sender: TObject); begin // 切断した ShowMessage('切断されました。'); end; // ---------------------------------------------------------------------------------- procedure TMainForm.NMSMTP1Success(Sender: TObject); begin // メールの送信に成功した if (NMSMTP1.Connected) then // 接続されていれば begin NMSMTP1.Disconnect(); // 切断する StatusBar1 . Panels[0] . Text := 'メールを送信しました。'; end; end; // ---------------------------------------------------------------------------------- procedure TMainForm.NMSMTP1ConnectionFailed(Sender: TObject); begin // ホスト先への接続に失敗 ShowMessage('接続できません。'); end;

送信処理の次に説明するのは、受信処理である。POP サーバにアクセスするためには、ClientSocketコンポーネントか、NMPOP3 コンポーネントを利用する。このプログラムでは受信に重きを置いていない。受信サーバに接続して「何通のメールが来ているか」だけを調べているという簡単なものなので、ClientSocket コンポーネントを利用した。本格的に受信機能を実装するなら、NMPOP3 コンポーネント等の専用コンポーネントの利用を薦める。ClientSocket コンポーネントは、ネットワークに接続するために必要な機能を備えているが、特定のサーバにアクセスするために作られたものではない。サーバとやりとりするコマンドやメッセージは、自分で処理しなければならない

ClientSocketの、プロパティは、NMSMTPのそれと比べると多くない。以下、このコンポーネントのプロパティ(一部)である。

プロパティ名 解説
Address 接続先の IP アドレス
Host 接続先のホスト名
Port ポート番号。POP3は、通常110番を使う。
Service サービスの種類。ここではPOP3

こちらも接続処理を行う前に、これらのプロパティにデータをセットする必要がある。ユーザが未記入などでセットできない場合は、処理を途中で打ちきり、接続しないようにする。ただし、Address と Host は、いずれか一方があれば良い。必要なデータが揃っているなら、POP サーバへ接続する。ClientSocket コンポーネントの Open メソッドを呼び出す。接続できれば受信メール数をサーバから得る。データを得たらに、POP サーバから切断する。サーバから切断するには、Close メソッドを呼び出す。接続に失敗した場合のエラーメッセージも実装する。

ClientSocketコンポーネントを使ってサーバとの通信をする場合、注意する点がある。クライアントがサーバに送ったコマンドの結果を得たら、一度メソッドを抜けなければならない。同一メソッド内で連続してコマンドのやりとりはできないのである。この点は、Timer コンポーネントの使用と、専用の列挙型のデータ内に状態を保持することで解決できる。

「受信」ボタンが押されることで、Timer コンポーネントが有効になり、OnTimer イベントが発生する。イベント内で状態を見て、それぞれに対応する実行するメソッドを実行する。

以下、受信するプログラムから必要な部分を抜粋する。Timer の OnTimer イベントは以下のようになる。

procedure TMainForm.SequenceProcess(); begin case pop3Status of // 状態を見る popConnecting : begin // サーバに繋いで、状態を popUser に Connect2POP3Server(); pop3Status := popUser; end; popUser : begin if( LastReceivedString = EmptyStr ) then // 通信に不都合が生じた begin DisconnectFromPOP3Server(); end else begin // ユーザ名を送って、状態を popPass に SendingUserName(); pop3Status := popPass; end; end; // パスワードを送って、状態を popStat に // 以下受信したデータチェック、コマンド発行、状態変更の繰り返し popPass : 〜処理〜 popStat : 〜処理〜 popViewStat : 〜処理〜 popDisconnecting : // サーバから切断する。 begin DisconnectFromPOP3Server(); Timer1 . Enabled := False; // Timer を無効にする end; end; // case の終わり end;

クライアントが、POP サーバへ送る命令文字列は、

コマンド名 [引数] CRLF

である(引数は必要があれば)。

クライアントへ、POP サーバから送られてくる文字列は、

状態 キーワード CRLF

である。

コマンドは、3,4文字のものである。状態とは、処理に成功したか否かで、成功した場合は+OKを、失敗した場合は-ERRである。キーワードには、サーバからのメッセージなどが含まれる。受信/送信共にCRLFで終わる(参考 RFC 1939)。以下は実際のサーバとやりとりしたもの(一部)である。

USER hoge +OK Password required for hoge PASS password +OK hoge has 20 messages ( 6914 octets ).

場合によってサーバから、一度に複数行の結果を受け取ることもある。その際は、.CRLFから始まり、.CRLFだけの文字列でない場合が、その行が最終行である(参考 RFC 1939)。また、サーバへパケットを送る場合、SendTextメソッドを使う。以上をふまえて、プログラムからサーバへコマンドを送る時は、以下のようになる。

// POP3 サーバにユーザ名を送る ClientSocket1 . Socket . SendText('USER ' + USERNAME + CRLF); // POP3 サーバにパスワードを送る ClientSocket1 . Socket . SendText('PASS ' + PASSWORD + CRLF); // 受信したメール数と総バイト数を尋ねる ClientSocket1 . Socket . SendText('STAT' + CRLF); // POP3 サーバから切断する ClientSocket1 . Socket . SendText('QUIT' + CRLF);

サーバから送られてくる文字列は、ReceiveTextメソッドを用いれば受信できる。ただし、一度次の文字列を受信すると、前に受信した内容は消えてしまうので、受信した物を一度にまとめて処理したい場合、別の場所に保持させる必要がある。

最後に、エラー処理である。ここはメッセージを表示して、切断するだけである。

procedure TMainForm.ClientSocket1Error(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer); begin StatusBar1 . Panels[0] . Text := '接続エラーが出ました'; // 以下POP3 サーバから接続を切る処理 end;

3-2. アカウント設定ページ

「アカウント設定」ページでは、送受信に使うサーバの設定を行う。

「保存」ボタンで設定したデータを、いわゆる.iniファイルに保存する。.iniファイルは、プログラムと同一ディレクトリに作成される。「読込」ボタンでは、作成されたデータを読み込む。ただし、パスワードだけは保存されない。.ini ファイルは、誰でも閲覧であるため、そのままパスワードを記載するのは危険であるからだ。Windows 95 以降の Windows 用のプログラムでは、.ini ファイルではなくレジストリの使用を薦めているが、.ini ファイルの方が手軽なので、こちらを利用した。

.ini ファイルを操作するには、uses 節に、IniFilesを追加する必要がある。さらに、ファイルに読み/書きするに実体を生成し、専用の関数でデータの読み書きを行う。

書き込みには Write〜(型によって異なる)関数を使う。引数は(セクション名,キーの名前,値)となる。

procedure TMainForm.SaveConfigButtonClick(Sender: TObject); var myIniFile : TIniFile; begin myIniFile := TIniFile . Create( ExtractFilePath( Application . ExeName ) + 'mail.ini'); with myIniFile do begin try WriteString('SMTP', 'ServerName', SMTPServerName ); WriteString('SMTP', 'PortNumber', SMTPPortNumber ); WriteBool('Auth', 'PbS', UsePbSAuth); WriteInteger('Connection', 'Time', IntervalValue); // その他に必要な値の書き込み except // IniFile へ書き込めない begin ShowMessage('IniFile に書き込めませんでした'); end; // except end; // try Free; end; end;

実際に生成されるファイルは、このようになる。

[SMTP] ServerName=smtp.foobar.ne.jp PortNumber=25 [Auth] PbS=0 [Connection] Time=2

.ini ファイルから値を読み出す。読み込みには Read〜(型によって異なる)関数を使う。引数は(セクション名,キーの名前,デフォルト値)となる。指定したキーの値が読み込めた場合はその値を返し、読み込めなかった場合は、デフォルト値で指定した値を返す。

書き込み時のWrite〜 の部分のみが違うので、そこだけを抜粋する。

// .ini ファイルを読み込み、値を代入する。 SMTPServerName := ReadString( 'SMTP', 'ServerName', '' ); SMTPPortNumber := ReadString( 'SMTP', 'PortNumber', '25' ); UsePbSAuth := ReadBool('Auth', 'PbS', False ); IntervalValue := ReadInteger('Connection', 'Time', 5 );

3.3 Databaseページ

「Database」ページでは、データの編集やデータベースのフィールドの操作を行う。

そのために、Query,Datasource,DBGrid,DBNavigator コンポーネントを利用する。

Query コンポーネントは、データベースそのものを操作するのために利用する。データベースを利用するのなら、Table コンポーネントもあるが、SQL による操作を行えるので、Query コンポーネントを利用する。

Datasource コンポーネントは、Query, Table コンポーネントとデータベース関係のコンポーネントとのやりとりを可能にさせる。

DBGrid コンポーネントは、データベースの表示や編集を行う。データの編集するには、DBGrid で表示されている、編集したいセルをダブルクリック、または Enter キーを押すことで、編集モードへ移行する。編集が済んだら、別のセルに移動すれば値は更新される。DBGrid で編集を行うには、DBGrid コンポーネントのOptionsプロパティdgEditingの値はTrueでなければならない。

レコードの追加、挿入、削除などの操作には、DBNavigator コンポーネントを利用する。各ボタンの意味は、マウスカーソルを上に置けば表示される。

データベースのフィールド(列)を追加、削除するには、「フィールド追加」、「フィールド削除」ボタンを利用する。このボタンを押すと、SQLを利用してフィールドを操作する。追加するには、以下のSQL文を実行する。

ALTER TABLE データベース名 ADD フィールド名 データの型

となる。このプログラムでは、以下の型が扱える。

型名説明
Varchar短い文字列(50文字まで)
Integer整数
Float実数
Boolean論理値
Money金額(\マークと共に数値が表示)
Date日付
Time時刻
TimeStamp日付+時刻
Blob長い文字列(メモ型)

フィールドを削除するSQL文は、

ALTER TABLE データベース名 DROP フィールド名

となる。削除は追加よりも注意するべきである。

procedure TMainForm.AddFieldButtonClick(Sender: TObject); begin // フィールドを追加 with Query1 do begin Close; SQL . Clear; // 空にしておく。 try SQL . Add('ALTER TABLE kadai_dm ADD'); SQL . Add( FieldNameEdit . Text + ' '); SQL . Add( [型名] ); ExecSQL; finally // 実行してもされなくても、表示を元に戻す。 SQL . Clear; SQL . Add(' Select * from kadai_dm.db '); Open; end; end; end;

削除の場合は、SQL.Addの部分を以下と読み替える。

SQL . Add('ALTER TABLE kadai_dm DROP'); SQL . Add( DBFieldListComboBox . Text );

このプログラムでは、データベースのフィールド名の一覧を多くの場所で必要とする。このページでも、フィールドを削除する時の ComboBox コンポーネントでも利用している。フィールド一覧を取得する方法を書いておく。

procedure TMainForm.GetFieldNames2CBBox(); begin // ComboBox に Field 一覧追加 DBFieldListComboBox . Clear; DBFieldListComboBox . Items := Query1 . FieldDefList; end;

3.4.絞り込み

「絞り込み」ページでは3つまでの条件か、SQL文によって絞り込みを行う。

「条件による絞り込み」で指定された場合は、指定された条件をSQLに翻訳する。「SQLによる絞り込み」の場合は、そのままSQLを実行する。「実行」ボタンを押すと、生成されたSQL文が実行される。「実行後にページを切り替える」をチェックしてあれば、「Database」ページに切り替わる。「解除」ボタンを押すとデータベース全体を表示する。

条件を指定して絞り込む場合、フィールド名から型名を調べ、それに見合う条件を ComboBox に追加する。

procedure TMainForm.FirstFieldNameComboBoxChange(Sender: TObject); var pFieldNameComboBox, pPreconditionComboBox : ^TComboBox; begin // ポインタにはあらかじめ代入してある。 // 複数から呼び出されるので、ポインタを利用している。 // FieldDefs は、1番目から順に、0,1,2…となる。 case Query1 . FieldDefs[ pFieldNameComboBox ^. ItemIndex ].DataType of // 型によって条件を変える ftBoolean : begin pPreconditionComboBox ^. Items . Add('真'); pPreconditionComboBox ^. Items . Add('偽'); end; ftTime, ftDate, ftDateTime : begin pPreconditionComboBox ^. Items . Add('古い'); pPreconditionComboBox ^. Items . Add('古いか等しい'); pPreconditionComboBox ^. Items . Add('等しい'); pPreconditionComboBox ^. Items . Add('新しいか等しい'); pPreconditionComboBox ^. Items . Add('新しい'); end; // 以下同様… ftString, ftMemo : …… ftCurrency, ftFloat, ftInteger, ftSmallInt : …… end; end;

ここではポインタを使っている。Pascalでは普段あまりポインタを意識する必要はないが、ポインタを利用する方がプログラムをきれいに書ける場合が多々ある。

このプログラムは、検索したいフィールド名を選ばせ、選ばれたフィールドの型によって、条件で選択可能なデータを変えている。検索には三つまでが指定できるようになっているので、呼び出される場所も3カ所である。「どこから呼び出されたか」を判断するには、Tag プロパティを使う。Tag を使うことで、それぞれのコンポーネントに番号を振り当てられる。このプログラムでは、一つ目のフィールド名を得るComboBox に1, 以後2,3とあらかじめ割り振ってあり、それに対応してポインタを初期化する。こうすることで、コーディング量を減らせる。

// 変数宣言部 pValueEdit : ^TEdit; pFieldNameComboBox , pPreconditionComboBox : ^TComboBox; // ---------------------------------------------------------------------------- // プログラム内 case TComboBox(Sender) . Tag of 1 : begin pValueEdit := @SecondValueEdit; pFieldNameComboBox := @SecondFieldNameComboBox; pPreconditionComboBox := @SecondPreconditionComboBox; end; 2 : begin pValueEdit := @ThirdValueEdit; pFieldNameComboBox := @ThirdFieldNameComboBox; pPreconditionComboBox := @ThirdPreconditionComboBox; end; else // case begin pValueEdit := @FirstValueEdit; pFieldNameComboBox := @FirstFieldNameComboBox; pPreconditionComboBox := @FirstPreconditionComboBox; end; end; // (以下1つ前のコードへ続く)

SQL に翻訳する時も、一つ一つ「型名を調べて、条件を追加する」を繰り返し、少しずつ作っているのである。レコードが何件あるかは、Query コンポーネントのRecordCountメソッドを使う。

StatusBar1 . Panels[1] . Text := '該当件数:' + IntToStr(Query1 . RecordCount ) + '件';

文字列の「含まれる」、「含まれない」などは、SQL のLIKE%を使う。含まない場合は、NOTを使う。

// 「文字列」から始まる文字列データを探す SELECT * FROM データベース名 WHERE フィールド名 LIKE '文字列%' // "foobar"を含まない文字列データを探す SELECT * FROM データベース名 WHERE NOT (フィールド名 LIKE '%foobar%')

データベースのシステムによっては、文字列検索に正規表現が使えるものもある。その場合はもう少し複雑な条件が使えるので、あらかじめ利用できるかを調べておく方がよい。

3-5. 似たメール作成

「似たメール作成」ページ。

メールアドレスのフィールドを選び、題名を書き、メールのソースと書き、送信する。このプログラムで一番重要な機能である。「Database」ページのレコードを元に、メールを送信する機能である。メールのソースで参照したいフィールドを指定する場合には、#{FIELDNAME}のように書かなければならない。文章は普通に書ける。

メール送信部は既に実装済みであるので、メール本文内にある、ユーザが指定したフィールドを個々人のデータに置き換えるだけである。

プログラムでは、メールのソースからフィールド名を抜き取る時に、正規表現を使って探している。(Ansi)Pos関数でも探せるが、このように特定の文字(列)から始まり、特定の文字(列)で終わる文字列を探すには、正規表現が向いている。正規表現はテキスト処理を得意とするスクリプト言語の多くに実装されている機能である。正規表現については、別の文献をあたって欲しい。

「送信」ボタンを押すことで、以下の処理が始まる。

  1. SMTP サーバへ接続
  2. 題名をセットする
  3. メールのソースを別の変数に入れておく
  4. プログラムは、データが無くなるまで以下の操作を繰り返す
    1. テキスト内の、#{フィールド名}の文字列を、全て値と置き換える
    2. 指定されたメールアドレスに、データを送る
    3. メールのソースを元に戻す
    4. 次のデータへ
  5. ループ
  6. SMTP サーバから切断
procedure TMainForm.MailWithDBSend(); var RStart, RLength: Integer; gotFieldName, tempStr, bak: AnsiString; begin // 既にメールサーバに繋いである AWKStr1 . RegExp := '#{[^}]+}'; // パターンを設定しておく。 bak := MailSourceMemo . Text; NMSMTP1 . PostMessage . Subject := DBMailSubject . Text; while not Query1.Eof do Begin while not (AWKStr1 . Match(AWKStr1.ProcessEscSeq( MailSourceMemo . Text ), RStart, RLength) = 0) do begin // フィールド名と値を取り替える gotFieldName := Copy( AWKStr1 . ProcessEscSeq( MailSourceMemo . Text ), RStart + 2, RLength - 3); tempStr := MailSourceMemo . Text; AWKStr1.Sub( Query1[gotFieldName] , tempStr ); MailSourceMemo . Text := tempStr ; end; NMSMTP1 . PostMessage . ToAddress . Text := Query1[MailAddressesComboBox . Text]; NMSMTP1 . PostMessage . Body . Text := MailSourceMemo . Text; MailSourceMemo . Text := bak; NMSMTP1.SendMail(); // メールを送る Query1 . Next; // 次のデータ end; NMSMTP1.Disconnect; // 自分で切断する end;

3-6. 編集フォーム

データを編集するフォーム。

Database の編集用フォームを「表示する」ボタンで表示を切り替えられる。全てのデータを編集できるようになっているが、Memo(Blob)型以外のデータを編集する場合は、DBGrid を使った方が早いので、Memo型のデータを編集するためのフォームと言っても過言ではない。

このフォームは、現在選択されているセルデータの型と値を調べる。現在選択されているセルは、マウス・キーボードによって移動・クリックされた時に、イベントを発生せさて取得する。

procedure TMainForm.DBGridCellMove(); begin // マウス・キーボード両方から呼ばれる // メモ型なら DBMemoコンポーネント、 // それ以外なら DBEdit コンポーネントを利用 …… …… end; // ----------------------------------------------------------------------------- // マウスでもキーボードでも、移動を取れるように。キーボード。 // ----------------------------------------------------------------------------- procedure TMainForm.DBGridViewerKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); begin DBGridCellMove(); // セルが動いたことにする end; // ---------------------------------------------------------------------------- // マウスでもキーボードでも、移動を取れるように。マウス。 // ---------------------------------------------------------------------------- procedure TMainForm.DBGridViewerMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin DBGridCellMove(); end;