バックログ

Delphi6 PersonalでOpenNapクライアントをつくろう!

注意:いちおうサービスパック2まで入れておいたほうがいいかも

ログインだけできるクライアント

 ログインだけできるクライアント

こういうかんじのを、はじめの目標にします。

 起動時の画面

機能はこれだけです。 でははじめましょう。

 プロジェクトの保存

起動したらすぐ適当な名前でプロジェクトを保存します。

まず、カタチから

 部品:メモとボタン

実際の動作はあとまわしで、見た目だけ先につくってしまいます。 とりあえずいりそうなのは、

ぐらいかとおもいます。

フォームに接続ボタンをはりつける

 ボタンの貼り付け

サーバー接続ボタンをはりつけます。Buttonを選んではりつけてください。 Button1という名前になるはずです。

ログ表示欄をはりつける

 メモの貼り付け

ログ表示欄をはりつけます。Memoを選んではりつけてください。 Memo1という名前になるはずです。

 プログラムのコンパイルと実行

これで見た目だけ完成です。F9を押すとかってに実行ファイルを生成して実行します。 サーバーを起動して接続ボタンをおしてみましょう。

 実行中!・・・?

サーバーに接続されません。 ボタンを押したときの動作をまだ書いてなかったので、しょうがないです。これからです。

サーバーにつないでみる

ボタンを押したときの動作を書きましょう。 ボタンを押したらサーバーに接続したいわけです。 サーバーに接続するにはどうしたらいいでしょうか? つぎのような手順をふみます。

「ソケットを開く」のあたりは、ClientSocketという部品が用意されてるので、 これをつかいます。

ClientSocketを追加する

012-ClientSocket.png

ボタンとちがって直感的にわかりにくいかもしれませんが、 TClientSocketは、通信ソケットを扱うための部品です (ソケットというのはコンセントのさしこみ口のことです)。 ボタンやメモをはりつけたときと同じようにはりつけます。 ツールパレットのIntenetタブからClientSocketを選んではりつけてください。 ClientSocket1という名前になるはずです。

接続ボタンを押したときの動作をコードにする

 ボタンをダブルクリックすると、かってに関数ができる

フォームにある接続ボタン(Button1)をダブルクリックすると、 Button1Clickという関数が自動的に作成されます。 この関数は、Button1のOnButtonClickイベントが起こったときに実行されます。 このことは、左下のオブジェクトインスペクタで確認できます。

 オブジェクトインスペクタに注目!

オブジェクトインスペクタのイベントタブをクリックすると、 Button1のOnClickイベントのところにButton1Clickと書いてあります。

 Button1Click手続きの中身を自分でかく

Button1Click関数の中身は、自分で書きます。 「サーバーに接続する」という内容のことを書けばいいです。

ここでは

procedure TForm1.Button1Click(Sender: TObject);
begin
  ConnectToServer;
end;

のようにとりあえず書いておいて、 あとでこのConnectToServer関数を書きたすことにします。

010-DeclareConnectToNapServer.png

どうやって書き足すかというと、

先頭の宣言のところで

procedure ConnectToNapServer;

のように宣言しておいて、

 ConnectToNapServer手続きの中身を自分でかく

実装のところで

procedure TForm1.ConnectToNapServer;
begin
  ClientSocket1.Host:='127.0.0.1';
  ClientSocket1.Port:=8888;
  ClientSocket1.Open;//ソケット接続開始
end;

このように書きます。

ここまでで、F9をおしてプログラムを実行してみましょう。 サーバーが127.0.0.1:8888で動いているときに、 接続ボタン(Button1)をおすと、サーバーに接続できるはずです。
といっても、接続できたかどうか確認する方法がありません(FWソフトとかでみれるけど) 接続できたらログ窓(Memo1)に「接続できました」とか表示したいですね。

接続状態をログ表示欄に表示

「接続できたら」「ログに表示」したいわけですが、 「接続できたら」の部分を判断するにはにはTClientSocket.OnConnectイベントを使います。 これはソケットが接続先のサーバーに接続できたときに起きるイベントです。

 OnConnectの項目をダブルクリックすると、かってに関数ができる

左上のオブジェクトツリーでClientSocket1を選択し、左下のオブジェクトインスペクタの イベントタブの、OnConnectの項目欄をダブルクリックすると、Delphiが 自動的にOnConnectイベントに対応する関数を宣言&実装してくれます。

 ClientSocket1Connectの中身を自分でかく

