第13章 コンボボックス |
13−1 コンボボックスをコードでつくる |
13−2 コンボボックスのラッパークラス |
13−3 ドライブコンボボックス |
13−4 まとめ |
コンボボックスコントロールは、CreateWindowEx( ) APIのクラス名に 'COMBOBOX' を指定して作られる。 他のコントロールとの違いは、 コントロールの高さには、リストボックスをドロップダウンするための領域を予め確保しておかなければならないことだ。windows.pas には、コンボボックスに指定できるウィンドウスタイルの値が以下のように定義されている。
const { Combo Box styles } CBS_SIMPLE = 1; //単純なコンボボックス。リストは常にドロップダウンされている。 CBS_DROPDOWN = 2; //CBS_SIMPLEで、リストはドロップダウンアイコンで表示する。 CBS_DROPDOWNLIST = 3; //CBS_DROPDOWNと同じであるが、 選択フィールドには入力不可。 CBS_OWNERDRAWFIXED = $10; //項目の高さが一定のオーナー描画。 CBS_OWNERDRAWVARIABLE = $20; //項目の高さが可変のオーナー描画。 CBS_AUTOHSCROLL = $40; //選択フィールドの水平スクロールを自動的に行う。 CBS_OEMCONVERT = $80; //入力文字をOEM文字セットに変換する。 CBS_SORT = $100; //リスト項目の文字列をソートする。 CBS_HASSTRINGS = $200; //文字列を保持する。 CBS_NOINTEGRALHEIGHT = $400; //項目の高さの途中でも表示できる。 CBS_DISABLENOSCROLL = $800; //スクロール不要なときに、スクロールバーを使用不可にする。 CBS_UPPERCASE = $2000; //入力文字を大文字に変換する。 CBS_LOWERCASE = $4000; //入力文字を小文字に変換する。
このスタイルを見ても分かるように、オーナー描画を除けば、コンボボックスにはシンプル、ドロップダウン、ドロップダウンリストの三つのスタイルがある。 選択フィールドに入力できない CBS_DROPDOWNLIST スタイルでは、選択フィールドはエディットコントロールではない。 VCLの TComboBox のデフォルトのインスタンスのクラス情報を見てみると、ExStyle がゼロであり、Style は $54010242 になっている。
デフォルトスタイルが $54010242 WS_CHILD = $40000000 WS_VISIBLE = $10000000 WS_CLIPSIBLINGS = $4000000 WS_TABSTOP = $10000 CBS_HASSTRINGS = $200 CBS_AUTOHSCROLL = $40 CBS_DROPDOWN = 2
これは、CBS_DROPDOWN 型のコンボボックスである。 スタイルの他のビットフィールドと異なり、最下位の2ビットはビットのオン・オフによるフラグではないことに気づいただろうか。 CBS_SIMPLE or CBS_DROPDOWN は CBS_DROPDOWNLIST と同じ値( = 3 )になる。 このことから、この3つのスタイルはそのうちのひとつだけを指定するものだ。 ここでは、この三つのスタイルのコンボボックスをコードで作ってテストしよう。 コンボボックスコントロールが認識できるメッセージは、Messages.pas に定義されている。
const { Combo Box messages } CB_GETEDITSEL = $0140; // エディットの選択領域の取得 CB_LIMITTEXT = $0141; // エディットに入力できる文字数の設定 CB_SETEDITSEL = $0142; // エディットの選択領域の設定 CB_ADDSTRING = $0143; // リストボックスへの文字列の追加 CB_DELETESTRING = $0144; // リストボックスの文字列の削除 CB_DIR = $0145; // リストボックスへのファイル一覧の設定 CB_GETCOUNT = $0146; // リストボックスの項目数の取得 CB_GETCURSEL = $0147; // リストボックスの選択項目インデックスの取得 CB_GETLBTEXT = $0148; // 任意リスト項目の文字列を取得 CB_GETLBTEXTLEN = $0149; // 任意リスト項目の文字列の長さを取得 CB_INSERTSTRING = $014A; // リストボックスへの文字列の挿入 CB_RESETCONTENT = $014B; // リストボックスとエディットの全内容をクリア CB_FINDSTRING = $014C; // リストボックス項目内の文字列検索 CB_SELECTSTRING = $014D; // 文字列を検索し見つかったら選択してエディットに表示 CB_SETCURSEL = $014E; // 特定項目を選択してエディットに表示 CB_SHOWDROPDOWN = $014F; // ドロップダウンリストの表示のスイッチ CB_GETITEMDATA = $0150; // 項目に関連づけられた32ビットデータの取得 CB_SETITEMDATA = $0151; // 項目に関連づける32ビットデータの設定 CB_GETDROPPEDCONTROLRECT = $0152; // ドロップダウンリストの座標を取得 CB_SETITEMHEIGHT = $0153; // 項目の高さの設定 CB_GETITEMHEIGHT = $0154; // 項目の高さの取得 CB_SETEXTENDEDUI = $0155; // 拡張UIの設定 CB_GETEXTENDEDUI = $0156; // 拡張UIの取得 CB_GETDROPPEDSTATE = $0157; // ドロップダウンリストの表示状況の取得 CB_FINDSTRINGEXACT = $0158; // リストボックス項目内の文字列検索 CB_SETLOCALE = 345; // ロケールの設定 CB_GETLOCALE = 346; // ロケールの取得 CB_GETTOPINDEX = 347; // リストボックスの可視領域の最初の項目インデックスの取得 CB_SETTOPINDEX = 348; // リストボックスの可視領域の最初の項目インデックスの設定 CB_GETHORIZONTALEXTENT = 349; // リストボックスの水平スクロール領域の取得 CB_SETHORIZONTALEXTENT = 350; // リストボックスの水平スクロール領域の設定 CB_GETDROPPEDWIDTH = 351; // ドロップダウンリストの横幅の取得 CB_SETDROPPEDWIDTH = 352; // ドロップダウンリストの横幅の設定 CB_INITSTORAGE = 353; // リストボックスのメモリの初期化 CB_MSGMAX = 354; // WM_SETTEXT = $000C; WM_GETTEXT = $000D; WM_GETTEXTLENGTH = $000E;
CB_ プリフィックス以外は、 リストボックスやエディットに送るメッセージとほぼ同じだ。 他のコントロールと同様、 コンボボックスもイベントを親ウィンドウのウィンドウ関数に WM_COMMAND メッセージとともに以下の通知コードを送って知らせる。
const CBN_ERRSPACE = (-1);//メモリ割り当てに失敗した。 CBN_SELCHANGE = 1; //リストの選択項目が変更された。 CBN_DBLCLK = 2; //CBS_SIMPLE のリスト項目がダブルクリックされた。 CBN_SETFOCUS = 3; //フォーカスを得ようとしている。 CBN_KILLFOCUS = 4; //フォーカスを失おうとしている。 CBN_EDITCHANGE = 5; //エディットのテキストが変更され、表示も更新された。 CBN_EDITUPDATE = 6; //エディットのテキストが変更され、表示は更新されていない。 CBN_DROPDOWN = 7; //リストがドロップダウンされようとしている。 CBN_CLOSEUP = 8; //リストが非表示にされた。 CBN_SELENDOK = 9; //リストの選択項目がエディットに受け入れられた。 CBN_SELENDCANCEL = 10;//選択項目がキャンセルされた。
以上の知識を使ってコードでコンボボックスを作ってみよう。
以下に ComboBox01.dpr の全リストを示す
program ComboBox01; uses windows, messages, UtilFunc in 'UtilFunc.pas', UtilClass in 'UtilClass.pas', GDIObject in 'GDIObject.pas', APIWindow in 'APIWindow.pas', APIControl in 'APIControl.pas'; //------------------------------------------------------------ // Global Types, Constants and Variables //------------------------------------------------------------ const SIMPLE = 600; DROPDOWN = 601; DROPDOWNLIST = 602; var MainWnd: TSDIMainWindow; hSimple,hDropDown,hDropDownList: HWND; L1,L2,L3: TAPILabel; //------------------------------------------------------------ // Helper Handlers //------------------------------------------------------------ function GetText(hComboBox: HWND): string; var s:string; Len: integer; begin SetLength(s,256); Len := SendMessage(hComboBox,WM_GETTEXT,256,integer(PChar(s))); SetLength(s,Len); result := s; end; function GetSelString(hComboBox: HWND): string; var SelIndex,Len: integer; s: string; begin SelIndex := SendMessage(hComboBox,CB_GETCURSEL,0,0); Len := SendMessage(hComboBox,CB_GETLBTEXTLEN, SelIndex,0); SetLength(s,Len); SendMessage(hComboBox,CB_GETLBTEXT, SelIndex, integer(PChar(s))); Result := s; end; //------------------------------------------------------------ // Object Event Handlers //------------------------------------------------------------ //------------------------------------------------------------ // Main Window Event Handlers //------------------------------------------------------------ procedure MainCreate(var Msg: TMessage); var hWindow: HWND; i: integer; s: string; begin hWindow := MainWnd.Handle; MakeInitMenu(hWindow); L1 := CreateLabel(hWindow,10,10,150,30,''); L1.Margin := 6; L2 := CreateLabel(hWindow,170,10,150,30,''); L2.Margin := 6; L3 := CreateLabel(hWindow,330,10,150,30,''); L3.Margin := 6; hSimple := CreateWindowEx(0,'COMBOBOX',nil, $54010240 or WS_VSCROLL or CBS_SIMPLE, 10,50,150,180, hWindow,SIMPLE, hInstance, nil); hDropDown := CreateWindowEx(0,'COMBOBOX',nil, $54010240 or WS_VSCROLL or CBS_DROPDOWN, 170,50,150,180, hWindow,DROPDOWN, hInstance, nil); hDropDownList := CreateWindowEx(0,'COMBOBOX',nil, $54010240 or WS_VSCROLL or CBS_DROPDOWNLIST, 330,50,150,180, hWindow,DROPDOWNLIST, hInstance, nil); for i := 1 to 12 do begin s := 'Item '+AIntToStr(i); SendMessage(hSimple,CB_ADDSTRING,0,integer(PChar(s))); SendMessage(hDropDown,CB_ADDSTRING,0,integer(PChar(s))); SendMessage(hDropDownList,CB_ADDSTRING,0,integer(PChar(s))); end; with MainWnd do begin ClientWidth := 490; ClientHeight := 240; Center; end; end; procedure OnTest(hWindow: HWND); var s: string; begin SendMessage(hSimple,CB_DELETESTRING,3,0); s := 'これが挿入文'; SendMessage(hDropDown,CB_INSERTSTRING,3,integer(PChar(s))); end; procedure MainCommand(var Msg: TMessage); begin Msg.Msg := MainWnd.Handle; InitMenuCommand(Msg,OnTest); if (Msg.LParam <> 0 ) then // from ComboBox case Msg.WParamLo of SIMPLE:begin case Msg.WParamHi of CBN_EDITCHANGE:L1.Text := GetText(hSimple); CBN_SELCHANGE:L1.Text := GetSelString(hSimple); end; end; DROPDOWN:begin case Msg.WParamHi of CBN_EDITCHANGE:L2.Text := GetText(hDropDown); CBN_SELCHANGE:L2.Text := GetSelString(hDropDown); end; end; DROPDOWNLIST:begin case Msg.WParamHi of CBN_SELCHANGE:L3.Text := GetText(hDropDownList); end; end; end; // case end; procedure MainDestroy(var Msg: TMessage); begin PostQuitMessage(0); end; //------------------------------------------------------------ // Main Procedure //------------------------------------------------------------ begin MainWnd := TSDIMainWindow.Create(0,GetExeName); with MainWnd do begin OnCreate := MainCreate; OnDestroy := MainDestroy; OnCommand := MainCommand; end; MainWnd.DoCreate; Halt(MessageLoopNormal); end.
実行すると右図のようになる。 三つのコンボボックスは左から、CBS_SIMPLE、CBS_DROPDOWN、CBS_DROPDOWNLIST のスタイルのものだ。 最初の二つは、エディットにキャレットを置いて任意の文字を入力できる。 エディットの文字列が変化するたびに上のラベルに変更された文字列が表示される。 CBS_DROPDOWNLIST のスタイルのコンボボックスはエディットに入力できない。リストから選択された文字列を表示するだけだ。 どのスタイルのコンボボックスもリストの選択を変更するたびに、エディットの表示が更新され、ラベルにも同じ文字列が表示されることが確かめられる。 Test! メニューを選択すると、左のコンボボックスのリストの4番目(ゼロベースのインデックスでは3)の項目が削除される。 さらに中央のコンボボックスのリストの4番目に新しい文字列が挿入される。
コードを見ていこう。MainCreate( ) ハンドラでは、 ラベルを三つ作ったあとで、CreateWindowEx( ) APIでコンボボックスを三つ作っている。 クラス名には、 Win32 で登録済みのウィンドウクラスである 'COMBOBOX' を指定している。 ウィンドウスタイルは、上記のVCLの値を参考にして $54010240 or WS_VSCROLL の後に、それぞれのコンボボックススタイルを追加している。 各コンボボックスのコントロールIDは、定数である SIMPLE、DROPDOWN、DROPDOWNLIST を割り当てている。ウィンドウハンドルは、対応するグローバル変数に格納される。 コンボボックスを作成したあとで各コントロールに CB_ADDSTRING メッセージを送って、12個の項目を追加している。
CB_ADDSTRING wParam = 0; // not used; must be zero lParam = (LPARAM) (LPCTSTR) lpsz; // address of string to add
戻り値には、追加された項目のリストにおけるゼロベースのインデックスが返される。エラーが起きると CB_ERR か CB_ERRSPACE が返される。 MainCommand( ) ハンドラでは、コンボボックスが親のウィンドウ関数に送ってきた WM_COMMAND を処理している。 コントロールからのメッセージであることを確認してから、 wParam の下位ワードを参照してコントロールIDごとに処理を分ける。 右側の CS_DROPDOWNLIST スタイルのコンボボックスはエディットを編集できないのでリストの選択項目が変更された通知である CBN_SELCHANGE だけをトラップしている。 ヘルパ関数 GetText( ) では、指定のコンボボックスに WM_GETTEXT メッセージを送って、エディットに表示される文字列を取得している。 ところが、CBS_SIMPLE と CBS_DROPDOWN のスタイルを持つコンボボックスでは、 CBN_SELCHANGE や CBN_SELENDOK の通知で、GetText( ) で文字列を取得すると直前にエディットに表示されていた文字列が得られるので、望みの文字列が得られない。 ここでは、ヘルパ関数 GetSelString( ) で CB_GETLBTEXT を送ってリストで選択されている項目の文字列を取得している。
CB_GETLBTEXT wParam = (WPARAM) index; // item index lParam = (LPARAM) (LPCSTR) lpszBuffer; // address of buffer
戻り値は、コピーされた文字列の長さをバイト単位で表したものだ。 ただし、ヌルターミネイトの分は含まれていない。 現在選択されている項目のインデックスは CB_GETCURSEL メッセージで取得する。
CB_GETCURSEL wParam = 0; // not used; must be zero lParam = 0; // not used; must be zero
項目の文字列の長さは、CB_GETLBTEXTLEN メッセージを送って取得する。
CB_GETLBTEXTLEN wParam = (WPARAM) index; // item index lParam = 0;
以上の三つのメッセージを送って、選択項目のインデックスと文字列の長さを取得してバッファを適切に用意してから、選択項目の文字列を取得している。 選択項目が変化してエディットの表示が更新されても CBN_EDITCHANGE メッセージを送ってこないので、CBN_SELCHANGE をトラップする必要がある。 CBN_EDITCHANGE の通知は、 キャレットを置いてキー入力により文字列が変更されたときに送られる。 このとき WM_GETTEXT を送って取得される文字列は、 エディットの表示が正しく反映されたものだ。 Test! メニューで実行されるリスト項目への挿入・削除には、CB_INSERTSTRING と CB_DELETESTRING を送ることで実現する。
CB_INSERTSTRING wParam = (WPARAM) index; // item index lParam = (LPARAM) (LPCTSTR) lpsz; // address of string to insert CB_DELETESTRING wParam = (WPARAM) index; // item to delete lParam = 0;
どちらの index にも挿入または削除する位置を示すゼロベースの項目インデックスを指定する。 上述の CB_ADDSTRING メッセージとあわせて、これで項目の追加・削除・挿入の仕方が分かった。 通知メッセージのタイミングにあわせて、エディットの文字列やリストの選択項目の文字列の取得の仕方も分かった。
前節でテストしたコンボボックスコントロールのラッパークラスを作ろう。 まず最初に、三つのスタイルに共通の性質をもつ TAPIComboBoxBase クラスを作成し、エディットの編集入力ができないのでイベントの数が一つ少ない CBS_DROPDOWNLIST スタイルのコンボボックスのラッパークラス TAPIDDLCombo クラスをこの TAPIComboBoxBase から派生して作る。 つぎに、CBS_SIMPLE スタイルのコンボボックスに相当するクラス TAPISMPCombo も TAPIComboBoxBase から派生し、さらにこの派生クラスから CBS_DROPDOWN スタイルのコンボボックスのラッパークラス TAPIDDCombo 派生して作ることにしよう。
まず最初に TAPIControl.pas に三つのクラスを実装しよう。宣言は以下のようになる。
//--------- TAPIComboBoxBase -------------------------------------------- TAPIComboBoxBase = class(TAPIControl) private FResultText: string; FOnSelChange: TNotifyMessage; function GetSelText:string; function GetSelIndex:integer; procedure SetSelIndex(index:integer); function GetCount: integer; function GetItemData(index:integer):integer; procedure SetItemData(index,value: integer); protected public function AddString(value: string):integer; procedure DeleteString(index:integer); function InsertString(value:string;index:integer):integer; function GetText(index: integer):string; procedure Clear; property SelText: string read GetSelText; property ResultText: string read FResultText; property SelIndex: integer read GetSelIndex write SetSelIndex; property Count: integer read GetCount; property ItemData[index:integer]:integer read GetItemData write SetItemData; property OnSelChange: TNotifyMessage read FOnSelChange write FOnSelChange; end; //--------- TAPIDDLCombo -------------------------------------------- TAPIDDLCombo = class(TAPIComboBoxBase) private protected procedure APICreateParams(var Params: TAPICreateParams); override; public procedure DefaultHandler(var Message); override; procedure ShowDropDown(value:Boolean); end; //--------- TAPISMPCombo -------------------------------------------- TAPISMPCombo = class(TAPIComboBoxBase) private FOnEditChange: TNotifyMessage; function GetEditText:string; procedure SetEditText(value:string); protected procedure APICreateParams(var Params: TAPICreateParams); override; public procedure DefaultHandler(var Message); override; property EditText: string read GetEditText write SetEditText; property OnEditChange: TNotifyMessage read FOnEditChange write FOnEditChange; end; //---------- TAPIDDCombo -------------------------------------------- TAPIDDCombo = class(TAPISMPCombo) protected procedure APICreateParams(var Params: TAPICreateParams); override; public procedure ShowDropDown(value:Boolean); end;
TAPIComboBoxBase クラスでは、コンボボックスのほとんどの性質をカプセル化しているが、上にあげたコンボボックス特有のメッセージのうち無視しているものもある。 リストボックスでの文字列項目の追加・挿入・削除・全削除の他に、任意項目の文字列を得る GetText( ) や項目数を得る Count プロパティーや選択文字列や項目インデックスを取得するプロパティーもある。 ItemData プロパティーは、リスト項目に任意の32ビット値を設定取得できる。 これから派生する三つの具体クラスは、ウィンドウスタイルを設定する APICreateParams( ) と WM_COMMAND メッセージを捕まえてイベントハンドラを呼び出す DefaultHandler( ) 以外に2,3のプロパティーやメソッドを追加するだけでできあがる。 ResutText プロパティーは、リストボックスの選択項目の変化やエディットの内容の変化のいずれにも、結果として得られる文字列を保持している。
実装部を以下に示す。
//--------- TAPIComboBoxBase -------------------------------------------- function TAPIComboBoxBase.AddString(value: string):integer; begin result := SendMessage(FHandle,CB_ADDSTRING,0,integer(PChar(value))); end; procedure TAPIComboBoxBase.DeleteString(index:integer); begin SendMessage(FHandle,CB_DELETESTRING,index,0); end; function TAPIComboBoxBase.InsertString(value:string;index:integer):integer; begin result := SendMessage(FHandle,CB_INSERTSTRING,index,integer(PChar(value))); end; function TAPIComboBoxBase.GetSelText:string; begin Result := GetText(SelIndex); end; function TAPIComboBoxBase.GetSelIndex:integer; begin result := SendMessage(FHandle,CB_GETCURSEL,0,0); end; procedure TAPIComboBoxBase.SetSelIndex(index:integer); var AMsg: TMessage; begin if index = SelIndex then exit; SendMessage(FHandle,CB_SETCURSEL,index,0); AMsg.Msg := WM_COMMAND; AMsg.WParamHi := CBN_SELCHANGE; AMsg.WParamLo := FID; AMsg.LParam := FHandle; Dispatch(AMsg); end; function TAPIComboBoxBase.GetText(index: integer):string; var s:string; Len: integer; begin Len := SendMessage(FHandle,CB_GETLBTEXTLEN,index,0); SetLength(s,Len); SendMessage(FHandle,CB_GETLBTEXT,index,integer(PChar(s))); result := s; end; procedure TAPIComboBoxBase.Clear; begin SendMessage(FHandle,CB_RESETCONTENT,0,0); end; function TAPIComboBoxBase.GetCount:integer; begin result := SendMessage(FHandle,CB_GETCOUNT,0,0); end; function TAPIComboBoxBase.GetItemData(index:integer):integer; begin result := SendMessage(FHandle,CB_GETITEMDATA,index,0); end; procedure TAPIComboBoxBase.SetItemData(index,value: integer); begin SendMessage(FHandle,CB_SETITEMDATA,index,value); end; //--------- TAPIDDLCombo -------------------------------------------- procedure TAPIDDLCombo.APICreateParams(var Params: TAPICreateParams); begin with Params do begin Caption := ''; Style := $54010240 or WS_VSCROLL or CBS_DROPDOWNLIST; ExStyle := WS_EX_CLIENTEDGE; X := 50; Y := 50; Width := 150; Height := 32; WinClassName := 'COMBOBOX'; end; end; procedure TAPIDDLCombo.DefaultHandler(var Message); var AMsg: TMessage; begin AMsg := TMessage(message); case AMsg.Msg of WM_COMMAND:begin if (AMsg.LParam <> LPARAM(FHandle)) then exit; case AMsg.WParamHi of CBN_SELCHANGE: begin AMsg.Msg := UINT(self); FResultText := SelText; if Assigned(FOnSelChange) then OnSelChange(AMsg); end; end; if AMsg.WParamHi = WORD(CBN_ERRSPACE) then MessageBox(Parent,'ComboBox Control Buffer Full!','Warning',MB_OK); end; end; // case end; procedure TAPIDDLCombo.ShowDropDown(value:Boolean); begin if value then SendMessage(FHandle,CB_SHOWDROPDOWN,1,0) else SendMessage(FHandle,CB_SHOWDROPDOWN,0,0); end; //--------- TAPISMPCombo -------------------------------------------- procedure TAPISMPCombo.APICreateParams(var Params: TAPICreateParams); begin with Params do begin Caption := ''; Style := $54010240 or WS_VSCROLL or CBS_SIMPLE; ExStyle := WS_EX_CLIENTEDGE; X := 50; Y := 50; Width := 150; Height := 32; WinClassName := 'COMBOBOX'; end; end; procedure TAPISMPCombo.DefaultHandler(var Message); var AMsg: TMessage; begin AMsg := TMessage(message); case AMsg.Msg of WM_COMMAND:begin if (AMsg.LParam <> LPARAM(FHandle)) then exit; case AMsg.WParamHi of CBN_EDITCHANGE: begin AMsg.Msg := UINT(self); FResultText := EditText; if Assigned(FOnEditChange) then OnEditChange(AMsg); end; CBN_SELCHANGE: begin AMsg.Msg := UINT(self); FResultText := SelText; if Assigned(FOnSelChange) then OnSelChange(AMsg); end; end; if AMsg.WParamHi = WORD(CBN_ERRSPACE) then MessageBox(Parent,'ComboBox Control Buffer Full!','Warning',MB_OK); end; end; // case end; function TAPISMPCombo.GetEditText:string; var s:string; Len: integer; begin SetLength(s,256); Len := SendMessage(FHandle,WM_GETTEXT,256,integer(PChar(s))); SetLength(s,Len); result := s; end; procedure TAPISMPCombo.SetEditText(value:string); var AMsg: TMessage; begin SetWindowText(FHandle,PChar(value)); AMsg.Msg := WM_COMMAND; AMsg.WParamHi := CBN_EDITCHANGE; AMsg.WParamLo := FID; AMsg.LParam := FHandle; Dispatch(AMsg); end; //---------- TAPIDDCombo -------------------------------------------- procedure TAPIDDCombo.APICreateParams(var Params: TAPICreateParams); begin inherited APICreateParams(Params); Params.Style := $54010240 or WS_VSCROLL or CBS_DROPDOWN; end; procedure TAPIDDCombo.ShowDropDown(value:Boolean); begin if value then SendMessage(FHandle,CB_SHOWDROPDOWN,1,0) else SendMessage(FHandle,CB_SHOWDROPDOWN,0,0); end;
これらのコードは、前節で説明した事を参照すれば容易に理解できるだろう。いずれもイベントハンドラを呼び出す前に、ResultText が正しい文字列を保持するようにしている。
選択項目の初期値を設定するには、 SelIndex プロパティーに希望する項目のインデックスを設定する。 SelIndex プロパティーの write アクセスメソッド SetSelIndex( ) では、CB_SETCURSEL メッセージを送って選択項目がコードで変更されるので、ここからも DefaultHandler( ) にメッセージを送って OnSelChange イベントを呼び出している。
CB_SETCURSEL wParam = (WPARAM) index; // item index lParam = 0;
GetText( ) メソッドを使うと、任意のインデックスの文字列を取得できる。 Clear メソッドでは、 CB_RESETCONTENT メッセージを送ってリストの全項目とエディットの文字列を削除する。
CB_RESETCONTENT wParam = 0; // not used; must be zero lParam = 0; // not used; must be zero
ShowDropDown メソッドを実行すると、CB_SHOWDROPDOWN を送ってコードによりドロップダウンリストを表示することができる。
CB_SHOWDROPDOWN wParam = (WPARAM) (BOOL) fShow; // the show/hide flag lParam = 0; // not used; must be zero
fShow にゼロ以外を設定するとリストが表示され、ゼロを設定するとリストが閉じられる。 リストボックスの項目への任意の32ビット値の取得・設定は以下のメッセージを使って行う。
CB_GETITEMDATA wParam = (WPARAM) index; // item index lParam = 0; // not used; must be zero CB_SETITEMDATA wParam = (WPARAM) index; // item index lParam = (LPARAM) (DWORD) dwData; // item data
いずれも wParam に設定・取得する項目のインデックスを設定する。 最後に、これらのクラスのインスタンスを作成する関数を APIControl.pas につくろう。宣言部を以下に示す。
function CreateDDLCombo(hParent: HWND; x,y,cx,cy: integer; sOnSelChange:TNotifyMessage): TAPIDDLCombo; function CreateSMPCombo(hParent: HWND; x,y,cx,cy: integer; sOnSelChange:TNotifyMessage): TAPISMPCombo; function CreateDDCombo(hParent: HWND; x,y,cx,cy: integer; sOnSelChange:TNotifyMessage): TAPIDDCombo;
実装部を以下に示す。
function CreateDDLCombo(hParent: HWND; x,y,cx,cy: integer; sOnSelChange:TNotifyMessage): TAPIDDLCombo; begin result := TAPIDDLCombo.Create(hParent); with result do begin Dimension := SetDim(x,y,cx,cy); OnSelChange := sOnSelChange; end; end; function CreateSMPCombo(hParent: HWND; x,y,cx,cy: integer; sOnSelChange:TNotifyMessage): TAPISMPCombo; begin result := TAPISMPCombo.Create(hParent); with result do begin Dimension := SetDim(x,y,cx,cy); OnSelChange := sOnSelChange; end; end; function CreateDDCombo(hParent: HWND; x,y,cx,cy: integer; sOnSelChange:TNotifyMessage): TAPIDDCombo; begin result := TAPIDDCombo.Create(hParent); with result do begin Dimension := SetDim(x,y,cx,cy); OnSelChange := sOnSelChange; end; end;
つぎに ComboBox02.dpr にこれらのインスタンスを作ってテストするプログラムを書こう。 以下に全リストを示す。
program ComboBox02; uses windows, messages, UtilFunc in 'UtilFunc.pas', UtilClass in 'UtilClass.pas', GDIObject in 'GDIObject.pas', APIWindow in 'APIWindow.pas', APIControl in 'APIControl.pas'; //------------------------------------------------------------ // Global Types, Constants and Variables //------------------------------------------------------------ var MainWnd: TSDIMainWindow; DDLCombo: TAPIDDLCombo; SMPCombo: TAPISMPCombo; DDCombo: TAPIDDCombo; L1,L2,L3: TAPILabel; //------------------------------------------------------------ // Helper Handlers //------------------------------------------------------------ //------------------------------------------------------------ // Object Event Handlers //------------------------------------------------------------ procedure SMPSelChange(var AMsg: TMessage); begin L1.Text := SMPCombo.ResultText; end; procedure DDSelChange(var AMsg: TMessage); begin L2.Text := DDCombo.ResultText; end; procedure DDLSelChange(var AMsg: TMessage); begin L3.Color := DDLCombo.ItemData[DDLCombo.SelIndex]; L3.Text := DDLCombo.ResultText; end; //------------------------------------------------------------ // Main Window Event Handlers //------------------------------------------------------------ procedure MainCreate(var Msg: TMessage); var hWindow: HWND; i: integer; s: string; begin hWindow := MainWnd.Handle; MakeInitMenu(hWindow); L1 := CreateLabel(hWindow,10,10,150,30,''); L1.Margin := 6; L2 := CreateLabel(hWindow,170,10,150,30,''); L2.Margin := 6; L3 := CreateLabel(hWindow,330,10,150,30,''); L3.Margin := 6; SMPCombo := CreateSMPCombo(hWindow,10,50,150,180,SMPSelChange); SMPCombo.OnEditChange := SMPSelChange; DDCombo := CreateDDCombo(hWindow,170,50,150,180,DDSelChange); DDCombo.OnEditChange := DDSelChange; DDLCombo := CreateDDLCombo(hWindow,330,50,150,180,DDLSelChange); for i := 1 to 12 do begin s := 'Item '+AIntToStr(i); SMPCombo.AddString(s); DDCombo.AddString(s); end; SMPCombo.SelIndex := 3; DDCombo.SelIndex := 3; with DDLCombo do begin ItemData[AddString('Lime')] := clrLIme; ItemData[AddString('Yellow')] := clrYellow; ItemData[AddString('Aqua')] := clrAqua; ItemData[AddString('Fuchsia')] := clrFuchsia; end; DDLCombo.SelIndex := 0; with MainWnd do begin ClientWidth := 490; ClientHeight := 240; Center; end; end; procedure OnTest(hWindow: HWND); begin DDCombo.ShowDropDown(true); end; procedure MainCommand(var Msg: TMessage); begin Msg.Msg := MainWnd.Handle; InitMenuCommand(Msg,OnTest); end; procedure MainDestroy(var Msg: TMessage); begin PostQuitMessage(0); end; //------------------------------------------------------------ // Main Procedure //------------------------------------------------------------ begin MainWnd := TSDIMainWindow.Create(0,GetExeName); with MainWnd do begin OnCreate := MainCreate; OnDestroy := MainDestroy; OnCommand := MainCommand; end; MainWnd.DoCreate; Halt(MessageLoopNormal); end.
実行すると右図のようになる。 MainCreate( ) ハンドラでは、三つのラベルと三種類のコンボボックスを作成している。 前節のコードベースの場合と異なり、 リストボックスの選択状態の変化とエディットの変化の両方のイベントで ResultText を参照できるので同じハンドラで処理できる。 右側のドロップダウンリストのコンボボックスでは、項目の文字列に色を設定し、対応する COLORREF 型の値を ItemData プロパティーを通して設定している。 選択項目の変化に対するイベントハンドラを以下に示す。
procedure DDLSelChange(var AMsg: TMessage); begin L3.Color := DDLCombo.ItemData[DDLCombo.SelIndex]; L3.Text := DDLCombo.ResultText; end;
このように、選択項目のインデックスを取得して、それからそれに関連づけられている32ビットデータをラベルの色に設定している。
コンボボックスの特徴は、リストボックスより配置にスペースをとらないことであり、リストが保持している文字列の一覧から簡単に一つだけを選べることだ。このコントロールの応用として、デフォルトでシステムにインストールされているディスクドライブの一覧をリストに表示するドライブコンボボックスのクラス TAPIDriveCombo を作ろう。 また、ドライブの一覧から一つ選ぶと、このクラスのプロパティーを通じて、そのドライブのラベルや容量、未使用容量などの情報をアクセスできるようにしよう。ドライブの一覧を得るためのAPIは GetDriveType( ) だ。
function GetDriveType(lpRootPathName: PChar): UINT; stdcall;
このAPIの唯一のパラメータには、ドライブのルートディレクトリを指定する文字列を指定する。例えば、
var s:string; ret:integer;
として
s := 'C:\'; ret := GetDriveType(PChar(s));
とする。このAPIの戻り値としては以下のような定数が windows.pas に宣言されている。
const DRIVE_UNKNOWN = 0; //ドライブタイプを決定できない DRIVE_NO_ROOT_DIR = 1; //このルートディレクトリは存在しない DRIVE_REMOVABLE = 2; //リムーバブルドライブである DRIVE_FIXED = 3; //ハードディスクである DRIVE_REMOTE = 4; //ネットワークドライブである DRIVE_CDROM = 5; //CD-ROMドライブである DRIVE_RAMDISK = 6; //ラムディスクである
システムに、対応するドライブがインストールされていないときは、1以下の値を返すのでこのときはコンボボックスのリストに加えない。 インストールされているドライブが分かったら、各ドライブの情報にアクセスするために、 GetVolumeInformation( ) と GetDiskFreeSpace( ) APIを使う。
function GetVolumeInformation(lpRootPathName: PChar; lpVolumeNameBuffer: PChar; nVolumeNameSize: DWORD; lpVolumeSerialNumber: PDWORD; var lpMaximumComponentLength, lpFileSystemFlags: DWORD; lpFileSystemNameBuffer: PChar; nFileSystemNameSize: DWORD): BOOL; stdcall;
lpRootPathName には、GetDriveType( )のパラメータと同じように 'C:\'などと指定する。 lpVolumeNameBuffer には、名前を受け取るバッファを指定する。 nVolumeNameSize には、受け取るバッファのサイズをバイト単位で指定する。 lpVolumeSerialNumber にはシリアル番号を受け取る DWORD 型変数のアドレスを指定する。 lpMaximumComponentLength はファイルシステムのパス名のうち \ 記号で挟まれている部分で使える最長の名前の大きさをバイト単位で受け取るために DWORD 型の変数を指定する。長いファイル名をサポートしているシステムでは、255 を受け取る。 lpFileSystemFlags には、以下の定数の組み合わせが返される。
FS_CASE_IS_PRESERVED = FILE_CASE_PRESERVED_NAMES;//大文字・小文字を予約ずみ FS_CASE_SENSITIVE = FILE_CASE_SENSITIVE_SEARCH; //大文字・小文字を区別する FS_UNICODE_STORED_ON_DISK = FILE_UNICODE_ON_DISK;//ファイル名にユニコードをサポート FS_PERSISTENT_ACLS = FILE_PERSISTENT_ACLS; //ACLSを使用(NTなど) FS_VOL_IS_COMPRESSED = FILE_VOLUME_IS_COMPRESSED;//圧縮されている FS_FILE_COMPRESSION = FILE_FILE_COMPRESSION; //ファイルベースの圧縮をサポート FILE_CASE_SENSITIVE_SEARCH = $00000001; FILE_CASE_PRESERVED_NAMES = $00000002; FILE_UNICODE_ON_DISK = $00000004; FILE_PERSISTENT_ACLS = $00000008; FILE_FILE_COMPRESSION = $00000010; FILE_VOLUME_IS_COMPRESSED = $00008000;
lpFileSystemNameBuffer は、そのボリュウムのファイルシステムの名前が返される。 筆者のシステムでは、ハードディスクは FAT、CD-ROM では CDFS が返ってきた。 nFileSystemNameSize は、システム名を受け取るバッファのサイズをバイト単位で指定する。ディスクの使用状況は GetDiskFreeSpace( ) APIで取得する。
function GetDiskFreeSpace(lpRootPathName: PChar; var lpSectorsPerCluster, lpBytesPerSector, lpNumberOfFreeClusters, lpTotalNumberOfClusters: DWORD): BOOL; stdcall;
lpRootPathName には、GetDriveType( ) のパラメータと同じように 'C:\'などと指定する。 lpSectorsPerCluster と lpBytesPerSector は、クラスタあたりのセクタ数とセクタあたりのバイト数 lpNumberOfFreeClusters と lpTotalNumberOfClusters は、未使用クラスタ数と前クラスタ数を受け取る DWORD 型の変数を指定する。
以上の知識を使って、 TAPIDriveCombo クラスを実装してテストするプログラムを作ろう。
まず最初に TAPIControl.pas に TAPIDriveCombo クラスを実装しよう。宣言は以下のようになる。
//---------- TAPIDriveCombo ----------------------------------------- TAPIDriveCombo = class(TAPIDDLCombo) private FRootDir: string; FVolumeName: string; FFileSystem: string; FSerialNumber: integer; FDiskSpace: integer; FUsedSpace: integer; FFreeSpace: integer; protected procedure Initialize; override; function GetVolumeName(Root: string):string; public procedure DefaultHandler(var Message); override; procedure GetVolumeInfo(Root: string); property RootDir: string read FRootDir; property VolumeName: string read FVolumeName; property FileSystem: string read FFileSystem; property SerialNumber: integer read FSerialNumber; property DiskSpace: integer read FDiskSpace; property UsedSpace: integer read FUsedSpace; property FreeSpace: integer read FFreeSpace; end;
このクラスは、エディットが編集できない TAPIDDLCombo クラスから派生する。 読みとり専用のプロパティーが7個追加されているが、これはディスクの情報をユーザーに公開するためのものだ。 実装コードは以下のようになる。
//---------- TAPIDriveCombo ----------------------------------------- procedure TAPIDriveCombo.Initialize; var i,r: integer; s,t: string; begin inherited Initialize; for i := byte('A') to byte('Z') do begin s := Char(i)+':\'; r := GetDriveType(PChar(s)); case r of DRIVE_UNKNOWN: t := 'Unknown'; DRIVE_NO_ROOT_DIR: t := 'No Root'; DRIVE_REMOVABLE: t := 'Removable'; DRIVE_FIXED: t := GetVolumeName(s); DRIVE_REMOTE: t := 'Network'; DRIVE_CDROM: t := 'CD_ROM'; DRIVE_RAMDISK: t := 'RamDisk'; end; if r >1 then begin t := s+' '+t; AddString(t); end; end; end; procedure TAPIDriveCombo.DefaultHandler(var Message); var AMsg: TMessage; begin AMsg := TMessage(message); case AMsg.Msg of WM_COMMAND:begin if (AMsg.LParam <> LPARAM(FHandle)) then exit; case AMsg.WParamHi of CBN_SELCHANGE: begin FRootDir := Copy(SelText,1,3); GetVolumeInfo(FRootDir); AMsg.Msg := UINT(self); FResultText := SelText; if Assigned(FOnSelChange) then OnSelChange(AMsg); end; end; if AMsg.WParamHi = WORD(CBN_ERRSPACE) then MessageBox(Parent,'ComboBox Control Buffer Full!','Warning',MB_OK); end; end; // case end; function TAPIDriveCombo.GetVolumeName(Root: string):string; var Vol,FileSys: array[0..100] of Char; SN,MaxN,FSF: DWORD; begin GetVolumeInformation(PChar(Root),Vol,100,@SN,MaxN,FSF,FileSys,100); result := '['+Vol+']'; end; procedure TAPIDriveCombo.GetVolumeInfo(Root: string); var Vol,FileSys: array[0..100] of Char; SN,MaxN,FSF: DWORD; SectPerClstr,BytePerSect,FreeClstr,Clstr: DWORD; begin GetVolumeInformation(PChar(Root),Vol,100,@SN,MaxN,FSF,FileSys,100); GetDiskFreeSpace(PChar(Root),SectPerClstr,BytePerSect,FreeClstr,Clstr); FRootDir := Root; FVolumeName := '['+Vol+']'; FFileSystem := FileSys; FSerialNumber := SN; FDiskSpace := Clstr*BytePerSect*SectPerClstr; FFreeSpace := FreeClstr*BytePerSect*SectPerClstr; FUsedSpace := DiskSpace-FreeSpace; end;
コードを見ていこう。 コンストラクタから呼び出される Initialize メソッドをオーバーライドして、最初に継承メソッドを実行した後、GetDriveType( ) を使ってインストールされているドライブを取得している。 その際、ドライブがハードディスクであるときは、GetVolumeName( ) メソッドを呼び出して、ドアライブ名を取得している。 オーバーライドした DefaultHandler( ) では、OnSelChange イベントを呼び出すまえに GetVolumeInfo( ) メソッドを呼び出し、読みとり専用プロパティーのフィールドに値を設定している。
これでこのクラスを実装できたので、いつもどおりインスタンス生成関数をつくろう。宣言部は、
function CreateDriveCombo(hParent: HWND; x,y,cx,cy: integer; sOnSelChange:TNotifyMessage): TAPIDriveCombo;
となり、実装部は、
function CreateDriveCombo(hParent: HWND; x,y,cx,cy: integer; sOnSelChange:TNotifyMessage): TAPIDriveCombo; begin result := TAPIDriveCombo.Create(hParent); with result do begin Dimension := SetDim(x,y,cx,cy); OnSelChange := sOnSelChange; end; end;
である。
それでは、TAPIDriveCombo クラスをテストするプログラムを DriveCombo.dpr に作成しよう。以下に全リストを示す。
program DriveCombo; uses windows, messages, UtilFunc in 'UtilFunc.pas', UtilClass in 'UtilClass.pas', GDIObject in 'GDIObject.pas', APIWindow in 'APIWindow.pas', APIControl in 'APIControl.pas'; //------------------------------------------------------------ // Global Types, Constants and Variables //------------------------------------------------------------ var MainWnd: TSDIMainWindow; DriveCmb: TAPIDriveCombo; L1: TAPILabel; LB: TAPIListBox; //------------------------------------------------------------ // Helper Handlers //------------------------------------------------------------ //------------------------------------------------------------ // Object Event Handlers //------------------------------------------------------------ procedure DriveSelChange(var AMsg: TMessage); var FreePercent,UsedPercent: integer; begin with DriveCmb do begin L1.Text := RootDir; LB.Clear; FreePercent := FreeSpace div (DiskSpace div 100); UsedPercent := 100 - FreePercent; LB.AddString(RootDir+#9#9+VolumeName); LB.AddString('Serial Number :'+#9+AIntToHex(SerialNumber,8)); LB.AddString('File System :'+#9+FileSystem); LB.AddString(' '); LB.AddString('Disk Space :'+#9+AIntToStr(DiskSpace)); LB.AddString(' '); LB.AddString('Used Space :'+#9+AIntToStr(UsedSpace)+ ' '+AIntToStr(UsedPercent)+'%'); LB.AddString('Free Space :'+#9+AIntToStr(FreeSpace)+ ' '+AIntToStr(FreePercent)+'%'); end; end; //------------------------------------------------------------ // Main Window Event Handlers //------------------------------------------------------------ procedure MainCreate(var Msg: TMessage); var hWindow: HWND; begin hWindow := MainWnd.Handle; MakeInitMenu(hWindow); L1 := CreateLabel(hWindow,10,10,150,30,''); L1.Margin := 6; DriveCmb := CreateDriveCombo(hWindow,10,50,150,160,DriveSelChange); LB := CreateListBox(hWindow,170,10,250,200,nil); with MainWnd do begin ClientWidth := 430; ClientHeight := 220; Center; end; end; procedure OnTest(hWindow: HWND); begin DriveCmb.SelIndex := 1; end; procedure MainCommand(var Msg: TMessage); begin Msg.Msg := MainWnd.Handle; InitMenuCommand(Msg,OnTest); end; procedure MainDestroy(var Msg: TMessage); begin PostQuitMessage(0); end; //------------------------------------------------------------ // Main Procedure //------------------------------------------------------------ begin MainWnd := TSDIMainWindow.Create(0,GetExeName); with MainWnd do begin OnCreate := MainCreate; OnDestroy := MainDestroy; OnCommand := MainCommand; end; MainWnd.DoCreate; Halt(MessageLoopNormal); end.
実行すると、右図のようになる。 MainCreate( ) ハンドラでは、ラベルとドライブコンボとリストボックスを一つずつ作っている。DriveCmb のリストからドライブを選択すると、 OnSelChange イベントを通して DriveSelChange ハンドラが実行される。 このハンドラでは、 DriveCmb のプロパティーを読みとって、選択されたドライブの情報をリストボックスに表示している。
TAPIDriveCombo のようなクラスは、インスタンスを作るだけで特定の情報にアクセスできる。 このようなコンボボックスクラスで最も一般的に使われているのは、 システムにインストールされているフォントをリストに列挙するものだろう。 たいていのワープロアプリケーションではメインウィンドウの左上にフォントを設定するコンボボックスがあるはずである。 このようなクラスは、 後の章でフォントを扱うときに作ることにしよう。
本章では、最初にコンボボックスの三つのスタイルについてコードで作成し、それらを制御するAPIについて調べた。次に、これらのコンボボックスのラッパークラスを作成し、テストした。最後に、 システムに存在するドライブの一覧とその情報を取得する TAPIDriveCombo クラスを作成し、テストした。
ここまでのプログラムファイル