7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

DelphiAdvent Calendar 2023

Day 4

チェックボックスを作ってみよう

Last updated at Posted at 2023-12-03

お題 delphi の VCL(Visual Component Library)で、コンポーネントを作ってみよう。

001お題発表.jpg

環境と準備

言語 Delphi 11 Community Edition
ターゲットプラットフォーム Windows 32ビット
※64ビットでもできますが、32ビットの方がデバッグしやすいため、32ビットがお勧め。完成したら、64ビットに切り替えてコンパイルしても良いです。

(前振り)チェックボックスでモヤる

チェックボックスでやらかしたことがあります。
Windows標準のチェックボックスって、見づらいですよね。ね?

とある業務の画面で、外したはずのチェックボックスに、チェックが残っていたのです。
結果は… 数か月後に、発覚。ボスの部屋に強制転送されました。お辞儀の角度は45度です。

CheckBoxのモヤるところは、
・視認性の悪さ。
・クリックがやりにくい。
・キーボードでも、マウスでも操作できてしまう。

この3つと思います。

この記事ですること

delphiは、オリジナルのコンポーネントを作ることもできます。
アプリを自作するなら、チェックボックスをデザインし直してみたいと思います。
 
派生元にするには、PaintBoxにします。
PaintBoxは、いわゆる本当のウインドウコントロールではないので、メモリーの節約にもなります。
TABキーでフォーカスを移せないため、テキストボックスに入力してたはずが、隣のチェックボックスにスペースキー打ってた!? って、事案も起きません。

それから、delphiでコンポーネントを作って試す方法は、インストールする方法と、インストールしない方法のふたつがありますが、今回はインストールしない方法を試します。

準備

コンポーネントを表示するためには、台紙になるフォームが必要です。
最初に、テスト用のアプリとフォームを作ります。

ファイル > 新規作成 > Windows VCLアプリケーション

画面に、何にも載っていない、のっぺらぼーなウインドウが表示されます。
これで、準備完了です。

コンポーネントを作ります。

コンポーネント > コンポーネントの新規作成 >
検索窓に「paint」と入力すると、今回使うPaintBoxが出てきます。

00001コンポーネント新規作成1.jpg

[次へ(N)>>]をクリックします。

00001コンポーネント新規作成2.jpg

クラス名を、ここでは TReCheckBox に変更します。
パレットページは、インストールしないので、Samplesのままです。
ユニット名はTReCheckBox.pasに変更します。

[次へ(N)>>]をクリックします。

00001コンポーネント新規作成3.jpg

ここでは、ユニットの作成(U)を選択します。

最小限のコードは、自動で用意されます。

システムが自動で作ってくれるコードは、ここ
unit ReCheckBox;

interface

uses
  System.SysUtils, System.Classes, Vcl.Controls, Vcl.ExtCtrls;

type
  TReCheckBox = class(TPaintBox)
  private
    { Private 宣言 }
  protected
    { Protected 宣言 }
  public
    { Public 宣言 }
  published
    { Published 宣言 }
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TReCheckBox]);
end;

end.

次に、いま作ったReCheckBox.pasをプロジェクトに追加します。

プロジェクトに追加しておくと、使用するファイル一式をセットで保存や読み込みができるようになります。

まず、ファイル > すべて保存 > で、プロジェクトやフォームとReCheckBox.pasを保存します。

たぶん、ドキュメント\Embarcadero\Studio\Projects がデフォルトの保存場所になっていると思います。ここへサブフォルダーを作ってまとめて保存します。
ファイル名はデフォルトのままでかまいません。
ReCheckBox.pasも同じ場所に保存します。

ReCheckBox.pasをプロジェクトに追加する、具体的な操作方法です。

標準の配置のままでしたら、画面右端に「プロジェクト」の小さい窓があります。
ここのツリー表示を右クリックして、ポップアップメニューを呼んで、「追加(A)」を選択します。
エクスプローラーが立ちあがるので、さきほど作ったReCheckBox.pasを探してください。

00002プロジェクトにファイルを追加.jpg

ここまでで、オリジナルデザインに直したチェックボックスを作る準備ができました。

ファイル > すべて保存を押してください。