生成された関数に次のように書き足せば、サーバーlocalhost:8888に接続できたとき、 Memo1に「接続できました」と表示されます。

procedure TForm1.ClientSocket1Connect(Sender: TObject;
  Socket: TCustomWinSocket);
begin
  Memo1.Lines.Add('接続できました');
end;
 実行中!

F9を押してプログラムを実行してみてください。こんどは接続ボタンを押してサーバーに接続すると、ログ窓に'接続できました'と出たとおもいます。 プログラムを終了すれば自動的にサーバーから切断できます。


ここまでのコードをのせておきます。


つぎに、サーバーにNapコマンドを送って、ログイン手続きができるようにします。
すこしむずかしくなります。

サーバーにログイン

サーバーにログインするときは、つぎのような文字列をサーバーに送ります。
(レイアウトのため2行で書きましたが、実際は1行で書きます)

<ユーザー名> <パスワード> <ファイル転送用ポート> "<クライアント名>"
<回線種類> [<クライアントバージョン番号>]

ただし、この文字列の前に、4バイト分のタグをつけます。
タグの前半2バイトは、文字列の長さをあらわす数字をいれます。
同様に後半2バイトは、Napコマンドの番号(この場合はログイン要求なので2)をいれます。
(それぞれ16進数表示で1の位を1バイト目に、16の位を2バイト目に書きます。)

ということで、けっきょくサーバーにログイン要求のためにおくるメッセージは、例えば
ユーザー名=user1, パスワード=pass, ファイル転送用ポート=6699,
クライアント名=NapChat, 回線種類=8(DSL), クライアントバージョン番号=なし
とすると、

#11#1#2#0user1 pass 6699 "NapChat" 8

のようになります。
では、送信部分の関数を書いてみましょう。

Napコマンドの送信

 ClientSocket1Connectの中身をすこし変える

Napコマンドの送信部分は、SendNapCommandという名前の手続きにします。 引数にはソケット、コマンドID、送信データの3つをとると都合がいいです。 さっきつかったClientSocket1Connect関数を書き換えて、 SendNapCommandを実行するようにします。

procedure TForm1.ClientSocket1Connect(Sender: TObject;
  Socket: TCustomWinSocket);
begin
  Memo1.Lines.Add('接続できました');
  SendNapCommand(Socket,2,'user1 pass 6699 "NapChat" 8');//この行を追加
end;

次にSendNapCommandの中身を書きます。

 SendNapCommand手続きを宣言
宣言
procedure SendNapCommand(Socket: TCustomWinSocket;
  id: Integer; data: String);
 SendNapCommand手続きの中身をかく
実装
procedure TForm1.SendNapCommand(Socket: TCustomWinSocket;
  id: Integer; data: String);
var
  msg: String;       //msg=タグ(4バイト)+文字列
  sentbytes: Integer;//SendTextで実際に送られたバイト数
begin
  msg:='    '+data;
  msg[1]:=Chr(Length(data) mod 256);//1バイト目=16進数1の位
  msg[2]:=Chr(Length(data) div 256);//2バイト目=16進数16の位
  msg[3]:=Chr(id mod 256);          //3バイト目=16進数1の位
  msg[4]:=Chr(id div 256);          //4バイト目=16進数16の位
  while msg<>'' do //msgが完全に送信されるまでSendTextを繰り返す
  begin
    sentbytes:=Socket.SendText(msg);//msgを送信。全部送れるとはかぎらない
    Delete(msg,1,sentbytes);        //送れた分だけmsgからけずる
  end;
  Memo1.Lines.Add('送信 ['+IntToStr(id)+'] "'+data+'"');
end;

これでログイン要求をサーバーに送れるようになりました。 サーバーが満員でなければ、きっと要求を受け入れてくれるでしょう。

要求が受け入れられたことを知るには?
サーバーは要求を受け入れたとき、Napコマンドの3番で応答してきます。
この許可応答を受信できなければいけないですね。
さらに、受信できたらそのことがわかるように、 ログに「ログインできました」とか表示したいです。

Napコマンドの受信

 OnReadの項目をダブルクリックすると、かってに手続きができる

