TListBox(VCL) で面白いこと、やってみましょう

#0.こんなことをしたいと思います。
　TListBoxをオーナードローで便利にかっこよく。
　標準のリストボックス（TListBox)は、項目を１行表示しかできません。
　でも、オーナードローを使えば、フォントの大きさを変えたり二段書きにしたりと、ちょっと面白いことができます。
　今回は、Windows環境でVCLアプリケーションを作ります。 

![絵菜TListBox.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/87a371b4-aef5-021a-197f-625c76c67fdc.jpeg)

#1.準備するもの
・OS ：Windows10が動くパソコン
・プログラム開発環境　：Delphi Community Edition

　Delphi Community Editionは、ムキムキの開発環境が無料で使えます。
　さあ、ダウンロードしましょう。

　Delphi Community Editionについては、こちらの、@pik様の記事をぜひご参照ください。
　Delphiの導入から、学ぶために必要な情報が網羅されています。
　https://qiita.com/pik/items/1db2b6d2f9972d953df4

#2.お題
・TListBoxのご紹介と使い方。
・オーナードローを使うと面白い使い方ができます。

　今回は、Windows環境でVCL(Visual Component Library)を使うアプリケーションを作ります。
　TListBoxは、パレットでは、Standard のグループにいる最古参のコンポーネントです。この２５年目の古強者は、そのままでも十分な働きをしてくれます。
　でもそれだけじゃありません。
　オーナードローを使えば、自由に見た目をデザインし直せるのです。
　あなたも、カッコ良くなったリストボックスを使ってみませんか？

#3.TListBoxの基本

　初めてDelphiを触る方向けに、簡単なListBoxの使い方を紹介します。すでにDelphiを知っている方は読み飛ばしてください。
　良く使うプロパティを抜粋で紹介します。

**最も重要なプロパティ**

|プロパティ|説明|備考|
|:---|:---|:--|
|Items|リストの中身|文字列リストです|
|ItemIndex|いま選択中の項目のインデックス番号が入っています|０以上の整数値です|
※itemsはリストの内容物を格納する場所です。このプロパティを使って、リストを読み書きします。

**ListBoxの便利な機能に関わるプロパティ**

|プロパティ|説明|備考|
|:---|:---|:--|
|Sorted|自動並べ替えのあり/なし|便利な自動ソート付き|
|MultiSelect|複数の項目を選択可能にします||
|ExtendedSelect|連続する項目をまとめて選択可能にします|MultiSelectがTrueのときだけ機能します|

**リストの見た目に関わるプロパティ**

|プロパティ|説明|備考|
|:---|:---|:--|
|ItemHeight|項目ひとつ分の高さ||
|Font|表示で使う文字フォント||
|Color|リストの地の色||
|Style|標準の見た目とオーナードローを切り替え|今回のお題で使います|

##3-1.リストの操作

　Itemsプロパティは文字列リスト（TSrings）という１次元の配列変数に雰囲気が似ているオブジェクトです。ListBoxのItemsプロパティの使い方を覚えると、文字列リストを使う他のコンポーネント（TmemoのLinesプロパティなど）にも応用ができます。

・リストに含まれる項目を読み書きするには、**Items[インデックス番号]**です。
・TListBoxの**ItemIndexプロパティ**には、リストの中で**いま、選択されている項目のインデックス**が入っています。

```pascal
bar
  S:string;
begin
 S:=ListBox.Items[ListBox.ItemIndex];　//選択中の項目を読み出し
 ListBox.Items[ListBox.ItemIndex]:=S;　//選択中の項目へ書き込み
end;
```

　リストへの項目の追加、挿入、削除はそれぞれ次のようにメソッドで行います。


![TListBox_e001.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/c3cef00b-83a0-2e3d-058c-4468b68a3758.jpeg)

```pascal
begin
 ListBox.Items.add('何か文字列') //最後尾に追加

end;
```

![TListBox_e002.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/c5bc6419-018a-7c16-0a58-bb6f44f925e6.jpeg)