ここからは、実際にコードを書いて自作コンポーネントを作ります。
あ、でも、PaintBoxに差分を書き足して作りますから、コード量は少しです。

自作コンポーネント ReCheckBox をつくります。

いまから、プログラムする内容を図にまとめると、こんな感じです。
リンク図.jpg

※記事の末尾にプログラムの全文を載せています。
 めんどくさかったら、コピペしちゃってください。

uses節に Vcl.Graphics を追加してください。

ここから、ReCheckBox.pas の中身を書いていきますが、最初に、一番上にある uses節に Vcl.Graphics を書き足してください。自動作成だと、たぶん付いていません。コンポーネントを画面に描く際に使う定義内容とかが入っています。uses節に書き足すだけで、Canvas関係で使ういろいろな定義が使えるようになります。(clSkyBlueとかの色の定義もここに入ってます)

uses
  System.SysUtils, System.Classes, Vcl.Controls, Vcl.ExtCtrls, Vcl.Graphics;

Checkedプロパティを作ります。

delphiは、宣言してから、コードを書いて、呼び出して使う流儀の言語です。
宣言 > 実装 > 参照の順にご説明します。

宣言を書きます。

データフィールドを作成します。
CheckBoxのチェック印は、あり/なしを表すだけですから、Boolean型のデータです。
データフィールドは、Checkedプロパティのデータを保持する箱です。

type
  TReCheckBox = class(TPaintBox)
  private
    { Private 宣言 }
    FChecked:boolean; //チェックされている/いない

type
TReCheckBox = class(TPaintBox) と書かれている場所がありますよね。
ここが、今回新しく作るReCheckBoxの宣言部です。

{ Private 宣言 } のすぐ下に、FChecked:boolean; と書きます。
意味は、「FCheckedは、boolean型だよ」です。

データフィールドは、外部から見えない場所、private に仕舞います。
慣習で、データフィールドには、F とあたまに付けます。

書き込み手続きを宣言

  protected
    { Protected 宣言 }
    procedure SetChecked(value:boolean);  //Checkedを書き込み

{ Protected 宣言 } のすぐ下に、procedure SetChecked(value:boolean); と書きます。
FCheckedのデータフィールドへ、データを書き込む手続き SetCheckedを定義します。
慣習で、プロパティへの書き込み手続きは、Set を頭に付けます。

プロパティを宣言

  published
    { Published 宣言 }
     property Checkcd:boolean read FChecked write SetChecked;

{ Published 宣言 } のすぐに下に、property Checkcd:boolean read FChecked write SetChecked; と書き込みます。

意味は、
「プロパティ Checked は、boolean型であり、読み出しは直接 FCheckedを参照、書き込みは SetChecked 手続きを使います」という内容です。

書き込み手続き SetChecked の実現部を作ります。

delphiのプログラムは、「implementation」って言葉を境に、上が宣言部、下が実際のコードを記述する実現部に分かれています。
なので……


implementation

procedure Register;
begin
  RegisterComponents('Samples', [TReCheckBox]);
end;

の下に、

procedure TReCheckBox.SetChecked(value: Boolean);
begin
  FChecked:=value;    //チェック状態の値を書き込み
  Refresh;            //画面更新
end;

と、書き足してください。
FChecked は、チェックの状態を保管します。
Refresh は、画面更新を行い、チェック印を画面に書きます。
あと、この下には、

end.

だけが残っています。
これ、プログラム全体の末尾なので、触らないで残しておいてください。

Captionも追加します。

チェックボックスには、Caption があります。

  • 猫を吸ったことがある

こんな感じで、「何をチェックするんですか?」を書く場所が必要です。

宣言部 { Private 宣言 }に、 Caption の文字列を入れる箱を宣言します。

    { Private 宣言 }
    FChecked:boolean; //チェックされている/いない
    FCaption:String;  //表示文字 <<<これを書き足し

宣言部 { Protected 宣言 }に、 Caption の文字列を書き込む手続きを宣言します。 

    { Protected 宣言 }
    procedure SetChecked(value:boolean);  //Checkedを書き込み
    procedure SetCaption(value:string);   //Captionを書き込む <<<これを書き足し

実現部に Caption の文字列を書き込む手続きの実現部を書き込みます。 