「許可応答を受信したら」「ログに表示」です。
「許可応答を受信したら」の部分には、 TClientSocket.OnReadイベントを使います。
オブジェクトインスペクタで、ClientSocket1のOnReadイベントの空欄を ダブルクリックして、イベントに対応する手続きを自動的に生成します。 手続きの名前はClientSocket1Readという名前になったとおもいます。

 ClientSocket1Readの中身をかく

ソケットが受信した許可応答を取り出す関数をRecvNapCommand、 引数はソケットだけとして、
ClientSocket1Read手続きの中に次のように書いておきます。

procedure TForm1.ClientSocket1Read(Sender: TObject;
  Socket: TCustomWinSocket);
begin
 RecvNapCommand(Socket);
end;
 RecvNapCommandを自分で宣言

それからRecvNapCommandの中身をきめます。

宣言
procedure RecvNapCommand(Socket: TCustomWinSocket);
RecvNapCommandを自分で実装
実装
procedure TForm1.RecvNapCommand(Socket: TCustomWinSocket);
var
  id,len: Integer;
  data: String;
  msg: PString;
begin
  if Socket.Data=nil then //はじめての受信
  begin
    New(msg);               //受信データの保存用領域を確保する
    Socket.Data:=msg;       //Socket.Dataに保存領域のアドレスを入れる
  end
  else                    //2回目以降の受信
    msg:=Socket.Data;       //msgに保存領域のアドレスを入れる

  msg^:=msg^+Socket.ReceiveText;       //ソケットから受信データを受けとる
  while Length(msg^)>=4 do
  begin
    len:=Ord(msg^[1])+Ord(msg^[2])*256;//データ文字列の長さ
    id :=Ord(msg^[3])+Ord(msg^[4])*256;//コマンドID
    if Length(msg^)<4+len then break;  //コマンドの長さが足りなかったら終了
    data:=Copy(msg^,5,len);            //データ文字列
    Delete(msg^,1,4+len);              //msgから処理ずみのデータを消す
    Memo1.Lines.Add('受信 ['+IntToStr(id)+'] "'+data+'"');
  end;
end;

ここで、New手続きをつかって受信用データを置いておくためのメモリを確保していますが、 New手続きで確保したメモリは、いらなくなったらDispose手続きで開放しないといけません。 ここではForm1を閉じたときに開放することにします。

 受信用データのためのメモリは自分で解放する

オブジェクトインスペクタでForm1のDestroyイベントの空欄をダブルクリックし、 Form1Destroy手続きを自動的に生成します。 そして、メモリを開放するためのコードをかきます。

procedure TForm1.FormDestroy(Sender: TObject);
var
  msg: PString;
begin
  msg:=ClientSocket1.Socket.Data;
  if msg<>nil then
    Dispose(msg);//受信用に確保していたメモリを開放
end;
 実行中!

F9を押して実行してみてください。Button1を押すと、 サーバーに接続して、なにかいろいろメッセージが返されてるのがわかるとおもいます。

しあげ

これでほとんどできあがりですが、ログ表示欄をもうすこしみやすくしたいですね。 もうすこしMemo1をひろげてみます。あと、縦スクロールバーもほしいです。

ログ表示欄をみやすくする

 Memo1を広げる

まず、ログのメッセージがみにくいので、Memo1のサイズを大きくします。 フォームでMemo1を選択すると、Memo1のふちのところに黒い点が8コついてるとおもいます。 これをドラッグすると、Memo1のサイズをかえることができます。

 縦スクロールバーもつける

つぎに、縦スクロールバーをつけます。
オブジェクトインスペクタでプロパティタブをクリックし、 ScrollBarsプロパティの項目をssNone(スクロールバーなし)から ssVertical(縦スクロールバーつき)に変更すればOKです。 ログがかなりみやすくなりました。

さいごに、もう一箇所だけ修正します。

 実行時にメモが広がらない

実行中に、もっとログ画面(Memo1)を広げようとして、 フォームのサイズを大きくしても、ログ画面が大きくなってくれません。
これでは不便なので、フォームにあわせてMemo1のサイズを変えられるようにします。

 オブジェクトインスペクタに注目!

フォームにあわせてMemo1のサイズを変えられるようにするには、 Anchorsプロパティを変更します。
いま、Anchors=[akLeft,akTop]となっているとおもいます。
これをAnchors=[akLeft,akTop,akRight,akBottom]にかえます。

 実行時にメモが広がる!

