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

#0 こんなことをしたいと思います。
 「[TListBox(VCL)で面白いこと、やってみましょう](https://qiita.com/maturi_tenna/items/a1fdb3e709ea00ba3d2d)」の第２回目になります。
　前回は、TListBoxの基本的な部分のご紹介と、簡単なオーナードローの実演でした。
　今回は、Delphi(VCLアプリケーション）で、もう少し、凝ったことをしたいな、と思います。

![0001.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/e688bc40-0b4f-36e9-1552-fef3808af36f.jpeg)

　まず、準備をお願いします。

・OS ：Windows10が動くパソコン
・プログラム開発環境　：Delphi Community Edition



#1 データ構造を決めます。
　今回は、TListBox.Items[n]の中身にする文字列データを、次のように決めたいと思います。

|1   |2   |3  |4   |5   |6  |7  |8～末尾  |
|:---|:---|:--|:---|:---|:--|:--|:--|
|予約|開閉|インデント|高さ|ライン色|チェック|セパレータ|文字列|
|0   |+,- |0～9|0～9|0～9|x,_|@ |S...|


※テスト用のデータを用意しました。これをコピーして、テキストファイルで保存してください。折り畳みは、クリックで開きます。
<details><summary>テスト用データ</summary>
0+011x@植物の分類
0+121x@胞子でふえる植物
0+232_@・シダ植物\　根・茎・葉の区別がある植物。
0+233_@・コケ植物\　根・茎・葉の区別がない。湿った場所に多い。雄株と雌株にわかれる。
0+124_@・種子でふえる植物
0+225_@・裸子植物\　子房がなく種子がむき出し。松、杉など
0+226_@・被子植物\　子房（果実）がある植物。イネ、イチゴ、サクラなど
</details>



#2　画面を作ります。
　さっそく画面を作りましょう。
　Delphiにはたくさんのコンポーネントがあります。今回は...

　>Standardのグループの中にあります。
　・ListBox　×１個
　・Memo　  ×１個
　・Button　×３個

　>Dialogsのグループの中にあります。
　・OpenDialog　×１個

　これらを使います。右下にあるパレットから選んで、画面中央にいるFormに貼り付けてください。


#2-1.テスト用データを読み込む準備

　前回もご紹介しましたが、DelphiはIDEという統合開発環境を持っています。次の絵のように、プロパティを設定したいときや、イベントを書きたいときは、画面上でオブジェクトを選択します。直感的に操作できるので、理解しやすいと思います。
　　
![0003.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/36280831-b75c-6f01-2aa1-5e2108936045.jpeg)
　オブジェクトインスペクタで上の絵のようにダブルクリックすると、自動的で、
　interface部に**procedure Button1Click(Sender: TObject);** 
　と、宣言が追加されます。
　また、implementationには、次のようにイベントハンドラのコードを書く場所が用意されます。あとは、begin end; の間に必要なコードを書くだけです。

```pascal
procedure TForm1.Button1Click(Sender: TObject);
begin
  //ここへコードを書き込みます。
end;
```

　では、次のようにコードを書き込んでください。begin end;の間をコピーして貼り付けても構いません。
　このコードの意味は、もしも、ファイルを開くのダイアログの結果がTrueだったら、取得したファイルをListBox1に読み込んでくださいな。　という意味です。

```pascal
procedure TForm1.Button1Click(Sender: TObject);
begin
  if OpenDialog1.Execute = true then  //ファイルを開く画面を表示します。
    ListBox1.Items.LoadFromFile(OpenDialog1.FileName);  //ListBox1へファイルを読み込みます。
end;
```

　Delphiの良い所は、すぐに実行して試して確認できることです。
　**[F9]キーで実行します。**
　いま、コードを書いた**Button1**をクリックして、テストデータを読み込んでください。
　ちゃんと動くかな？　試してみましょうね。

　うまくいったら、こうなります。
![0004.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/3a5135df-f6b7-bdde-ecc3-8c31de700413.jpeg)

　button2にもClickイベントを追加します。
　ListBox1の表示スタイルを標準に戻すボタンを作ります。

```pascal
procedure TForm1.Button2Click(Sender: TObject);
begin
  ListBox1.Style:=lbStandard;
end;
```

　さらに、button3にClickイベントをを追加します。
　これが、ListBox1の表示スタイルを今回のお題、オーナードローにするボタンになります。

```pascal
procedure TForm1.Button3Click(Sender: TObject);
begin
  ListBox1.Style:=lbOwnerDrawVariable;
end;
```

　ここまでが、準備です。
　次から、実際にコードをガリガリ書いていきます。

#3 インデント表示をしたい。
　インデントは案外、簡単にできます。
　OnDrawItemイベントでは、描画領域はRectで与えられます。なので、表示位置を右へ少しずらすだけです。

 インデントのレベルは、先ほど決めたデータ構造では先頭から数えて３文字目にあります。なので、Copy(S,3,1)で取り出せます。それから、抜き出した**数字**を**数値**に変換するには、**StrToIntDef(文字列,規定値の数値)**を使っています。
　
##3-1.**数字**を**数値**に変換するのは、なぜ？　
プログラムでは、数字と数値は別のものとして考えます。
![0005.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/408f2948-671d-b9dc-e72a-daf666ba1f2d.jpeg)
　ListBox.Items[n]に保存できるのは、**文字列**です。
　これに対して、描画位置の計算を行う**Rect.Leftの値は整数型の数値**です。
　このため、**StrToIntDef(文字列,規定値の数値)**を使って、文字を数値に変換しています。既定の数値を設定したのは、何かの手違いで**数値に変換できない文字列**が混入した際は、エラーにならないように、無難そうな数値を返すための工夫です。
　それから...　データ型はとても複雑なシステムです。とても全部を覚えきれるものじゃありません。最初は、**Longint**、 **String**、 **Boolean**の３つだけは覚えましょう。

　前置き長くなりましたが、プログラムは下のようになります。

```pascal
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
  Rect: TRect; State: TOwnerDrawState);
var
  Lv:longunt;
  S:string;
begin
  S:=ListBox1.items[index];       //項目を見出し
  Lv:=StrToIntDef(Copy(S,3,1),0); //3文字目を読み出して数値に変換
  Rect.Left:=Rect.Left+Lv*20;     //インデントレベル×20pixを右へ下げます

《中略》

end;
```

#4 テキストを折り返し表示したい。
　TListBoxの項目内で、テキストを折り返し表示するには、次のふたつのことを考える必要があります。

#4-1. 描画領域の中で文字列を折りたたみます。
 OnDrawItemイベントで与えられるRectの範囲中に納まるように、文字列を調節して改行表示するには、どんな数値が必要になるのでしょうか？

|求める部分のサイズ|算出方法|
|:---|:---|
|描画領域の横幅　A|Rect.Right - Rect.Left|
|文字列の大きさ　B|Canvas.TextWidth(文字列)|
|文字の高さ　　　C|Canvas.TextHeight(文字列)|
したがい、A < B のとき、Cだけ下へ表示位置をずらすと改行表示になります。

　参考までに、文字列を改行表示する方法は、もうひとつあります。　
　**Canvas.TextRect(Rect,文字列)**でも、Rectの四角内に収まるように文字を描画してくれます。


#4-2　改行コードは代替文字に変換します。
　もうひとつ考えなければいけないことがあります。
・TListBoxで項目を保持しているItemsは、文字列リストです。**改行コードは項目を区切るためのセパレータとして使われています。**改行コードを含む文字列は、そのままでは文字列リスト内に保持できません。

　具体的には、ListBox1.Items[index]:=Memo1.text; と代入することはできるのですが、改行コードで項目が区切られて、ひとつの項目には収まらなくなります。

　そこで、次のように、改行コードを代替文字に置き換えることになります。
　ListBox1.Items[index]:=ReturnToAlt(Memo1.text);
　今回は、「 \ 」（日本語環境のパソコンでは、半角￥マーク）に置き換えています。どんな文字に置き換えるのかは、アプリの使い道に合わせて考えるとよいでしょう。

```pascal
function  TForm1.ReturnToAlt(Mes:String) :string; //改行コードを\記号へ代替
var
  i:longint;
  S:string;
begin
  Mes:=AdjustLineBreaks(Mes);  //改行コードを直すおまじない

  S:='';
  i:=1;
  while i <= length(Mes) do
    if Copy(Mes,i,2)=chr(13)+chr(10) then
    begin
      S:=S+'\';
      i:=i+2;
    end
    else
    begin
      S:=S+Copy(Mes,i,1);
      i:=i+1;
    end;

  result:=S;
end;
```

　コードの中に、Mes:=AdjustLineBreaks(Mes); とあるのは、改行コードを直すおまじないです。入れてなくっても、たぶん、問題なく動きます。
　おまじないの紹介を兼ねて入れてます。**AdjustLineBreaks 関数**は、改行コードを調整してくれます。というのも、この世には、Windowsとは違うシステムで作られたアプリがあり、そこには、chr(13)+chr(10)の組み合わせ とは違う改行コードを含む文字列があるからです。いつか、そういう文字列を貼り付ける時が来ても大丈夫なようにAdjustLineBreaks 関数を混ぜました。

　さて、OnDrawItemイベントで与えられるRectの範囲中に、文字列を折りたたむコードはこんな感じになります。

```pascal
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
  Rect: TRect; State: TOwnerDrawState);
var
  i,TpPos,Lv:longint;  //i ループカウンタ TpPos文字表示の改行縦位置 Lvインデントレベル横位置
  S,Mes:string;        //Mes 文字列加工元 S 加工後の文字列

begin
  Mes:=ListBox1.Items[index]; //ListBox から項目を読み出し

    //テキスト描画ループ
    TpPos:=4; //textout の Top 位置

    i:=1;
    S:='';

    while i<=length(Mes) do  // i を1つずつ増やしながら文字列を頭から尻尾まで１文字ずつ確認します。
    begin
      //\が改行記号置き換え
      if Copy(Mes,i,1)='\' then
      begin
        textOut(Rect.left+8,Rect.Top+TpPos,S); //文字を表示してから
        TpPos:=TpPos+textheight('漢')+2;       //改行します
        font.Style:=[];                        //タイトル行の太文字指定を解除します。★（#6でタイトル行を解除するために使います）
        S:='';
      end
      else
        S:=S+Copy(Mes,i,1);                    //改行がないときは文字列を伸ばします


      if Rect.Right-Rect.Left-textwidth('漢')-10 < textwidth(S) then //漢１文字分引いてはみ出し１つ手前確認
      begin
        if TpPos+textheight('漢') > Rect.Bottom-Rect.Top then
          exit;              //縦位置がはみ出す場合は終了

        textOut(Rect.left+8,Rect.Top+TpPos,S);
        TpPos:=TpPos+textheight('漢')+2;  //改行
        S:='';
      end;

      if TpPos+textheight('漢') > Rect.Bottom-Rect.Top-2 then  //縦位置がはみ出す場合は終了
        exit;

      i:=i+1;
    end;

    textOut(Rect.left+8,Rect.Top+TpPos,S);                   //改行なしand 右端超えもないときの文字列表示
   end;
```

　いまのコードで使った関数はこんな感じです。

|関数|書式|得られる値、結果|
|:---|:---|:---|
|Lenght |Length(文字列) |文字列の長さ|
|TextWidth |TextWidth(文字列) |文字列が表示しきれる横幅|
|TextHeight |TextHeight(文字) |文字を表示できる高さ|
|TextOut|TextOut(x,y,文字列)|文字列を x, y の位置へ表示|
|Copy|Copy(文字列,インデックス,読み出す文字数|部分文字列（文字列をインデックスの位置から指定された文字数分読み出す）|

　


#5 項目の高さ（ItemHeight）を変更したい。
　項目ごとに高さを変更することもできます。
　方法は、**ListBoxのStyleをlbOwnerDrawVariable**に設定して、**OnMeasureItemイベント**の中で、**Heightの値を変える**だけです。Height:=0; とすれば、項目を隠すこともできます。非表示をきれいに処理するには、少し手間がかかりますけど（後述）

　プログラムの例を示します。今回のお題では、最初にデータ構造を決めました。4文字目を項目の高さに割り振っています。
　0～9までをListBox.ItemHeightにかけて、１行だけの高さ（ListBox1.ItemHeight）×倍数で高さを表すことにしました。

```pascal
procedure TForm1.ListBox1MeasureItem(Control: TWinControl; Index: Integer;
  var Height: Integer);
begin
  Height:=ListBox1.ItemHeight*StrToIntDef(Copy(ListBox1.Items[index],4,1),1); 
end;
```

　関数は入れ子にすることもできます。上の例では、StrToIndDef関数の中に、Copy関数が入っています。

#6 項目を開閉表示したい。
　前節で、項目ごとに高さを変えられることをお話ししました。
　**Height:=0;** とやっちゃえば、項目を非表示にできるはず...　と言いたいのですが、実際はもう少しコードが必要です。

##6-1.非表示の場合は、OnDrawItemeで描画しないようにします。
　実は、OnDrawItemeイベントは、たとえRectの高さがゼロであっても、がんばって描画しようとします。試してみると画面表示が微妙に乱れます。
　なので、非表示の時は描画を止めるように判定を増やします。

　具体的には、最初に決めたデータ構造では、２文字目を開閉フラグに割り振っています。**項目の高さがゼロの時に閉じる**でも、同じことができますが、開閉と項目の高さは別にした方が便利です。項目の高さを覚えたまま開閉できるため、使い勝手が良いのです。なので開閉フラグも用意しました。

```pascal
//OnDrawItemeイベントの先頭へ追加
  if Copy(S,2,1)='-' then  //閉じている項目は描画しない。
    exit;
```

##6-2.［↑］［↓］キーで移動する場合も、非表示の項目を飛ばすようにします。
　つまり、ListBox1.ItemIndexも、項目の高さがゼロの項目は通り過ぎるようにします。
 [↑]キーの時は、項目を上に遡りながら、開閉フラグが開(２文字目が'+')になっている項目を探します。
 [↓]キーの時は、項目を下へ降りながら、開閉フラグが開(２文字目が'+')になっている項目を探します。
※先頭と最後尾は閉じていない想定です。

　プログラムの中では、どのキーが押されているのか？　を判定するために、仮想キーコードを使います。KeyDownイベントでは、Keyに仮想キーコードが格納されています。

　よく使いそうな仮想キーコードはこんな感じです。

|キー|仮想キーコード|
|:---|:---|
|[↑]キー|VK_Up|
|[↓]キー|VK_Down|
|[←]キー|VK_Left|
|[→]キー|VK_Right|
|[Enter]キー|VK_Return|

　KeyDownは後で、もう一度使います。

```pascal
procedure TForm1.ListBox1KeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
var
  i:longint;
begin
  //エラーチェック
  if ListBox1.ItemIndex<0 then
    exit;
 
 //[↑]キーのとき
 if (key=VK_Up)and(ListBox1.ItemIndex>0) then
  begin
    for i:=ListBox1.ItemIndex downto 0 do
      if Copy(ListBox1.Items[i-1],2,1)='+' then　//開いている項目を見つけたらItemIndexを移動
      begin
        ListBox1.ItemIndex:=i;
        exit;
      end;

    ListBox1.ItemIndex:=0; //該当なしのばあい 上端
  end;

  //[↓]キーのとき
  if (key=VK_Down)and(ListBox1.ItemIndex<ListBox1.Items.Count-2) then
  begin
    for i:=ListBox1.ItemIndex to ListBox1.Items.Count-2 do
      if Copy(ListBox1.Items[i+1],2,1)='+' then  //開いている項目を見つけたらItemIndexを移動
      begin
        ListBox1.ItemIndex:=i;
        exit;
      end;

    ListBox1.ItemIndex:=ListBox1.Items.Count-1; //該当なしのばあい 下端
  end;

```


#7 見栄えをカッコよくしたい。
　項目ごとに色を付けて分類できたら見栄えが良くて使いやすいかもしれません。カラーバーをどの色で塗るのか？　は、データ構造の５文字目です。

　**Rectange(X1, Y1, X2, Y2）**は、左上と右下を指定すると四角形を描いてくれます。輪郭の色はPen.Colorで、塗りつぶしはBrush.Colorです。

```pascal
   //OnDrawItemeイベントへ追加
   //カラーバー塗り
   brush.Color:=AskColor(StrToIntDef(Copy(S,5,1),0));
   Pen.Color:=ListBox1.Color;
   Rectangle(Rect.Left,Rect.bottom,Rect.Left+16,Rect.Top);
   Rect.Left:=Rect.Left+16;
```

　塗り色については、次のように簡単な関数をつくりました。
　Font,Pen,BrushのColorは、longint型の整数値です。
　Color は　青、緑、赤の輝度を１バイトずつで表しています。
　

```pascal
function TForm1.AskColor(Clr:longint):longint;
begin
  case clr of
  0:
    Result:=$00E6E6E6;
  1:
    Result:=$00EACDF3;
  2:
    Result:=$00F4CCD3;
  3:
    Result:=$006A6AFB;
  4:
    Result:=$00E0F4CC;
  5:
    Result:=$00CDF3DC;
  6:
    Result:=$0079DE74;
  7:
    Result:=$00CED7F2;
  8:
    Result:=$00D8773D;
  9:
    Result:=$00F7F7F7;
  else
    Result:=$00C6C6C6;  //elseは、それ以外のばあい
  end;
```

　それから、いま、選択されている項目を目立つように塗ります。OnDrawItemeイベントの**State**を確認すると、いま描画している項目が選択されているか？　が、わかります。

　stateはTOwnerDrawStateというオーナードローに便利な情報のセットです。複数の値が詰め合わせになっているため、**if odSelected in state  then**　という書き方で、**Stateの中にodSelectedが含まれていますか？**を評価しています。

```pascal
     //OnDrawItemeイベントへ追加
     //項目塗り 塗色決め
     if odSelected in state  then      //state odSelectedが含まれている？ 
       Brush.Color:= clActiveCaption   //選択状態の色
     else
       brush.Color:=clBtnFace;         //それ以外の色

     FillRect(Rect);  //項目塗り
```

　さらに、カッコよくしてみましょう。
　項目の初めの１文字目が「・」全角中点の時は、中点～改行までがタイトル行にしましょう。太文字にすると同時に下線をつけてカッコよくします。太文字属性の解除は改行処理の中で**Font.Style:=[]**で元に戻してます。

　タイトル行の下へ線を引きます。
　２点間を結ぶ直線は、Canvas.MoveTo(x,y)で始点を指定します。次に、 Canvas.LineTo(x,y)で終点を指定するとともに、始終点を結ぶ線をCanvas.Pen.Colorで引きます。

```pascal
     //OnDrawItemeイベントへ追加
     //タイトル行 （中点が行頭にあるとき）
     if Copy(Mes,1,1)='・' then
     begin
       font.Style:=[fsBold];
       Pen.Color:=AskColor(StrToIntDef(Copy(S,5,1),0));
       Moveto(Rect.Left,Rect.Top+4+textheight('漢'));     //MoveToで始点を設定
       LineTo(Rect.Right-10,Rect.Top+4+textheight('漢')); //LineToで終点を設定し、線を引きます。
     end;
```

|関数|書式|得られる値、結果|
|:---|:---|:---|
|MoveTo|MoveTo(x,y)|x, y へ描画の指定を移動（絵は描きません）|
|LineTo|LineTo(x,y)|前回の描画位置を始点に、x, y を終点にして、Pen の Color で線を引きます|


　**ListBox1DrawItem** に色んな処理を付け足して、こんなことができる　と試してみました。バラバラに分割して説明したので、全体がどうなっているのか？　わかりにくかったと思います。
　まとめに、全部をつないだ状態の ListBox1DrawItem イベントハンドラを再掲します。

```pascal
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
  Rect: TRect; State: TOwnerDrawState);
var
  i,TpPos,Lv:longint;  //i ループカウンタ TpPos文字表示の改行縦位置 Lvインデントレベル横位置
  S,Mes:string;        //Mes 文字列加工元 S加工後の文字列
begin

  S:=ListBox1.Items[index]; //ListBoxから項目を読み出し
  Rect.Left:=Rect.Left+8;

　//閉じてる
  if Copy(S,2,1)='-' then  //閉じている項目は描画しない。
    exit;

  Lv:=StrToIntDef(Copy(S,3,1),0); //インデントのレベル　0-9

  Mes:=Copy(S,8,length(S)-7);  //文字列を読み出し

   with ListBox1.Canvas do
   begin
     font:=ListBox1.font;

     //下地塗り
     Brush.Color:=ListBox1.Color;
     FillRect(Rect);

     //項目塗り 位置決め
     Rect.Top:=Rect.Top+2;
     Rect.Left:=Rect.Left+Lv*20; //インデントレベル×20pixを字下げ

     //カラーバー塗り
     brush.Color:=AskColor(StrToIntDef(Copy(S,5,1),0));
     Pen.Color:=ListBox1.Color;
     Rectangle(Rect.Left,Rect.bottom,Rect.Left+16,Rect.Top);
     Rect.Left:=Rect.Left+16;

     //項目塗り 塗色決め
     if odSelected in state  then
       Brush.Color:= clActiveCaption   //選択状態の色
     else
       brush.Color:=clBtnFace;         //それ以外の色

     FillRect(Rect);  //項目塗り


     //タイトル行 （中点が行頭にあるとき）
     if Copy(Mes,1,1)='・' then
     begin
       font.Style:=[fsBold];
       Pen.Color:=AskColor(StrToIntDef(Copy(S,5,1),0));
       Moveto(Rect.Left,Rect.Top+4+textheight('漢'));
       LineTo(Rect.Right-10,Rect.Top+4+textheight('漢'));
     end;

    //テキスト描画ループ
    TpPos:=4; //textoutのTop位置
    i:=1;
    S:='';

    while i<=length(Mes) do
    begin

      //\が改行記号置き換え
      if Copy(Mes,i,1)='\' then
      begin
        textOut(Rect.left+8,Rect.Top+TpPos,S); //文字を表示してから
        TpPos:=TpPos+textheight('漢')+2;       //改行します
        S:='';
        font.Style:=[];                        //タイトル行の太文字指定を解除します
      end
      else
        S:=S+Copy(Mes,i,1);                    //改行がないときは文字列を伸ばします

      if Rect.Right-Rect.Left-textWidth('漢')-10 < textwidth(S) then //漢１文字分引いてはみ出し１つ手前確認
      begin
        if TpPos+textheight('漢') > Rect.Bottom-Rect.Top then
          exit;               //アイテム描画域を超える場合は終了

        textOut(Rect.left+8,Rect.Top+TpPos,S);
        TpPos:=TpPos+textheight('漢')+2;  //改行
        S:='';
      end;

      if TpPos+textheight('漢') > Rect.Bottom-Rect.Top-2 then  //縦位置がはみ出す場合は終了
        exit;

      i:=i+1;
    end;

    textOut(Rect.left+8,Rect.Top+TpPos,S);                   //改行も右端超えもないときの文字列表示
   end;
end;
```

#8　キーボードやマウスから操作できるようにします。

　せっかく画面を作っても動かなければ、意味がありませんし、寂しいです。
　キーボード操作で、インデントの上げ下げをできるようにします。

　先ほど、選択中の項目を移動する処理でも紹介しましたが、キーボードで何かキーを押し下げると、**KeyDownイベント**が起こります。このイベントハンドラの中で、どのキーが押されているのかを判定して、必要な処理をします。

　今度は、**Shift** と **key** の組み合わせで判定します。
　**if (key=VK_LEFT) and (shift=[ssShift]) then ～**と、if文の判定に２つ条件を書くことで、組み合わせを表現します。

インデントを減らしてレベル上げ　[Shift] + [←]

```pascal
 //ListBox1KeyDownイベントの中に書きます。
 //←レベル上げ
  if (key=VK_LEFT)and(shift=[ssShift]) then
  begin
    Lv:=StrToIntDef(Copy(Mes,3,1),0);
    Lv:=Lv-1;
    if Lv<0 then
      Lv:=0;

    S1:=Copy(Mes,1,2);
    S2:=Copy(Mes,4,length(Mes)-3);
    ListBox1.Items[ix]:=S1+IntToStr(Lv)+S2;
    ListBox1.ItemIndex:=ListBox1.ItemIndex+1;
    exit;
  end;
```

インデントを増やしてレベル下げ　[Shift] + [→]

```pascal
 //→レベル下げ
  //ListBox1KeyDownイベントの中に書きます。
  if (key=VK_Right)and(shift=[ssShift]) then
  begin
    if ListBox1.ItemIndex=0 then
      exit; //先頭行レベルはゼロ固定

    Lv:=StrToIntDef(Copy(Mes,3,1),0);
    Lv:=Lv+1;
    if Lv>9 then
      Lv:=9;

    S1:=Copy(Mes,1,2);
    S2:=Copy(Mes,4,length(Mes)-3);
    ListBox1.Items[ix]:=S1+IntToStr(Lv)+S2;
    ListBox1.ItemIndex:=ListBox1.ItemIndex-1;
    exit;
  end;
```

　項目を開閉は、DblClickイベントで処理しましょう。先ほどと同じく、オブジェクトインスペクタの ListBox1 のイベントの一覧表から、**OnDblClick** を探して、空欄をダブルクリックしてください。
　処理の内容は、２文字目にある開閉フラグを書き換えているだけです。
　フラグは、'+'が開きで、'-'が閉じ にしました。

```pascal
procedure TForm1.ListBox1DblClick(Sender: TObject);    //開閉
var
  Mes,S,S1,S2:String;

begin
    Mes:=ListBox1.Items[ListBox1.ItemIndex];
    S:=Copy(Mes,2,1);
    S1:=Copy(Mes,1,1);
    S2:=Copy(Mes,3,length(Mes)-2);　//バラバラにします。

    if S='+' then   //開閉フラグを反転します
      S:='-'
    else
      S:='+';

    ListBox1.Items[ListBox1.ItemIndex]:=S1+S+S2;　//くっつけて書き戻し
end;
```

#9 デバッグします。
　ListBoxでオーナードローを使う一番のメリットがこれです。
　StyleをlbStandardに戻すと、実行中でも、データ構造の中身が見ることができます。
　キー入力やダブルクリックをすると、フラグが書き換わる様子がそのまま見て確認できます。なるほど～と、思ってくれたらうれしいです。

　[button2] で標準の表示に戻ります。
　[button3] でオーナードローに切り替わります。

![0006.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273793/f1c7b97f-5554-d2dd-58ef-86c692d9fecd.jpeg)


#9 ありがとうございます。

　今回も、長くなりましたが、ここまでご覧いただきありがとうございます。

　次回は――
　いま作ったリストボックスに、memoから文字列を書き込んだり、ファイルに保存したりする予定です。