procedure TReCheckBox.SetCaption(value:string);   //Captionを書き込む
begin
// Captionに合わせてサイズ変更
  Canvas.Font:=Self.Font;                      //フォントをコピー
  Self.Width:=Canvas.TextWidth('□ '+value)+8;  //横幅計測
  Self.Height:=Canvas.TextHeight('漢')+8;      //高さ計測

  FCaption:=Value;    //文字を書き込み
  Refresh;            //画面更新
end;

Caption の文字列を表示するのに必要な幅と高さを算出して、Width と Height を書き換えています。その前に、Canvas にフォントを設定しているのは、TextWidth や TextHeight は、Canvs に設定されているフォントを使って、サイズを計るためです。

以上で、コンポーネントの作成に必要なこと をとりあえず、ご説明できました。
でも、[F9]で実行しても、まだ何も表示されません。
画面表示をするには、paintイベントが必要です。マウスでチェック印をつけたり抜いたりするには、Clickイベントが必要です。

つぎは、イベントを実装しますね。

paint、Clickイベントをoverrideします

paintは、コンポーネントを描画する処理を担当します。
Clickは、コンポーネントをクリックしたときの処理を担当します。

プロパティと同じく、宣言部を書いてから、実現部を書きます。
まず、
{ Public 宣言 }の下へpaint、Click のoverridの宣言を書きます。
オーバーライドってついているのは、PaintBoxは、もともとpaint、Click イベントを持っているためです。

 public
    { Public 宣言 }
    procedure   Paint; override;
    procedure   Click; override;

さらに、実現部に次のように書きます。

procedure TReCheckBox.Paint;   //画面にコンポーネントを描きます
begin
  with Canvas do
  begin
    pen.Color:=clGray;
    Font:=Self.Font;      //ReCheckBox.fontをCanvs.fontへ代入

    if FChecked=True then  //チェックの状態を反映します
    begin
      Brush.Color:=clSkyBlue;
      RoundRect(0,0,Width,Height,4,4);
      TextOut(4,4,'■ '+FCaption);
    end
    else
    begin
      Brush.Color:=Self.Color;
      RoundRect(0,0,Width,Height,4,4);
      TextOut(4,4,'□ '+FCaption);
    end;
  end;

  inherited;  //もともとのPaintイベントを呼び出し
end;

procedure TRecheckBox.Click;
begin
  if FChecked=True then  //チェックの状態を反転します
    FChecked:=False
  else
    FChecked:=true;

  Refresh; //画面を更新します。結果的にpaintイベントを呼びます。
  
  inherited;  //もともとのClickイベントを呼び出し
end;

paintは、paintイベントが発生した時に、処理がここへ飛んできます。
コンポーネントは、自分で自分を描かなきゃいけないのです。
既定のpaintイベントを処理する必要もありますから、inherited; は、最後に付けてください。

文字の表示は、TextOut(横,縦,文字列); です。
Canvas が持っているフォントで文字を書きます。
背景色は、Canvas の Brush の色と Style になります。
座標系は、コンポーネントの Canvas のローカルな座標系です。コンポーネントの中だけを考えればよいため、上の例では、横、縦ともにゼロで、コンポーネントの左端上に詰めて描画されます。

RoundRect は、角丸四角を描きます。
書式は、RoundRect(横,縦,幅,高さ,横丸み,縦丸み); です。
Canvas の pen の色で枠線を引き、 brush の色で中を塗りつぶします。

Clickは、Clickイベントが発生した時に、処理がここへ飛んできます。
FChecked を参照し、状態を反転させてから、FChecked へ格納しています。
ReFresh は画面更新を行う指示です。結果、paintイベントが発生して、いま、チェックした状態が描画されます。
もともとのClickイベントを処理できるようにする必要もあるから、inherited; は、最後に付けてください。

コンストラクタとディストラクタについて

宣言部に、 constructor と destructor をこんな感じで追加します。
constructor は、コンポーネントが作成されるときに呼び出されます。コンポーネント内でさらにオブジェクトを何か動的に生成するときなど、初期設定をすることに使います。
destructor は、コンポーネントが廃棄されるときに呼び出されます。

 public
    { Public 宣言 }
    constructor Create(AOwner: TComponent); override;
    destructor  Destroy; override;
    procedure   Paint; override;
    procedure   Click; override;