これで実行中にログ画面を広げられるようになりました。 ログインだけできるクライアントのできあがりです。


ここまでのコードをのせておきます。


任意のコマンドを送れるクライアント

 任意のコマンドを送れるクライアント

ログインだけできるクライアントでは、任意のコマンドを受信して、Memo1に表示できました。 しかし、こちらから送信しているコマンドは2番コマンドだけでした。 実行時にコマンドを入力して、任意のコマンドを送れるクライアントをつくりたいです。 右の絵のようなのを目標にします。 あたらしい機能は、

これだけです。

カタチ

コマンド入力欄

 Memo2を貼り付け

右の絵のように、Memo1の下をあけて、そこにあたらしいMemoをはりつけてください。 Memo2という名前になるとおもいます。これをコマンド入力欄にします。

オブジェクトインスペクタで、Memo2のプロパティを、

に設定します。

自分で入力したコマンドをおくる

 OnKeyDownの項目をダブルクリックすると、かってに手続きができる

オブジェクトインスペクタで、Memo2のOnKeyDownイベントの項目をダブルクリックし、 対応する手続きMemo2KeyDownを自動的に宣言します。

 Memo2KeyDownの中身を自分でかく

中身をつぎのように書きます。

procedure TForm1.Memo2KeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
var
  str,data: String;
  id: Integer;
begin
  if Key<>VK_Return then exit;    //Enterキーを押したときだけ処理
  str:=TrimRight(Memo2.Text)+' '; //入力の右側に空白・制御文字があれば除去
  if Length(str)<2 then exit;     //空のメッセージは送信しない
  if str[1]<>'/' then exit;       //先頭が'/'の入力だけ処理
  id:=StrToIntDef(Copy(str,2,Pos(' ',str)-2),-1);       //strからidをとりだす
  data:=Copy(str,Pos(' ',str)+1,Length(str)-Pos(' ',str)-1); //dataをとりだす
  SendNapCommand(ClientSocket1.Socket,id,data);
  Memo2.Clear;
end;
 実行中!任意のコマンドが送れる!

実行して、いろいろなコマンドを送ってみてください。 たとえば400番コマンド(チャンネルに入室)を送ると、 右の絵のようにいくつかのコマンドが返ってきます。

任意のコマンドを送れるクライアントが完成しました。
IMのやりとりや管理用コマンドの送信にはこのクライアントで十分ですし、 チャンネルでのチャットやファイル参照も可能です。 検索はちょっときついかもしれませんが、できないことはありません。


ここまでのコードをのせておきます。


チャットできるクライアント

 チャットできるクライアント

任意のコマンドを送れるクライアントでもチャットはできますが、 あきらかに不便です。複数の部屋に入っていると、各部屋の会話が ひとつの画面の中で混ざって混乱するし、 だれが入室しているのかもわかりにくいです。
そこで、各部屋ごとにチャット窓をひらき、 チャンネルメッセージの入力・表示や入室メンバーの一覧ができるようにしたいです。 右の絵のようなのを目標にします。

あたらしい機能は、つぎのとおりです。

カタチ

PageControl

 ページコントロールをはる

タブをクリックしてページを切り替えられるコントロールです。 実行時にページを作成して、ページにチャット窓をはりつけるために使います。 PageControlコンポーネントはコンポーネントパレットのWin32タブにあります。 はりつけたオブジェクトは、PageControl1という名前になるはずです。

 TabSheet1に部品をはる

つぎに、オブジェクトツリーかフォーム上のPageControl1を右クリックして、 でてきたメニューから「ページ新規作成(W)」を選択してクリックします。 すると、PageControl1にあたらしいページが追加されます。 このページの名前はTabSheet1という名前になるはずです。 オブジェクトツリー上で、Memo1をTabSheet1にドラッグ&ドロップ してみてください。Memo1がTabSheet1の上に移動します。 同様にして、Memo2,Button1もTabSheet1の上に移動します。 PageControl1は、フォームいっぱいにひろげます。 PageControl1のAlignプロパティをalClientにしてください。 Memo1,Memo2,Button1も、てきとうに大きさや位置を調節します。

チャット窓

 新しいフレームを作る

PageControl1のページにはりつけるチャット窓です。 メニュー→ファイル→新規作成→フレームを選択してクリックし、 フレームを作成します。このフレームはひとつのユニットとして扱われます。 ユニット名はUnit2となっているはずです。

 フレームの名前を変更