```pascal
begin
 ListBox.Items.Insert(ListBox.ItemIndex,'何か文字列');  //選択中の項目の直上に追加

end;
```
![TListBox_e003.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/5fc88b87-8820-4bde-bd79-cd4ff2a9feac.jpeg)


```pascal
begin
 ListBox.Items.Delete(ListBox.ItemIndex);  //選択中の項目を削除

end;
```

##3-2.インデックスの値にご注意

　ここで、ひとつ注意しなければいけないことがあります。
　ListBox.ItemIndexには、**いま、選択されている項目**のインデックスが入っています。
　なので、何も選択していない場合は、-１などゼロ未満の値が入っています。これをうっかり、**Items[ゼロ未満の値]**と入れてしまうと、エラーになります。
　また、items[任意の値]を入れて読み書きする場合は、**項目の総数-1**を超える値を入れるとエラーになります。**ゼロ始まりで項目を数える**ため、最後尾の項目のインデックス番号は総数-１番目になるためです。
　ListBoxのitemsに何個の項目が含まれているのか？　は、**ListBox.Items.Count** を参照すると解ります。

![絵菜TListBox081.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/5f0d229c-4652-bc1c-2001-8ae2ea534b15.jpeg)


```pascal
var
  ix:longint; //任意のインデックス。整数値
begin
  if (ix<0) or (ix > ListBox.Items.Count-1) then  //任意のインデックス が　ゼロ未満　または　項目の総数-１を超えるとき
    exit;

  ListBox.Items[ix]:=S;  //ixが範囲をはみ出していないか？　判定後に項目へ書き込み
end;
```

#4.オーナードロー
　オーナードローは、コンポーネントの見た目の一部を変更できる機能です。お手軽で便利な機能なので、もう少し何か欲しいと感じたときに使っていきましょう。

##4-1.画面をデザインします。
　それではアプリを実際に作ってみましょう。
　今回は、Windowsで動くVCLアプリケーションになります。

　まず、**メインメニュー > ファイル > 新規作成 > windows VCLアプリケーション**と選択します。そうすると、デザイナ画面が開いてまっさらな何もないウインドウが表示されているはずです。
　下の絵のように、このまっさらウインドウにコンポーネントを貼り付けます。

![TLIstBox6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/4781cda8-362f-ffdc-cd8f-a97520bcb8c4.jpeg)


##4-2.プロパティを設定します。
|プロパティ|設定値|説明|
|:---|:---|:--|
|font|お好みで|...で設定画面が開きます|
|ExtendedSelect|False|使いません|
|ItemHeight|40|項目のひとつぶんの高さです|
|MultiSelect|False|使いません|
|Name|ListBox1|初期値のままです|
|Sorted|true|自動ソートします|
|Style|lbOwnerDrawFixed|項目の高さ固定でオーナードローします|
|Items|テストデータを入力します|...で設定画面が開きます。**次の絵を参照**|

![TListBox5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/2fb0b688-4d3a-5993-ff80-5c296d797636.jpeg)

　テストデータの構造は、**ひらがな+ , +漢字**にしてください。ひらがなと漢字を半角「,」カンマで区切る様式になります。

|ひらがな| , |漢字|
|:---|:---|:---|

　今回のプログラムで使うデータの構造はこれになります。


##4-3.OnDrawItemにプログラムを書きます。
　
　StyleプロパテをlbOwnerDrawFixedに設定すると、OnDrawItemイベントが有効になります。
　項目を描画すべきタイミングで、OnDrawItemイベントが起きて、プログラム実行の流れがここに来ます。

　OnDrawItemのようなイベントを処理するプログラムを**イベントハンドラ**といいます。イベントハンドラを書く方法は次のとおり。オブジェクトインスペクタでイベントのタブを開いて、OnDrawItemのとなりにある空欄をダブルクリックしてください。自動的にイベントハンドラの最初の記述が用意されます。

![絵菜TListBox002.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/eb2acc22-5b7f-5b60-9fb8-d6fdac4ea8a7.jpeg)