実現部にも、 constructor と destructor を追加します。

constructor TReCheckBox.Create(AOwner: TComponent);
begin
  //独自にオブジェクトをつくったりメモリーを確保するときは、ここで行う。
 
  inherited Create(AOwner); //既定コンストラクターを呼び出し
end;

destructor  TReCheckBox.Destroy;
begin
 //独自にオブジェクトをつくったりメモリーを確保したら、ここで解放する。
  
  inherited;  //既定ディストラクターを呼び出し
end;

inheritedとは、既定の処理をする指示です。必ず最後に、追加してください。
上の例では、既定にお任せの処理しかしませんが、今後、もっといろんなコンポーネントをつくるときのために、説明で書きました。
もしも、新しく作るコンポーネントに、独自にCanvasを作ったり、メモリーを確保したりするときは、constructorで作成し、destructorで解放してください。
特に、destructorでリソースをWindowsのシステムに返納するのを忘れないでください。

以上で、自作コンポーネントReCheckBox 側のコードは完了です。

親のプログラムに、TRecheckBoxを追加します。

インストールしないで新しいコンポーネントを試すため、システムが自動でしてくれることを、手打ちで代行することになります。

いま、ReCheckBoxの中身を書いてました。
つぎは、親のアプリつまり、のっぺらぼーを呼んでください。
タブから、Unit1 をクリックすると、画面にのっぺらぼーが帰ってきます。

そうしたら、画面の一番下にも「コード、デザイン、履歴」というタブがあります。
コードを選択してください。

uses節に ReCeckBox.pas を追加します。

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, ReCheckBox;

uses節には、システムが予め標準で使うコンポーネントについてファイルを並べています。
新しく作った ReCheckBox は、一番後ろに書き足してください。
あと、ファイル名だけで拡張子の「.pas」は不要です。

宣言部に ReCheckBox1 を追加します。

次に、
type
TForm1 = class(TForm)
の下にも、 { Public 宣言 } がありますよね。
これが、のっぺらぼーこと、Form1 の宣言部です。
ここに、宣言を書きます。

  public
    { Public 宣言 }
    ReCheckBox1:TReCheckBox;  //ReCheckBox1は、TReCheckBoxクラスです。

ReCheckBox1がのっぺらぼーの上に貼りつける自作チェックボックスの名前です。そいつは、TReCheckBoxクラスのオブジェクトです。という意味です。
クラスとインスタンスの関係は、たい焼き型とたい焼きの関係です。
さきほどTReCheckBoxのコンポーネントを書いてたのは、 たい焼きの型をつくる作業 でした。
Form1の上にReCheckBox1を作るのは、TReCheckBoxクラスの型を使って たい焼きを焼く 作業になります。
ReCheckBox1 と、数字の1が名前についているのは、1匹目のたい焼きって意味です。たい焼き型を作ったら、たい焼きはどんどん焼くことができます。チェックボックスは、たくさん使うコンポーネントなので、型を作ると便利なのです。

たいやき美味しいですよね。

実現部 FormCreate で ReCheckBox1 を作り初期化します。

それから、実現部も次のように書きます。
FormCreate は、のっぺらぼー、Form1が作成されたときに呼ばれます。
ここで、ReCheckBox を実際に作成して、初期化します。

procedure TForm1.FormCreate(Sender: TObject);
begin
  ReCheckBox1:=TReCheckBox.Create(self); //コンポーネント作成

  with ReCheckBox1 do //コンポーネントの初期値設定
  begin
    parent:=self;     //コンポーネントの親を設定
    parentfont:=true; //フォントは親のフォントを使用
    top:=150;         //コンポーネントの位置を設定
    left:=50;
    Caption:='猫を吸ったことがある';
  end;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  ReCheckBox1.Free; //コンポーネントを廃棄
end;

FormCreateイベントは、のっぺらぼーこと、Form1が作成される時に呼ばれます。ここで、ReCheckBox1を作成します。
parent:=self; の意味ですが、ここは、Form1の中ですから、selfとはForm1のことです。つまり、ReCheckBoxの親は、Form1視点で言うと「parent := ぼく」 ということです。コンポーネントには、必ず親になるFormが必要です。親がいないと表示されません。