作成したフレームの名前をChannelFrameに変更します。 このフレームの上に、チャット窓用のコンポーネントをいろいろのせます。

ListView1についてはつぎのセクションで説明します。

メンバー表示欄

 リストビューをはる

メンバー一覧を表示する欄です。ListViewコンポーネントを使います。 ListViewコンポーネントはコンポーネントパレットのWin32タブにあります。

 ListView1にカラムを追加する

オブジェクトツリーのListView1を右クリックし、「カラムの設定(U)...」をえらびます。 「ListView1.Columnsの編集」ウィンドウがでてくるので、 ここでカラムを3つ追加し、各カラムのCaptionを'Nick','Files','Line'とします。

これでチャット窓の形ができました。 つぎに、このチャット窓をよびだしたり、 メッセージを表示したりするところのコードを追加していきましょう。

チャット部屋に入室する

まず、チャット部屋に入室するあたりの処理を書きます。 405番(入室許可)コマンドをサーバーから受信したときに、 そのチャンネル名でチャット窓をひらいて手前に表示します。

受信したコマンドの処理

 RecvNapCommandの中身を書き足す

あとあとの拡張のため、受信したコマンドごとの処理をする手続きをひとつ作ります。 ProcessCommandという名前にしましょう。 引数はソケット、コマンド番号、コマンドの中身の3つでいいとおもいます。
RecvNapCommandの最後に次の一行を追加します。

    ProcessCommand(Socket,id,data);

ProcessCommandはあとで宣言・実装します。

チャット窓をひらく

 ProcessCommand手続きの宣言

ProcessCommandをつぎのように宣言します。

宣言

procedure ProcessCommand(Socket: TCustomWinSocket;
      id: Integer; data: String);
 Unit1はインターフェイス部でUnit2を使います、という宣言

つぎに実装します。このとき、さっき作成したChannelFrameをつかうので、 Unit1のinterface部のusesにUnit2を追加します。

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ScktComp, ComCtrls, Unit2;
 ProcessCommandの中身をかく

実装

procedure TForm1.ProcessCommand(Socket: TCustomWinSocket;
  id: Integer; data: String);
var
  room: TChannelFrame;
  sh: TTabSheet;
begin
  case id of
    405: //入室許可 <channel>
    begin
      sh:=TTabSheet.Create(PageControl1); //ページ(チャット窓)の新規作成
      sh.PageControl:=PageControl1;       //PageControl1にページを追加
      room:=TChannelFrame.Create(sh);     //チャンネルフレームの新規作成
      room.Parent:=sh;                    //ページにチャンネルフレームを貼り付け
      room.Align:=alClient;               //フレームをページいっぱいにひろげる
      sh.Caption:=data;                   //ページのCaptionをチャンネル名にする
      PageControl1.ActivePage:=sh;        //ページを前面に表示
    end;
  end;
end;
 実行中!チャンネル入室しているところ

実行します。コマンド入力欄から、チャンネル入室コマンドを送信してみてください。

 実行中!チャンネル窓が開く

入室許可コマンドが受信されると、PageControl1に新しいページが追加され、 アクティブになります。また、ページのタブの文字列には、 入力したチャンネル名が入ります。


ここまでのコードをのせておきます。


 実行中!400番への応答がずらずらと返ってくる

右の絵のように、400番をおくるといくつかのメッセージが返ってきます。

このほかに、チャンネル関係のコマンドとして

の2つもチャットに必要だとおもいます。 くわしくはnap.txtをみてください。

403,410,824番コマンドがきたら、チャット窓にメッセージを表示したいし、 406,407,408番コマンドがきたらチャット窓のメンバー表示欄でメンバーを追加・削除したいです。

そこで、各コマンドごとの処理を、順番にProcessCommandに書き足していきましょう。

メッセージの表示

まず、メッセージの表示です。

403番コマンドの<text>には、空白がふくまれることがあるので注意してください。

各コマンドごとの処理を書くまえに、ひとつ準備をします。 405番コマンドのときは、データに<channel>しか含みませんでしたが、 これらのコマンドは複数の項目をふくんでいます。 そのため、データを複数の項目にきりわけて、個別に利用できるようにしておきたいです。 そこで、つぎのようにします。