```pascal
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
  Rect: TRect; State: TOwnerDrawState);

begin
      // ここへプログラムを書きます。
end;
```

　それからコードをスクロールして、上の方を確認すると、**procedure ListBox1DrawItem**が増えているのが、わかります。
　ついでなので { Private 宣言 } の中へ gRubyStr,gKanjStr:string; と下のように書き足してください。
　この Type Form1 から end; の中に書き込んだものは、Form1のユニットの中にあるすべてのProcedureやFunctionから呼び出せるようになります。 グローバル変数と呼ばれています。

　それから、 { Public 宣言 }の中へ procedure ReadRubySub(ix:longint); とサブルーチンの宣言を足してください。Delphiでは基本的には使う前に先に宣言するルールになっています。（最近、インライン定義もできるようになりましたけど）

```pascal
type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    Edit1: TEdit;
    Button1: TButton;
    Edit2: TEdit;
    procedure ListBox1DrawItem(Control: TWinControl; Index: Integer;　//ここ
      Rect: TRect; State: TOwnerDrawState);
  private
    { Private 宣言 }
    gRubyStr,gKanjStr:string;  // これを書き足してください

  public
    { Public 宣言 }
    procedure ReadRubySub(ix:longint);  // これも後で使うので書き足してください。
  end;
```

　次に、下へスクロールして、ListBox1DrawItemのイベントハンドラに戻ります。
　下のように書き込んでください。「//」より後ろはコメントなので省略できます。

```pascal
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
  Rect: TRect; State: TOwnerDrawState);

begin
   with ListBox1.Canvas do 
   begin
     font:=ListBox1.font; // fontはListBoxのfontをコピーして使います

     if (State=[odSelected,odFocused])or(State=[odSelected])   then
        Brush.Color:=clActiveCaption  // いま描画する項目が選択状態のとき
     else
        Brush.Color:=ListBox1.color;  // 選択状態ではないときはListBoxのColorで塗ります。
  
     FillRect(Rect);  // 下地塗りをします。

     ReadRubySub(Index);  // サブルーチンに文字列の切り出しを依頼

//ルビ（ひらがな）を描画
     font.Style:=[];  // fontの飾りはなし
     font.size:=8;    // fontの大きさは８ポイント
     textout(Rect.left+14,Rect.top+2,gRubyStr);  // 文字を描画

//漢字を描画
     font.Style:=[fsbold];  // fontの飾りは太文字
     font.size:=10;         // fontの大きさは10ポイント
     textout(Rect.left+14,Rect.top+16,gKanjStr);  //  文字を描画
   end;
end;
```

　オーナードローはCanvasオブジェクトに対して、Canvasオブジェクトが用意したメソッドやオブジェクトを使って絵を描きます。
　プログラムの流れとしては―――
1.FontをListBox1からCanvasにコピーします。
2.いま描画しようとしている項目が選択状態ならば、背景色をclActiveCaption に、選択されてないなら、ListBox1.colorをコピーして使います。
3.FillRectメソッドで背景色を塗ります。
4.ReadRubySubというサブルーチンをこの後に作ります。Indexを引数として渡します。
5.ひらがなをTextOutで描画します。
6.下へ位置をずらして、太文字で漢字を描画します。

まとめるとこんな感じです。

|メソッド|書式|説明|
|:---|:---|:---|
|FillRect|FillRect(TRect);|Trectで指定した矩形領域をBrush.Colorの色で塗ります|
|TextOut|TextOut(横位置、縦位置、文字列);|Canvasのフォントで文字を指定した位置へ描きます|

|TCanvasに含まれるオブジェクト|説明|
|:---|:---|
|TFont|文字フォントです。プロパティとして、フォントの名前や色、サイズなどを持ちます。|
|Brush|背景色や塗りつぶし系のメソッドで使うブラシです。|
|TRect|矩形領域（長方形）を定義したもの。描画位置を表すのによく使います。|