parentfont:=true; も同じです。「僕のフォントを使うよ」って意味です。

FormDestroyイベントは、form1が廃棄される時に呼ばれます。ここで、ReCheckBox1を廃棄します。
システムから借り出したリソースは、返納しましょう。

以上でプログラム作業は完了です。
[F9]キーを押してください。

完成図.jpg

マウスでクリックしたら、■でチェックが入ったら、成功です。

最後にプログラムコードの全文を載せます。

クリックで開きます。
入力がメンドクサイときは、コピペしてください。

 Unit1.pas
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, ReCheckBox, Vcl.ExtCtrls;

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    ReCheckBox1:TReCheckBox;

  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  //新しいコンポーネントをインストールしないでテストします

  ReCheckBox1:=TReCheckBox.Create(self); //コンポーネント作成

  with ReCheckBox1 do //コンポーネントの初期値設定
  begin
    parent:=self;     //コンポーネントの親を設定
    parentfont:=true; //フォントは親のフォントを使用
    top:=50;         //コンポーネントの位置と大きさを設定
    left:=50;

    Caption:='猫を吸ったことがある';
  end;

end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  ReCheckBox1.Free; //コンポーネントを廃棄
end;

end.
 ReCheckBox.pas
unit ReCheckBox;

interface

uses
  System.SysUtils, System.Classes, Vcl.Controls, Vcl.ExtCtrls, Vcl.Graphics;

type
  TReCheckBox = class(TPaintBox)
  private
    { Private 宣言 }
    FChecked:boolean; //チェックされている/いない
    FCaption:String;  //表示文字

  protected
    { Protected 宣言 }
    procedure SetChecked(value:boolean);  //Checkedを書き込み
    procedure SetCaption(value:string);   //Captionを書き込む
  public
    { Public 宣言 }
    constructor Create(AOwner: TComponent); override;
    destructor  Destroy; override;
    procedure   Paint; override;
    procedure   Click; override;

  published
    { Published 宣言 }
     property Checked:boolean read FChecked write SetChecked;
     property Caption:String  read FCaption write SetCaption;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TReCheckBox]);
end;

procedure TReCheckBox.SetChecked(value: Boolean);  //Checkedの状態を書き込む
begin
  FChecked:=value;    //チェック状態の値を書き込み
  Refresh;            //画面更新
end;

procedure TReCheckBox.SetCaption(value:string);   //Captionを書き込む
begin
// Captionに合わせてサイズ変更
  Canvas.Font:=Self.Font;                      //フォントをコピー
  Self.Width:=Canvas.TextWidth('□ '+value)+8;  //横幅計測
  Self.Height:=Canvas.TextHeight('漢')+8;      //高さ計測

  FCaption:=Value;    //文字を書き込み
  Refresh;            //画面更新
end;


constructor TReCheckBox.Create(AOwner: TComponent);
begin
  inherited Create(AOwner); //規定コンストラクターを呼び出し

end;

destructor  TReCheckBox.Destroy;
begin
  inherited;  //規定ディストラクターを呼び出し

end;

procedure TRecheckBox.Click;
begin
  if FChecked=True then  //チェックの状態を反転します
    FChecked:=False
  else
    FChecked:=true;

  Refresh; //画面を更新します。結果的にpaintイベントを呼びます。
  inherited;
end;

procedure TReCheckBox.Paint;   //画面にコンポーネントを描きます
begin
  with Canvas do
  begin
    pen.Color:=clGray;
    Font:=Self.Font;      //ReCheckBox.fontをCanvs.fontへ代入

    if FChecked=True then  //チェックの状態を反転します
    begin
      Brush.Color:=clSkyBlue;
      RoundRect(0,0,Width,Height,4,4);
      TextOut(4,4,'■ '+FCaption);
    end
    else
    begin
      Brush.Color:=Self.Color;
      RoundRect(0,0,Width,Height,4,4);
      TextOut(4,4,'□ '+FCaption);
    end;
  end;

  inherited;
end;

end.
7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?