ProcessCommand手続きで、varのところにTStrings型の変数datasetを追加します。

procedure TForm1.ProcessCommand(Socket: TCustomWinSocket;
  id: Integer; data: String);
var
  room: TChannelFrame;
  sh: TTabSheet;
  dataset: TStrings;

そして、ProcessCommand手続きの中身をつぎのようにします。

begin
  dataset:=TStringList.Create;//dataset変数を作成する
  dataset.Delimiter:=' ';     //コマンドデータの区切り文字は半角空白
  dataset.DelimitedText:=data;//コマンドデータを区切ってdatasetに入れる
  try
    case id of
      (略)
    end;
  finally
    dataset.Free;//dataset変数を破棄する
  end;
end;

こうすると、受け取ったデータの各項目を、 dataset[0],dataset[1],...のように指定して呼び出すことができます。

それでは、各コマンドの処理を書きます。
ProcessCommandのcase id of...の中に、つぎのように書き足すことになります。 メッセージを表示するチャット窓をさがすための関数を、とりあえず FindChannelWindowとしておきます。チャンネル名を引数にとります。

     403: //チャンネルメッセージ
     //<channel> <nick> <text> (<text>は空白を含む)
     begin
       room:=FindChannelWindow(dataset[0]);
       if room=nil then exit;
       room.Memo1.Lines.Add('<'+dataset[1]+'> '
         +Copy(data,Length(dataset[0]+' '+dataset[1]+' ')+1,
         Length(data)-Length(dataset[0]+' '+dataset[1]+' ')));
     end;
     410: //チャンネルトピック
     //<channel> <topic>
     begin
       room:=FindChannelWindow(dataset[0]);
       if room=nil then exit;
       room.Memo1.Lines.Add(dataset[1]);
     end;
     824: //emote
     //<channel> <user> "<text>"
     begin
       room:=FindChannelWindow(dataset[0]);
       if room=nil then exit;
       room.Memo1.Lines.Add('* '+dataset[1]+' '+dataset[2]);
     end;

FindChannelWindowはつぎのように宣言・実装します。
宣言

    function FindChannelWindow(channel: String): TChannelFrame;

実装

function TForm1.FindChannelWindow(channel: String): TChannelFrame;
var
  i: Integer;
begin
  Result:=nil;
  with PageControl1 do
    for i:=0 to PageCount-1 do
      if Pages[i].Caption=channel then
      begin
        Result:=Pages[i].Components[0] as TChannelFrame;
        exit;
      end;
end;
 実行中!チャンネルメッセージがメッセージ表示欄に表示される

実行してみます。 これで、トピックやチャンネルメッセージをメッセージ表示欄に表示できるようになりました。 つぎに、メンバー表示欄でメンバーを表示・追加・削除できるようにしましょう。

入室メンバーの表示

入室メンバーの表示まわりのコードをかきます。

409番のための処理はとくにありません。 他のメンバーの入室時にはメンバーを表示欄に追加、 メンバーの退室時には削除したいです。 ProcessCommandのcase id of...の中に、つぎのように書き足すことになります。

      406: //入室メッセージ
      //<channel> <user> <sharing> <link-type>
      begin
        room:=FindChannelWindow(dataset[0]);
        if room=nil then exit;
        with room.ListView1.Items.Add do
        begin
          Caption:=dataset[1];
          SubItems.Add(dataset[2]);
          SubItems.Add(dataset[3]);
        end;
        room.Memo1.Lines.Add('+ '+dataset[1]+' ('+dataset[3]+') [sharing '
          +dataset[2]+' files] has joined.');
      end;
      407: //ユーザーがチャンネルから退室
      //<channel> <nick> <sharing> <linespeed>
      begin
        room:=FindChannelWindow(dataset[0]);
        if room=nil then exit;
        room.ListView1.FindCaption(0,dataset[1],false,false,true).Delete;
        room.Memo1.Lines.Add('- '+dataset[1]+' ('+dataset[3]+') [sharing '
          +dataset[2]+' files] has left.');
      end;
      408: //チャンネルユーザーの一覧の項目
      //<channel> <user> <sharing> <link-type>
      begin
        room:=FindChannelWindow(dataset[0]);
        if room=nil then exit;
        with room.ListView1.Items.Add do
        begin
          Caption:=dataset[1];
          SubItems.Add(dataset[2]);
          SubItems.Add(dataset[3]);
        end;
      end;
 実行中!チャンネルメンバーの一覧がリアルタイムで変化する