##4-4.文字列の切り出しをするサブルーチンを書きます。
　ListBoxのItems[index]の中身は、「ひらがな,漢字」の書式で格納されています。これをOnDrawItemイベントで、２段書きにするには、**ひらがなと漢字を別の変数へ分ける**必要があります。
　この処理を行うサブルーチンが次に書くReadRubySub です。
　このサブルーチンは自分で書くため、命名規則に抵触しない限り自由に名前を付けられます。
　先ほどTForm1の定義を確認した際に、ついでにこのサブルーチンで使うグローバル変数とサブルーチンの定義も済ませました。
　次は、サブルーチン本体を手で書きます。OnDrawItemの下に次のように書き足してください。コピーして貼り付けても良いです。

```pascal
procedure  TForm1.ReadRubySub(ix:longint);
var
  i:longint;  // ループカウンタ
  S,M:string; // 文字列　Sが検査する元の文字列　Mが出力

begin
//範囲チェック
  if ix<0 then // ゼロ未満
    exit;

  if ListBox1.items.count-1 < ix then // 項目総数越え
    exit;

  if ListBox1.items.count<=0 then  // ListBoxが空っぽ
    exit;

//ここから本番
  S:=ListBox1.items[ix]; //　ListBoxからixで指定された項目を読み出してコピー 
  M:='';  //出力用はクリアして準備

  for i:=1 to length(S) do  // 文字列の長さ分を一文字ずつループして調べます
  begin
    if copy(S,i,1)=',' then  // 「,」カンマを見つけた？
    begin
      gRubyStr:=M;  // 「,」を発見。ひらがなを連絡用グローバル変数へコピー
      M:='';        // 出力用のMをクリアして漢字を集める準備
      continue;     // コンティニュー
    end;

    M:=M+Copy(S,i,1);  // 一文字ずつCoptして集めます 
  end;

  gKanjStr:=M;  //ループを抜けたら集めた漢字を連絡用グローバル変数へコピー
end;
```
　プログラムの前半は、ListBox1.Items[index]にアクセスする前に、indexの値がはみ出してないか？をチェックしています。はみ出しと判断したら、このサブルーチンをExit;で抜けます。

　プログラムの後半は、ループです。
　Copy関数を使用して１文字ずつ前から確認します。
　「,」を見つけたら、先頭文字からカンマまでをひらがなと判断して、連絡用グローバル変数のgRubyStrに格納します。
　「,」から文字列の末尾までを漢字と判断して、連絡用グローバル変数のgKanjStrに格納します。
　呼び出し元のListBox1.onDrawItemから見ると、描画しようとするindexを引数として与えると、グローバル変数にひらがなと漢字が格納されて戻ってくるというサブルーチンになります。

　プログラムのポイントとしては、**Copy関数**です。
　ループのプログラムを見ると、先ほどのListBox.Items.Countと何かが違います。

　ややこしくてごめんなさい。
　**Copy関数は、１始まりのインデックスで文字列を数えます。**
　このため、For i:=1 to Length('文字列') do　というループの数え方になります。
![TlistBox_ee01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/3058a395-614f-e261-90a2-276ff5d343d2.jpeg)

|名前|書式|説明|
|:---|:---|:---|
|Copy|Copy(文字列,読み出し位置,読み出す文字数);| 文字列の一部を抜き出します|

##4-5.仕上げ
　最後にEdit1とEdit2に文字を入れて、Button1をクリックするとリストに追加されるようにします。

```pascal
procedure TForm1.Button1Click(Sender: TObject);
begin
  ListBox1.Items.Add(Edit1.Text+','+Edit2.text)

end;
```

#5.実行します
　プログラムの実行は、［F9］キーになります。
　Edit1にひらがな、Edit2に漢字を入力してボタンを押すと、リストボックスに追加されるはずです。今回は、自動ソートのプロパティをTrueにしているため、書き込んだ文字は自動的にあいうえお順に並べ変わるはずです。

　もしも余裕がありましたら、ListBoxの自動ソートを切って（SortedプロパティをFalseにします） Button1Clickの中身を3-1.で紹介しましたInsertやDeleteも試してみてください。

#6.ありがとうございました。
　ここまでご覧いただきありがとうございます。