[[バックログ>OpenNapClient/BackLog]]
&new{2006.01.31 (火) 04:35:43}; COMMENT{<jhghk> 2580}~
&new{2008-10-30 (木) 06:28:06}; COMMENT{<ASearch> http://memori.ru/goyahoos/ look more}~
//#comment

#comment

*Delphi6 PersonalでOpenNapクライアントをつくろう! [#va98ea92]
注意:いちおうサービスパック2まで入れておいたほうがいいかも

#contents

*ログインだけできるクライアント [#nece6dab]
#ref(029-Running-Memo1CanResize.png,right,around,50%, ログインだけできるクライアント)
こういうかんじのを、はじめの目標にします。~
-ボタンをおすと、サーバーに接続して、OpenNapのログイン要求メッセージをおくる
-ログイン許可応答など、サーバーからのOpenNapメッセージをログに表示する
#clear

#ref(001-Start.png,right,around,50%, 起動時の画面)
機能はこれだけです。
でははじめましょう。
#clear

#ref(002-SaveAs.png,right,around,50%, プロジェクトの保存)
起動したらすぐ適当な名前でプロジェクトを保存します。
#clear

**まず、カタチから [#ab267dbf]
#ref(d6025 - Memo and Button - Closeup.png,right,around,50%, 部品:メモとボタン)
実際の動作はあとまわしで、見た目だけ先につくってしまいます。
とりあえずいりそうなのは、
-サーバーに接続するボタン
-サーバーログを表示する場所

ぐらいかとおもいます。
#clear

***フォームに接続ボタンをはりつける [#qee69689]
#ref(003-AddButton.png,right,around,50%, ボタンの貼り付け)
サーバー接続ボタンをはりつけます。Buttonを選んではりつけてください。
Button1という名前になるはずです。
#clear

***ログ表示欄をはりつける [#k703c278]
#ref(004-AddMemo.png,right,around,50%, メモの貼り付け)
ログ表示欄をはりつけます。Memoを選んではりつけてください。
Memo1という名前になるはずです。
#clear
#ref(005-Run.png,right,around,50%, プログラムのコンパイルと実行)
これで見た目だけ完成です。F9を押すとかってに実行ファイルを生成して実行します。
サーバーを起動して接続ボタンをおしてみましょう。~
#clear
#ref(006-Running.png,right,around,50%, 実行中!・・・?)
サーバーに接続されません。
ボタンを押したときの動作をまだ書いてなかったので、しょうがないです。これからです。
#clear

**サーバーにつないでみる [#zdc42d34]
ボタンを押したときの動作を書きましょう。
ボタンを押したらサーバーに接続したいわけです。
サーバーに接続するにはどうしたらいいでしょうか?
つぎのような手順をふみます。
-接続したいサーバーのアドレスをしらべる
-接続したいサーバーのポートをしらべる
-しらべたアドレス・ポートに対してソケットを開く

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

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

***接続ボタンを押したときの動作をコードにする [#l5f2c583]
#ref(007-Button1DoubleClick.png,right,around,50%, ボタンをダブルクリックすると、かってに関数ができる)
フォームにある接続ボタン(Button1)をダブルクリックすると、
Button1Clickという関数が自動的に作成されます。
この関数は、Button1のOnButtonClickイベントが起こったときに実行されます。
このことは、左下のオブジェクトインスペクタで確認できます。
#clear
#ref(008-Button1Ivent-OnClick.png,right,around,50%, オブジェクトインスペクタに注目!)
オブジェクトインスペクタのイベントタブをクリックすると、
Button1のOnClickイベントのところにButton1Clickと書いてあります。
#clear
#ref(009-AddCodeToButton1Click.png,right,around,50%, Button1Click手続きの中身を自分でかく)
Button1Click関数の中身は、自分で書きます。
「サーバーに接続する」という内容のことを書けばいいです。
#clear
ここでは
 procedure TForm1.Button1Click(Sender: TObject);
 begin
   ConnectToServer;
 end;
のようにとりあえず書いておいて、
あとでこのConnectToServer関数を書きたすことにします。
#ref(010-DeclareConnectToNapServer.png,right,around,50%)
どうやって書き足すかというと、
#clear
先頭の宣言のところで
 procedure ConnectToNapServer;
のように宣言しておいて、
#ref(013-ImplimentConnectToNapServer-Complete.png,right,around,50%, ConnectToNapServer手続きの中身を自分でかく)
#clear
実装のところで
 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)に「接続できました」とか表示したいですね。

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

#ref(014-ClientSocket1-OnConnectDoubleClick.png,right,around,50%, OnConnectの項目をダブルクリックすると、かってに関数ができる)
左上のオブジェクトツリーでClientSocket1を選択し、左下のオブジェクトインスペクタの
イベントタブの、OnConnectの項目欄をダブルクリックすると、Delphiが
自動的にOnConnectイベントに対応する関数を宣言&実装してくれます。
#clear

#ref(015-ImplimentClientSocket1Connect.png,right,around,50%, ClientSocket1Connectの中身を自分でかく)
生成された関数に次のように書き足せば、サーバーlocalhost:8888に接続できたとき、
Memo1に「接続できました」と表示されます。
#clear
 procedure TForm1.ClientSocket1Connect(Sender: TObject;
   Socket: TCustomWinSocket);
 begin
   Memo1.Lines.Add('接続できました');
 end;

#ref(016-Running-Button1Clicked.png,right,around,50%, 実行中!)
F9を押してプログラムを実行してみてください。こんどは接続ボタンを押してサーバーに接続すると、ログ窓に'接続できました'と出たとおもいます。
プログラムを終了すれば自動的にサーバーから切断できます。
#clear

----
ここまでのコードをのせておきます。
#ref(Unit1.pas,left)
#ref(Unit1.dfm,left)
----

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

**サーバーにログイン [#j2cdbbed]
サーバーにログインするときは、つぎのような文字列をサーバーに送ります。~
(レイアウトのため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コマンドの送信 [#p5bc8292]
#ref(017-AddCodeToClientSocket1Connect.png,right,around,50%, ClientSocket1Connectの中身をすこし変える)
Napコマンドの送信部分は、SendNapCommandという名前の手続きにします。
引数にはソケット、コマンドID、送信データの3つをとると都合がいいです。
さっきつかったClientSocket1Connect関数を書き換えて、
SendNapCommandを実行するようにします。
#clear
 procedure TForm1.ClientSocket1Connect(Sender: TObject;
   Socket: TCustomWinSocket);
 begin
   Memo1.Lines.Add('接続できました');
   SendNapCommand(Socket,2,'user1 pass 6699 "NapChat" 8');//この行を追加
 end;

次にSendNapCommandの中身を書きます。
#ref(018-DeclareSendNapCommand.png,right,around,50%, SendNapCommand手続きを宣言)
#clear
 宣言
 procedure SendNapCommand(Socket: TCustomWinSocket;
   id: Integer; data: String);
#ref(019-ImplimentSendNapCommand.png,right,around,50%, SendNapCommand手続きの中身をかく)
#clear
 実装
 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コマンドの受信 [#x8985e5a]
#ref(020-ClientSocket1-OnReadDoubleClick.png,right,around,50%, OnReadの項目をダブルクリックすると、かってに手続きができる)
「許可応答を受信したら」「ログに表示」です。~
「許可応答を受信したら」の部分には、
TClientSocket.OnReadイベントを使います。~
オブジェクトインスペクタで、ClientSocket1のOnReadイベントの空欄を
ダブルクリックして、イベントに対応する手続きを自動的に生成します。
手続きの名前はClientSocket1Readという名前になったとおもいます。~
#clear
#ref(021-ImplimentClientSocket1Read.png,right,around,50%, ClientSocket1Readの中身をかく)
ソケットが受信した許可応答を取り出す関数をRecvNapCommand、
引数はソケットだけとして、~
ClientSocket1Read手続きの中に次のように書いておきます。
#clear
 procedure TForm1.ClientSocket1Read(Sender: TObject;
   Socket: TCustomWinSocket);
 begin
  RecvNapCommand(Socket);
 end;

#ref(022-DeclareRecvNapCommand.png,right,around,50%, RecvNapCommandを自分で宣言)
それからRecvNapCommandの中身をきめます。
#clear
 宣言
 procedure RecvNapCommand(Socket: TCustomWinSocket);
#ref(023-ImplimentRecvNapCommand.png,right,around,50%,RecvNapCommandを自分で実装)
#clear
 実装
 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を閉じたときに開放することにします。~
#ref(030-DisposeSocketData.png,right,around,50%, 受信用データのためのメモリは自分で解放する)
オブジェクトインスペクタでForm1のDestroyイベントの空欄をダブルクリックし、
Form1Destroy手続きを自動的に生成します。
そして、メモリを開放するためのコードをかきます。
#clear
 procedure TForm1.FormDestroy(Sender: TObject);
 var
   msg: PString;
 begin
   msg:=ClientSocket1.Socket.Data;
   if msg<>nil then
     Dispose(msg);//受信用に確保していたメモリを開放
 end;

#ref(024-Running-LoginSucceeded.png,right,around,50%, 実行中!)
F9を押して実行してみてください。Button1を押すと、
サーバーに接続して、なにかいろいろメッセージが返されてるのがわかるとおもいます。
#clear

**しあげ [#lfec09ae]
これでほとんどできあがりですが、ログ表示欄をもうすこしみやすくしたいですね。
もうすこしMemo1をひろげてみます。あと、縦スクロールバーもほしいです。
***ログ表示欄をみやすくする [#b5c225e1]
#ref(025-ModifyMemo1Appearence.png,right,around,50%, Memo1を広げる)
まず、ログのメッセージがみにくいので、Memo1のサイズを大きくします。
フォームでMemo1を選択すると、Memo1のふちのところに黒い点が8コついてるとおもいます。
これをドラッグすると、Memo1のサイズをかえることができます。
#clear

#ref(026-Running-Memo1Improved.png,right,around,50%, 縦スクロールバーもつける)
つぎに、縦スクロールバーをつけます。~
オブジェクトインスペクタでプロパティタブをクリックし、
ScrollBarsプロパティの項目をssNone(スクロールバーなし)から
ssVertical(縦スクロールバーつき)に変更すればOKです。
ログがかなりみやすくなりました。
#clear

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

#ref(027-Running-Memo1CantResize.png,right,around,50%, 実行時にメモが広がらない)
実行中に、もっとログ画面(Memo1)を広げようとして、
フォームのサイズを大きくしても、ログ画面が大きくなってくれません。~
これでは不便なので、フォームにあわせてMemo1のサイズを変えられるようにします。
#clear

#ref(028-ModifyMemo1Anchors.png,right,around,50%, オブジェクトインスペクタに注目!)
フォームにあわせてMemo1のサイズを変えられるようにするには、
Anchorsプロパティを変更します。~
いま、Anchors=[akLeft,akTop]となっているとおもいます。~
これをAnchors=[akLeft,akTop,akRight,akBottom]にかえます。
#clear

#ref(029-Running-Memo1CanResize.png,right,around,50%, 実行時にメモが広がる!)
これで実行中にログ画面を広げられるようになりました。
ログインだけできるクライアントのできあがりです。
#clear

----
ここまでのコードをのせておきます。
#ref(Unit2.pas,left)
#ref(Unit2.dfm,left)
----

*任意のコマンドを送れるクライアント [#f7948d6b]
#ref(034-Running-CanSendAnyCommand.png,right,around,50%, 任意のコマンドを送れるクライアント)
ログインだけできるクライアントでは、任意のコマンドを受信して、Memo1に表示できました。
しかし、こちらから送信しているコマンドは2番コマンドだけでした。
実行時にコマンドを入力して、任意のコマンドを送れるクライアントをつくりたいです。
右の絵のようなのを目標にします。
あたらしい機能は、
-コマンド入力欄で、"/<id> <data>"の形式でコマンドを入力し、Enterキーを押すと、そのコマンドをサーバーに送信する

これだけです。
#clear

**カタチ [#n403df86]
***コマンド入力欄 [#t6e3cb19]
#ref(031-AddMemo2.png,right,around,50%, Memo2を貼り付け)
右の絵のように、Memo1の下をあけて、そこにあたらしいMemoをはりつけてください。
Memo2という名前になるとおもいます。これをコマンド入力欄にします。
#clear
オブジェクトインスペクタで、Memo2のプロパティを、
-Anchors=[akLeft,akRight,akBottom]
-Lines=空(0行)
-WantReturns=False
-WordWrap=False

に設定します。

**自分で入力したコマンドをおくる [#sedc9810]
#ref(032-Memo2-OnKeyDownDoubleClick.png,right,around,50%, OnKeyDownの項目をダブルクリックすると、かってに手続きができる)
オブジェクトインスペクタで、Memo2のOnKeyDownイベントの項目をダブルクリックし、
対応する手続きMemo2KeyDownを自動的に宣言します。
#clear

#ref(033-ImplimentMemo2KeyDown.png,right,around,50%, Memo2KeyDownの中身を自分でかく)
中身をつぎのように書きます。
#clear
 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;

#ref(034-Running-CanSendAnyCommand.png,right,around,50%, 実行中!任意のコマンドが送れる!)
実行して、いろいろなコマンドを送ってみてください。
たとえば400番コマンド(チャンネルに入室)を送ると、
右の絵のようにいくつかのコマンドが返ってきます。

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

----
ここまでのコードをのせておきます。
#ref(Unit3.pas,left)
#ref(Unit3.dfm,left)
----

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

あたらしい機能は、つぎのとおりです。
-405番(入室許可)メッセージを受信すると、チャット窓をひらく
-チャット窓のメッセージ表示欄にトピック、メッセージ、emoteメッセージを表示する
-チャット窓のコマンド入力欄からチャンネルメッセージを送信できる
-チャット窓のリストビューに入室メンバーの一覧を表示する
-チャット窓の退室ボタンをおすと退室コマンドを送信し、チャット窓をとじる

**カタチ [#s61bae5d]
***PageControl [#vd3c486a]
#ref(035-AddPageControl1.png,right,around,50%, ページコントロールをはる)
タブをクリックしてページを切り替えられるコントロールです。
実行時にページを作成して、ページにチャット窓をはりつけるために使います。
PageControlコンポーネントはコンポーネントパレットのWin32タブにあります。
はりつけたオブジェクトは、PageControl1という名前になるはずです。
#clear

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

***チャット窓 [#j516f559]
#ref(037-CreateNewFrame.png,right,around,50%, 新しいフレームを作る)
PageControl1のページにはりつけるチャット窓です。
メニュー→ファイル→新規作成→フレームを選択してクリックし、
フレームを作成します。このフレームはひとつのユニットとして扱われます。
ユニット名はUnit2となっているはずです。
#clear

#ref(038-RenameFrame2ToChannelFrame.png,right,around,50%, フレームの名前を変更)
作成したフレームの名前をChannelFrameに変更します。
このフレームの上に、チャット窓用のコンポーネントをいろいろのせます。
#clear

-Memo1: メッセージ表示欄
--Anchors=[akLeft,akTop,akRight,akBottom]
--Lines=空(0行)
-Memo2: メッセージ入力欄
--Anchors=[akLeft,akRight,akBottom]
--Lines=空(0行)
--WantReturns=False
--WordWrap=False
-Button1: 退室ボタン
--Anchors=[akRight,akBottom]
-ListView1: メンバー表示欄
--Anchors=[akTop,akRight,akBottom]
--ViewStyle=vsReport

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

***メンバー表示欄 [#mf721329]
#ref(039-AddListView1.png,right,around,50%, リストビューをはる)
メンバー一覧を表示する欄です。ListViewコンポーネントを使います。
ListViewコンポーネントはコンポーネントパレットのWin32タブにあります。~
#clear

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

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

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

***受信したコマンドの処理 [#a54385de]
#ref(041-AddCodeToRecvNapCommand.png,right,around,50%, RecvNapCommandの中身を書き足す)
あとあとの拡張のため、受信したコマンドごとの処理をする手続きをひとつ作ります。
ProcessCommandという名前にしましょう。
引数はソケット、コマンド番号、コマンドの中身の3つでいいとおもいます。~
RecvNapCommandの最後に次の一行を追加します。
#clear
     ProcessCommand(Socket,id,data);

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

***チャット窓をひらく [#fe3689d5]
#ref(042-DeclareProcessCommand.png,right,around,50%, ProcessCommand手続きの宣言)
ProcessCommandをつぎのように宣言します。
#clear
宣言
 procedure ProcessCommand(Socket: TCustomWinSocket;
       id: Integer; data: String);

#ref(043-Unit1usesUnit2atInterface.png,right,around,50%, Unit1はインターフェイス部でUnit2を使います、という宣言)
つぎに実装します。このとき、さっき作成したChannelFrameをつかうので、
Unit1のinterface部のusesにUnit2を追加します。
#clear
 unit Unit1;
 
 interface
 
 uses
   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
   Dialogs, StdCtrls, ScktComp, ComCtrls, Unit2;

#ref(044-ImplimentProcessCommand.png,right,around,50%, ProcessCommandの中身をかく)
#clear
実装
 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;

#ref(045-Running-EnteringChannel.png,right,around,50%, 実行中!チャンネル入室しているところ)
実行します。コマンド入力欄から、チャンネル入室コマンドを送信してみてください。
#clear

#ref(046-Running-ChannelWindowActive.png,right,around,50%, 実行中!チャンネル窓が開く)
入室許可コマンドが受信されると、PageControl1に新しいページが追加され、
アクティブになります。また、ページのタブの文字列には、
入力したチャンネル名が入ります。
#clear

----
ここまでのコードをのせておきます。
#ref(Project1-4.zip,left)
----

#ref(047-Running-ReceivedCommandSequence.png,right,around,50%, 実行中!400番への応答がずらずらと返ってくる)
右の絵のように、400番をおくるといくつかのメッセージが返ってきます。
#clear
-403: チャンネルメッセージ: <channel> <nick> <text>
-405: 入室許可: <channel>
-408: チャンネルユーザーの一覧の項目: <channel> <user> <sharing> <link-type>
-409: チャンネルユーザーの一覧を終了: <channel>
-410: チャンネルトピック: <channel> <topic>
-824: emote: <channel> <user> "<text>"

このほかに、チャンネル関係のコマンドとして
-406: 入室メッセージ: <channel> <user> <sharing> <link-type>
-407:ユーザーがチャンネルから退室: <channel> <nick> <sharing> <linespeed>

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

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

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

***メッセージの表示 [#ud3af59a]
まず、メッセージの表示です。
-403: チャンネルメッセージ: <channel> <nick> <text>
-410: チャンネルトピック: <channel> <topic>
-824: emote: <channel> <user> "<text>"

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;

#ref(048-Running-CanShowChannelMessages.png,right,around,50%, 実行中!チャンネルメッセージがメッセージ表示欄に表示される)
実行してみます。
これで、トピックやチャンネルメッセージをメッセージ表示欄に表示できるようになりました。
つぎに、メンバー表示欄でメンバーを表示・追加・削除できるようにしましょう。
#clear

***入室メンバーの表示 [#g30cfe4b]
入室メンバーの表示まわりのコードをかきます。
-406: 入室メッセージ: <channel> <user> <sharing> <link-type>
-407:ユーザーがチャンネルから退室: <channel> <nick> <sharing> <linespeed>
-408: チャンネルユーザーの一覧の項目: <channel> <user> <sharing> <link-type>
-409: チャンネルユーザーの一覧を終了: <channel>

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;
#ref(049-Running-CanShowChannelMembers.png,right,around,50%, 実行中!チャンネルメンバーの一覧がリアルタイムで変化する)
実行してみます。
これで、メンバー表示欄にメンバーの入室・退室を反映できるようになりました。
つぎに、チャット窓のメッセージ入力欄からメッセージを送れるようにしましょう。
#clear


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

***入力メッセージの送信 [#b4c1f317]
#ref(050-ShowFormDialog.png,right,around,50%, フォームの表示ダイアログをだす)
メニュー→表示→フォームで「フォームの表示」ダイアログを出し、
ChannelFrameを選択し、OKボタンをおします。
#clear

#ref(051-TChannelFrame-Memo2-OnKeyDownDoubleClick.png,right,around,50%, OnKeyDownの項目でダブルクリックすると、かってに手続きができる)
つぎに、オブジェクトツリーでChannelFrameのMemo2を選択し、
オブジェクトインスペクタでMemo2のOnKeyDownイベントの欄をダブルクリックして、
TChannelFrame.Memo2KeyDownを自動的に作成します。
#clear

そして、つぎのように中身をかきます。
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;

#ref(052-Unit2usesUnit1atImplimentation.png,right,around,50%, Unit2は実装部でUnit1をつかいます、という宣言)
このコードでは、Form1を参照しようとしているため、実行する前に次のように
implementation部のusesにUnit1を追加しておいてください。
#clear
 implementation
 
 {$R *.dfm}
 
 uses Unit1;

#ref(053-Running-SendingChannelMessage.png,right,around,50%, 実行中!メッセージを送ってみる)
実行してみます。メッセージ入力欄になにかことばをいれて、
Enterキーをおしてみてください。
#clear

#ref(054-Running-CanSendChannelMessage.png,right,around,50%, 実行中!メッセージ送信成功!)
チャット窓のメッセージ入力欄からメッセージをおくれました。
さいごに、退室のための処理をコードにします。
#clear

**チャット部屋から退室する [#u898e373]
退室のながれとしては、
-チャット窓のButton1をおす
-サーバーに401番(退室)コマンドを送る
-チャット窓をとじる

というかんじです。

***チャット窓をとじる [#h32dc712]
チャット窓をとじる部分は、すこしむずかしいかもしれません。
Button1Clickで直接
 PageControl1.ActivePage.Free;
とか
 (TChannelFrame(Self.Parent).Parent as TTabSheet).Free;
としても、
実行時に例外がでてうまくいきません。なぜかというと、
処理のながれは、チャット窓が閉じられるのを待ってから
Button1Clickにもどってこようとするのですが、
チャット窓を閉じたときにButton1もなくなってしまっているので、
Button1Clickにもどってこれないからです。

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

#ref(055-DeclareConst-WM_ACTIVE_PAGE_FREE.png,right,around,50%, ウィンドウメッセージWM_ACTIVE_PAGE_FREEを宣言)
具体的には、まず、Unit2.pasでウィンドウメッセージのIDを予約します。
#clear
Unit2.pas
 const
   WM_ACTIVEPAGE_FREE=WM_USER+1;

#ref(056-TChannelFrame-Button1-OnClick.png,right,around,50%, OnClickの項目をダブルクリックして手続きをつくる)
つぎに、オブジェクトインスペクタでフレームChannelFrameのButton1のOnClickイベントの欄をダブルクリックし、対応する手続きButton1Clickを自動的に宣言・実装します。
#clear

そして、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;

#ref(057-Running-LeavingChannel.png,right,around,50%, 実行中!チャンネルから退室するところ)
実行してみます。
ひらいたチャット窓のButton1をおしてみてください。
#clear

#ref(058-Running-LeftChannel.png,right,around,50%, 実行中!チャット窓クローズ成功!)
退室コマンドを送り、チャット窓をとじることができました。
#clear

----
ここまでのコードをのせておきます。
#ref(Project1-5.zip,left)
----

**しあげ [#oba66aed]
ここでは、チャットできるクライアントのしあげとして、つぎの2つのことをします。
-メッセージ表示欄とメンバー表示欄の境界を実行時に移動できるようにする
-チャット窓のタブ順をいれかえて、入室したあとすぐにメッセージを入力できる状態にする

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

#ref(059-MoveComponentsToPanel1.png,right,around,50%, Panel1に部品を移動)
さきにパネルコントロールをはりつけ、そこにMemo1とListView1を移動します。
パネルコントロールはコンポーネントパレットのStandardタブにあります。
パネルコントロールの名前はPanel1となっているとおもいます。
#clear

Panel1のAlignプロパティをalTopにし、縦の幅をてきとうに調節したあと
-Anchors=[akLeft,akTop,akRight,akBottom]
-BevelOuter=bvNone

とします。

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

#ref(060-AddSplitterToChannelFrame.png,right,around,50%, チャンネルフレームにスプリッタをはる)
つぎにスプリッタをはりつけます。
スプリッタコントロールは、コンポーネントパレットのAdditionalタブにあります。
はりつけたスプリッタは、Splitter1という名前になるとおもいます。
Panel1の上にうまくのっていなかったら、
オブジェクトツリーでSplitter1をPanel1にドラッグ&ドロップしてください。

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

#ref(061-CanResizeMemo1andTreeView1.png,right,around,50%, 実行中!Memo1とTreeView1の境目をスプリッタで移動できる)
実行してみます。メッセージ表示欄とメンバー表示欄の境目を、
ドラッグ操作で左右に移動できるようになりました。
#clear

***タブ順の入れ替え [#w7308e62]
ChannelFrameにMemo2やButton1をはりつけたとき、
もしMemo2をはりつける順番がButton1よりあとだと、
チャット窓を開いたときは、Button1にフォーカスがきているはずです。
たいていの場合、チャット窓を開いてからすることはメッセージの入力ですから、
-チャット窓を開く→メッセージ入力欄(Memo2)をクリックする→メッセージ入力

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

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

#ref(062-ChannelFrame-Memo2-TabOrderZero.png,right,around,50%, Memo2のTabOrderプロパティを0にする)
具体的にすることは、Memo2のTabOrderプロパティの値を0にするだけです。
これで、チャット窓のタブ順はMemo2が一番はじめになります。
もしすでにTabOrderプロパティが0なら、入室からメッセージ入力までスムーズに
行えているはずです。
#clear

#ref(063-Running-Memo2-AutoSetFocus.png,right,around,50%, 実行中!Memo2に自動でフォーカスがうつる)
実行してみます。チャット窓をひらくと、メッセージ入力欄にフォーカスがきていて、
すぐにメッセージを入力できる状態になっています。
#clear

ついでに、クライアントの起動時から接続ボタンを押す動作、入室コマンドの送信までが
行いやすいようにしておきましょう。Form1について、以下のことをします。
-PageControl1のTabStopプロパティをFalseにする
-Button1のTabOrderプロパティを0にする
-Memo2のTabOrderプロパティを1にする

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

#ref(064-Running-CanChat.png,right,around,50%, 実行中!チャットができる!)
これで、チャットができるクライアントのできあがりです。
#clear

----
ここまでのコードをのせておきます。
#ref(Project1-6.zip,left)
----

*〜便利な機能〜 [#f31e0162]
**終了確認メッセージ [#jf623f94]
**サーバーを指定してログイン [#u0fd47a9]
**ID・Passを変更してログイン [#q2973ba0]
**接続ボタンをおして切断 [#ec44fab6]
**メタサーバーへの対応 [#g24844a6]
**設定をファイルに保存 [#ece6d143]
**ログをファイルに保存 [#w7f51716]
**タスクトレイにアイコンを表示 [#ieb576e6]
**ステータスバーにサーバー統計を表示 [#gf4d810d]

----
-サーバーとの間でやりとりするNapコマンドを記録
-管理者用検索機能
-管理者用whois/browse
-管理者用転送機能
-Banリストとユーザーリストをリストビューで表示
-その他管理者にとって都合のいい機能

#ref(onc001.png,right,around,50%)
#ref(onc002.png,right,around,50%)
#ref(onc003.png,right,around,50%)
#ref(onc004.png,right,around,50%)
#ref(onc005.png,right,around,50%)

#clear