実行してみます。 これで、メンバー表示欄にメンバーの入室・退室を反映できるようになりました。 つぎに、チャット窓のメッセージ入力欄からメッセージを送れるようにしましょう。

メッセージを書き込む

チャット窓のメッセージ入力欄を実装します。 任意のコマンドを送れるクライアントのコマンド入力欄と、だいたいおなじですが、 入力した文字列の先頭が'/'じゃないときの処理を追加します。

入力メッセージの送信

 フォームの表示ダイアログをだす

メニュー→表示→フォームで「フォームの表示」ダイアログを出し、 ChannelFrameを選択し、OKボタンをおします。

 OnKeyDownの項目でダブルクリックすると、かってに手続きができる

つぎに、オブジェクトツリーでChannelFrameのMemo2を選択し、 オブジェクトインスペクタでMemo2のOnKeyDownイベントの欄をダブルクリックして、 TChannelFrame.Memo2KeyDownを自動的に作成します。

そして、つぎのように中身をかきます。 TForm1.Memo2KeyDownと似ていますが、 str[1]が'/'でないときはチャンネルメッセージとして送るようになっています。

procedure TChannelFrame.Memo2KeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
var
  str,data: String;
  id: Integer;
begin
  if Key<>VK_Return then exit;
  str:=TrimRight(Memo2.Text)+' ';
  if Length(str)<2 then exit;
  if str[1]<>'/' then
  begin
    Form1.SendNapCommand(Form1.ClientSocket1.Socket,403,
      (Self.Parent as TTabSheet).Caption+' '+str);
  end
  else
  begin
    id:=StrToIntDef(Copy(str,2,Pos(' ',str)-2),-1);
    data:=Copy(str,Pos(' ',str)+1,Length(str)-Pos(' ',str)-1);
    Form1.SendNapCommand(Form1.ClientSocket1.Socket,id,data);
  end;
  Memo2.Clear;
end;
 Unit2は実装部でUnit1をつかいます、という宣言

このコードでは、Form1を参照しようとしているため、実行する前に次のように implementation部のusesにUnit1を追加しておいてください。

implementation

{$R *.dfm}

uses Unit1;
 実行中!メッセージを送ってみる

実行してみます。メッセージ入力欄になにかことばをいれて、 Enterキーをおしてみてください。

 実行中!メッセージ送信成功!

チャット窓のメッセージ入力欄からメッセージをおくれました。 さいごに、退室のための処理をコードにします。

チャット部屋から退室する

退室のながれとしては、

というかんじです。

チャット窓をとじる

チャット窓をとじる部分は、すこしむずかしいかもしれません。 Button1Clickで直接

PageControl1.ActivePage.Free;

とか

(TChannelFrame(Self.Parent).Parent as TTabSheet).Free;

としても、 実行時に例外がでてうまくいきません。なぜかというと、 処理のながれは、チャット窓が閉じられるのを待ってから Button1Clickにもどってこようとするのですが、 チャット窓を閉じたときにButton1もなくなってしまっているので、 Button1Clickにもどってこれないからです。

これを解決するには、PostMessage関数をつかいます。 Button1ClickでPostMessageを使って、 PageControl1にウィンドウメッセージをとばし、 PageControl1にチャット窓をとじてもらいます。 PostMessageはチャット窓がとじられるまでまたずに 処理の流れをButton1Clickにもどすので、 例外をださずにチャット窓をとじれます。

 ウィンドウメッセージWM_ACTIVE_PAGE_FREEを宣言

具体的には、まず、Unit2.pasでウィンドウメッセージのIDを予約します。

Unit2.pas

const
  WM_ACTIVEPAGE_FREE=WM_USER+1;
 OnClickの項目をダブルクリックして手続きをつくる

つぎに、オブジェクトインスペクタでフレームChannelFrameのButton1のOnClickイベントの欄をダブルクリックし、対応する手続きButton1Clickを自動的に宣言・実装します。

そして、Button1Clickの中身をつぎのように書きます。 PostMessageでForm1にウィンドウメッセージWM_ACTIVEPAGE_FREEを送っています。

procedure TChannelFrame.Button1Click(Sender: TObject);
begin
  Form1.SendNapCommand(Form1.ClientSocket1.Socket,401,
    (Self.Parent as TTabSheet).Caption);
  PostMessage(Form1.Handle,WM_ACTIVEPAGE_FREE, 0, 0);
end;

ここまでできたら、つぎはForm1側がこのメッセージをうけとったときの動作を書きます。 Unit1.pasで、Form1がWM_ACTIVEPAGE_FREEメッセージをうけとったときの動作を WMActivePageFreeという手続きにして、つぎのように宣言・実装します。 宣言

procedure WMActivePageFree(var Message: TMessage); message WM_ACTIVEPAGE_FREE;

実装

procedure TForm1.WMActivePageFree;
begin
  PageControl1.ActivePage.Free; //表示しているページをとじる
end;
 実行中!チャンネルから退室するところ

実行してみます。 ひらいたチャット窓のButton1をおしてみてください。

 実行中!チャット窓クローズ成功!

退室コマンドを送り、チャット窓をとじることができました。


ここまでのコードをのせておきます。


しあげ

ここでは、チャットできるクライアントのしあげとして、つぎの2つのことをします。

スプリッタ

長い名前のユーザーがチャットにいるとき、メンバー表示欄の幅をひろげたいことがあります。 メッセージ表示欄とメンバー表示欄の境界を左右にずらせると便利ですね。 こういうことができるのがスプリッタです。

 Panel1に部品を移動

さきにパネルコントロールをはりつけ、そこにMemo1とListView1を移動します。 パネルコントロールはコンポーネントパレットのStandardタブにあります。 パネルコントロールの名前はPanel1となっているとおもいます。

Panel1のAlignプロパティをalTopにし、縦の幅をてきとうに調節したあと

とします。

Memo1はAlignプロパティをalClientに、 ListView1はAlignプロパティをalRightにします。

 チャンネルフレームにスプリッタをはる

つぎにスプリッタをはりつけます。 スプリッタコントロールは、コンポーネントパレットのAdditionalタブにあります。 はりつけたスプリッタは、Splitter1という名前になるとおもいます。 Panel1の上にうまくのっていなかったら、 オブジェクトツリーでSplitter1をPanel1にドラッグ&ドロップしてください。

Splitter1のAlignプロパティをalRight、Widthプロパティの値を3にします。

 実行中!Memo1とTreeView1の境目をスプリッタで移動できる

実行してみます。メッセージ表示欄とメンバー表示欄の境目を、 ドラッグ操作で左右に移動できるようになりました。

タブ順の入れ替え

ChannelFrameにMemo2やButton1をはりつけたとき、 もしMemo2をはりつける順番がButton1よりあとだと、 チャット窓を開いたときは、Button1にフォーカスがきているはずです。 たいていの場合、チャット窓を開いてからすることはメッセージの入力ですから、

という動きを毎回やることになり、面倒です。 これを、チャット窓を開いたときに、はじめからMemo2にフォーカスがくるようにすれば、

と、操作がかなりラクになります。

 Memo2のTabOrderプロパティを0にする

具体的にすることは、Memo2のTabOrderプロパティの値を0にするだけです。 これで、チャット窓のタブ順はMemo2が一番はじめになります。 もしすでにTabOrderプロパティが0なら、入室からメッセージ入力までスムーズに 行えているはずです。

 実行中!Memo2に自動でフォーカスがうつる

実行してみます。チャット窓をひらくと、メッセージ入力欄にフォーカスがきていて、 すぐにメッセージを入力できる状態になっています。

ついでに、クライアントの起動時から接続ボタンを押す動作、入室コマンドの送信までが 行いやすいようにしておきましょう。Form1について、以下のことをします。

これで、起動時にスペースキーで接続→Tabキーでコマンド入力欄にフォーカス→メッセージ入力という流れになって、とても操作しやすくなりました。

 実行中!チャットができる!

これで、チャットができるクライアントのできあがりです。


ここまでのコードをのせておきます。


〜便利な機能〜

終了確認メッセージ

サーバーを指定してログイン

ID・Passを変更してログイン

接続ボタンをおして切断

メタサーバーへの対応

設定をファイルに保存

ログをファイルに保存

タスクトレイにアイコンを表示

ステータスバーにサーバー統計を表示


onc001.png
onc002.png
onc003.png
onc004.png
onc005.png