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?

DelphiAdvent Calendar 2023

Day 22

サークルメニューを作ってみよう

Last updated at Posted at 2023-12-21

お題 サークルメニューを作ってみよう

ゲームやCADで使われているサークルメニューは、マウス操作向けの何気にカッコイイ感じのメニューです。
それをDelphi の VCLアプリで作ってみましょう。

リングメニューお題.jpg

環境と準備

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

画面を準備します

今回は、formをふたつ使います。
form1 がアプリの本体です。
form2 にサークルメニューを表示します。

form1とform2.jpg

ファイル > 新規作成 > Windows VCLアプリケーション
まず、form1 と Vclアプリケーションのプロジェクトを作ります。

コンポーネントのバレットから、ボタン、エデット、ラベルをひとつずつ貼り付けてください。
form1は、form2を呼び出すための台紙として機能します。

form1のプログラムコード(unit1.pas)の全文

Form1 は、サークルメニューを呼びだす台紙になります
button1 をクリックすると、form2のサークルメニューをモード付きで表示します。
結果は、Edit1のテキストに表示します。

form1 (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;

type
  TForm1 = class(TForm)
    Edit1: TEdit;
    Button1: TButton;
    Label1: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

uses Unit2;

procedure TForm1.Button1Click(Sender: TObject);
begin
  form2.showmodal;

  Edit1.Text:=form2.CirCleMenu[Form2.gCircleResult].caption+'が選択されました';
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Label1.Caption:='ねこが現れました。どうする?';
  Button1.Caption:='選択円を表示';
end;

end.

form2のプログラムコード(unit2.pas)の全文

実際にサークルメニューを描く form2 (unit2.pas) プログラムの全文を折り畳みで載せます。

form2 (unit2.pas) のプログラム全文 [クリックで開いてください]
unit Unit2;

interface

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

type TCirCleMenu = record  //選択円のデータを仕舞う構造体
   CirX,             //選択円の中心 X
   CirY:longint;     //選択円の中心 Y
   Caption:string;   //選択円のキャプション
 end;

type
  TForm2 = class(TForm)
    //サークルメニューを表示
    procedure FormPaint(Sender: TObject);

    //メニューを選択
    procedure FormMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);

    //メニューにマウスを載せたとき色を変える
    procedure FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    gCircleResult:longint; //グローバル変数 form1への回答連絡用
    oldpos:longint; //ひとつ前に載せた選択円の場所
    CirCleMenu: array[0..11] of TCirCleMenu; //選択円を12個用意します

    //マウスの座標が選択円の何番目に在るか?
    function  AskCirCleMenuId(x,y:longint):longint;

    //サークルメニューを描画
    procedure DrawCirMenu;
    procedure DrawCirSub(pos,Clr:longint);
  end;

const
   LRadSize =200; //メニュー半径200pix
   SRadSize =50;  //小さいメニュー半径 30pix
   CenterX=LRadSize+SRadSize*2;   //メニュー表示位置中心
   CenterY=LRadSize+SRadSize*2;

var
  Form2: TForm2;

implementation

{$R *.dfm}

//マウスダウンイベントでポチった選択円を判定して、form1へ回答
procedure TForm2.FormCreate(Sender: TObject);
begin
  form2.Width :=(CenterX+LRadSize+SRadSize*2);
  form2.Height:=(CenterY+LRadSize+SRadSize*2);
  BorderStyle:=bsnone;
  BorderWidth:=0;
  font.Name:='Meiryo UI';
  font.Size:=10;
  font.Color:=clGray;

  //テストデータ ネコの気持ちは難しい
  form2.CirCleMenu[0].Caption:='吸う';
  form2.CirCleMenu[1].Caption:='しっぽで遊ぶ';
  form2.CirCleMenu[2].Caption:='一緒にこたつで寝る';
  form2.CirCleMenu[3].Caption:='おもちゃをあげる';
  form2.CirCleMenu[4].Caption:='お風呂に入れる';
  form2.CirCleMenu[5].Caption:='モフモフする';
  form2.CirCleMenu[6].Caption:='猫まんまをあげる';
  form2.CirCleMenu[7].Caption:='かつおぶしをあげる';
  form2.CirCleMenu[8].Caption:='障子でやりたい放題';
  form2.CirCleMenu[9].Caption:='爪とぎ段ボールを用意';
  form2.CirCleMenu[10].Caption:='ボールを投げる';
  form2.CirCleMenu[11].Caption:='猫缶をあげる';
end;

procedure TForm2.FormMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  Pos:longint;
begin
  Pos:=AskCirCleMenuId(x,y);  //ポチった位置を問合せ
  if pos=-1 then
    exit;        //resultが-1はハズレ

  gCircleResult:=Pos; //form1への回答はグローバル変数を使います
  form2.Close;        //form2を閉じます
end;

//マウス移動イベントで選択円のどれに載せたか? 判定して描画します
procedure TForm2.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  pos:longint;
begin
  Pos:=AskCirCleMenuId(x,y);  //ポチった位置を問合せ

  //載せたメニューを黄色に塗ります
  with form2.Canvas do
  begin
    if Pos=-1 then
    begin
      DrawCirSub(oldpos,clSilver);
      exit;
    end;

    //古い位置の円を描き直す
    if OldPos>=0 then
      DrawCirSub(oldpos,clSilver);

    if pos>=0 then
      DrawCirSub(pos,clYellow);

    OldPos:=Pos;  //処理の最後で、古い位置を更新
  end;

end;

//サークルメニューを描画します
procedure TForm2.DrawCirMenu;
var
  i:longint;
  R:TRect;
begin
  with form2.Canvas do
  begin
    //form2の全部を黒ベタ塗り
    R:=rect(0,0,Form2.Width,form2.Height); //矩形領域を作成
    brush.Color:=clblack;    //ブラシの色を黒にする
    FillRect(R);             //矩形領域を塗ります
    TransparentColor:=true;  //透過色の機能を使います
    TransparentColorValue:=clblack;  //透過色は黒色に設定

    //選択円から外れたときの判定のために、背景が必要
    //透過する場所はmousemoveイベントが起きないので判定できないんです
     brush.Color:=clGray;    //ブラシの色を灰色にする
     pen.Color:=clGray;      //ペンの色を灰色にする
     form2.Canvas.Ellipse(CenterX-LRadSize-SRadSize-10,
     CenterY-LRadSize-SRadSize-10, CenterX+LRadSize+SRadSize+10,
     CenterY+LRadSize+SRadSize+10); //背景に灰色の円を描く。

    //見た目重視で真ん中を抜きます
    brush.Color:=clblack;
    form2.Canvas.Ellipse(
      CenterX-LRadSize+SRadSize+10, CenterY-LRadSize+SRadSize+10,
      CenterX+LRadSize-SRadSize-10, CenterY+LRadSize-SRadSize-10);
      //背景に真ん中を黒=透過色で抜きます

    //選択円を12個描きます
    for i:=0 to 11 do
      DrawCirSub(i,clSilver);

  end;
end;

//Posの位置に、Clrの色で、選択円をひとつ分描きます。
procedure Tform2.DrawCirSub(pos,Clr:longint);
var
  rd:Double;
  ax,ay,bx,by:longint;
  TextR:TRect;
begin
  rd:= 3.14 / 6;  //2π で1周なので π / 6 で1/12

  with form2.Canvas do
  begin
      with CirCleMenu[Pos] do
      begin
        CirX:=Trunc(cos(rd*pos-rd*3)*LRadSize+CenterX); //-rd*3 はスタートを
        CirY:=Trunc(sin(rd*pos-rd*3)*LRadSize+CenterY); //零時の位置にするため
                                                        //3引いてます

        //円を描く
        pen.Color:=Clr;     //ペンの色を銀色
        brush.Color:=Clr;   //ブラシの色も銀色

        //楕円を描画(左上x, 左上y, 右下x, 右下y);
        Ellipse(CirX-SRadSize,CirY-SRadSize,CirX+SRadSize,CirY+SRadSize);

        //文字描画
        //10時の位置の座標(ax,ay)
        ax:=Trunc(cos(rd*10-rd*3)*SRadSize+CirX);  //-rd*3 は スタートを零時に
        ay:=Trunc(sin(rd*10-rd*3)*SRadSize+cirY);  //するために3引いてます

        // 4時の位置の座標(bx,by)
        bx:=Trunc(cos(rd*4-rd*3)*SRadSize+CirX);
        by:=Trunc(sin(rd*4-rd*3)*SRadSize+cirY);

        //テキスト矩形領域を作成 4pixは余白
        TextR:=rect(ax+4,ay,bx-4,by);

        //テキスト描画API(キャンバスのハンルドル,
        //Pchar(文字列データ), -1で文字数自動計算,
        //矩形領域はTRect型, DT_WORDBREAKで自動折返し)
        Drawtext(form2.Canvas.handle,Pchar(Caption),-1,TextR,DT_WORDBREAK);
      end;
  end;
end;


//form2を描画するイベントからサークルメニューの描画を呼出
procedure TForm2.FormPaint(Sender: TObject);
begin
  DrawCirMenu;
end;


//問合せ、マウス座標 X,Y は選択円のどこにある?
//時計と同じ配置にしています。回答は、0~11
function  Tform2.AskCirCleMenuId(x,y:longint):longint;
var
  i:longint;
  Lx,Ly,L:longint;
begin
  //選択円12個についてマウス座標X,Yをチェック
  for i:=0 to 11 do
    with CirCleMenu[i] do
    begin
      Lx:=abs(CirX-x)*abs(CirX-x);  // 2点間の距離
      Ly:=abs(CirY-y)*abs(CirY-y);  // べき乗演算子がないので2回掛け
      L:=Trunc(Sqrt(Lx+Ly));        // L=Sqrt( (X2-X1)^2 + (y2-y1)^2 )

      if L<SRadSize then
      begin
        result:=i;  //中心から半径未満の距離X,Yがあるから、選択円の中
        exit;       //判定に通った選択円の番号をresultに返します
      end;
    end;

  result:=-1; //外れているときは-1を返します
end;

end.

プログラムの抜粋説明

実際にサークルメニューを作る Form2 (Unit2.pas)について、抜粋でご説明します。

選択円のデータを仕舞うRecordについて

サークルメニューには、いろんなデザインがありますが、ここでは12か所の選択円があるメニューを作ります。
選択円のデータを保持するため、レコード構造体を作ります。
選択円の中心座標 X,Y と 半径 と 表示する文字列を持てるようにします。


type TCirCleMenu = record  //選択円のデータを仕舞う構造体
   CirX,             //選択円の中心 X
   CirY:longint;     //選択円の中心 Y
   Caption:string;   //選択円のキャプション
 end;
 

recordで保持しているデータは、サークルメニューの選択円で下図のように使います。

form2 の宣言部について

unit2.pasを抜粋

type
  TForm2 = class(TForm)
    //サークルメニューを表示
    procedure FormPaint(Sender: TObject);

    //メニューを選択
    procedure FormMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);

    //メニューにマウスを載せたとき色を変える
    procedure FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    gCircleResult:longint; //グローバル変数 form1への回答連絡用
    oldpos:longint; //ひとつ前に載せた選択円の場所
    CirCleMenu: array[0..11] of TCirCleMenu; //選択円を12個用意します

    //マウスの座標が選択円の何番目に在るか?
    function  AskCirCleMenuId(x,y:longint):longint;

    //サークルメニューを描画
    procedure DrawCirMenu;
    procedure DrawCirSub(pos,Clr:longint);
  end;

FormPaintイベント

フォームを描画するタイミングで自動的に呼ばれるイベントハンドラです。ここから、サークルメニューを描く手続き DrawCirMenu を呼び出します。 

FormMouseDownイベント

フォームの中でマウスのボタンを押し下げたときに発生します。クリックと違い、左右どっちのボタンでも発生します。押し下げただけで発生します。
ここでもらえるマウス座標から、選択円のどれをポチったのかを判定します。

FormMouseMoveイベント

フォームの中でマウスを転がしたら発生するイベントです。マウスを移動させただけで発生します。カッコよさを実現するために、選択しない状態でも、選択円の上にマウスを載せただけで、選択円の色を変えるために使います。
やっぱりカッコ良さは大事です。

AskCirCleMenuId(x,y:longint):longint;

オリジナルで書く手続きです。マウス座標X,Y から選択円の上にマウスがあるのか? を判定して回答を0から11の番号で返します。

DrawCirMenu; と DrawCirSub(pos,Clr:longint);

サークルメニューを実際に描きます。

※手続きの細かい内容は、後述の実現部で説明します。

次にグローバル変数の宣言について

gCircleResult

form2 から form1 へ選択結果を返すための連絡用変数です。

oldpos

ひとつ前にマウスを載せた選択円を覚えるために使います。このデータを使って、マウスが移動したら、古い選択円の絵を、元の色に描き直します。

CirCleMenu: array[0..11] of TCirCleMenu;

さきほど宣言した 選択円のデータを仕舞うためのRecordを配列で12個用意します。
12個って数を決めてますから、配列の宣言方法は array[0..11] of 型 って書き方になります。

実現部にプログラムについて

実現に書くプログラムは、
・サークルメニューの絵を描く手続き
・マウスの位置を調べる手続き
のふたつのグループに分かれます。

DrawCirMenuで、サークルメニューの絵を描きます。 

formには、透過色の設定があります。
TransparentColor:=true; で TransparentColorValue に設定した色を透過するようになります。
このプログラムの例では、form2のウインドウ領域をclBlackで黒ベタに塗ってからの、TransparentColorValue := clblack; なので、真っ黒を透過します。 
次に、選択円を表示する手続きを12回呼び出して、選択円が12個並ぶサークルメニューを描いてます。
あと、黒色を透過しますから、文字フォントの色は、黒以外にしてください。お勧めは、ClGrayです。

//サークルメニューを描画します
procedure TForm2.DrawCirMenu;
var
  i:longint;
  R:TRect;
begin
  with form2.Canvas do
  begin
    //form2の全部を黒ベタ塗り
    R:=rect(0,0,Form2.Width,form2.Height); //矩形領域を作成
    brush.Color:=clblack;    //ブラシの色を黒にする
    FillRect(R);             //矩形領域を塗ります
    TransparentColor:=true;  //透過色の機能を使います
    TransparentColorValue:=clblack;  //透過色は黒色に設定

    //選択円から外れたときの判定のために、背景が必要
    //透過する場所はmousemoveイベントが起きないので判定できないんです
    brush.Color:=clGray;    //ブラシの色を灰色にする
    pen.Color:=clGray;      //ペンの色を灰色にする
     form2.Canvas.Ellipse(CenterX-LRadSize-SRadSize-10,
     CenterY-LRadSize-SRadSize-10, CenterX+LRadSize+SRadSize+10,
     CenterY+LRadSize+SRadSize+10); //背景に灰色の円を描く。

    //見た目重視で背景の真ん中を抜きます。これ、なくても動作します。
    brush.Color:=clblack;
    form2.Canvas.Ellipse(
      CenterX-LRadSize+SRadSize+10, CenterY-LRadSize+SRadSize+10,
      CenterX+LRadSize-SRadSize-10, CenterY+LRadSize-SRadSize-10);
      //背景に真ん中を黒=透過色で抜きます

    //選択円を12個描きます
    for i:=0 to 11 do
      DrawCirSub(i,clSilver);
  end;
end;

DrawCirSubは、サークルメニューの選択円のひとつ分を描きます。

DrawCirMenu 手続きから呼び出されて、Pos(0~11)の位置に、Clrの色で、選択円をひとつ分描きます。
12個の選択円の中心座標は、ここで絵を描くときに使った数値をCirCleNume[pos]のCirXとCirYに格納します。格納した中心座標は、別途、マウスの位置判定の手続きで使用します。

表示文字は、CirCleNume[pos]のCaption から読み出しています。
テスト用の文字列は、form2.Createの中で用意しています。

//Posの位置に、Clrの色で、選択円をひとつ分描きます。
procedure Tform2.DrawCirSub(pos,Clr:longint);
var
  rd:Double;
  ax,ay,bx,by:longint;
  TextR:TRect;
begin
  rd:= 3.14 / 6;  //2π で1周なので π / 6 で1/12

  with form2.Canvas do
  begin
      with CirCleMenu[Pos] do
      begin
        CirX:=Trunc(cos(rd*pos-rd*3)*LRadSize+CenterX); //-rd*3 はスタートを
        CirY:=Trunc(sin(rd*pos-rd*3)*LRadSize+CenterY); //零時の位置にするため
                                                        //3引いてます

        //円を描く
        pen.Color:=Clr;     //ペンの色を銀色
        brush.Color:=Clr;   //ブラシの色も銀色

        //楕円を描画(左上x, 左上y, 右下x, 右下y);
        Ellipse(CirX-SRadSize,CirY-SRadSize,CirX+SRadSize,CirY+SRadSize);

        //文字描画
        //10時の位置の座標(ax,ay)
        ax:=Trunc(cos(rd*10-rd*3)*SRadSize+CirX);  //-rd*3 は スタートを零時に
        ay:=Trunc(sin(rd*10-rd*3)*SRadSize+cirY);  //するために3引いてます

        // 4時の位置の座標(bx,by)
        bx:=Trunc(cos(rd*4-rd*3)*SRadSize+CirX);
        by:=Trunc(sin(rd*4-rd*3)*SRadSize+cirY);

        //テキスト矩形領域を作成 4pixは余白
        TextR:=rect(ax+4,ay,bx-4,by);

        //テキスト描画API(キャンバスのハンルドル,
        //Pchar(文字列データ), -1で文字数自動計算,
        //矩形領域はTRect型, DT_WORDBREAKで自動折返し)
        Drawtext(form2.Canvas.handle,Pchar(Caption),-1,TextR,DT_WORDBREAK);
      end;
  end;
end;

Ellipse で円を描きます。

Ellipseは、対角線で与えられた楕円を描く手続きです。
輪郭線は、Canvasのpenで、塗りつぶしはBrushを用います。

文字表示は、DrawtextというAPIを使います。

DT_WORDBREAKを書式に指定すると、面倒な折り返し処理を丸投げできます。
文字数も、第3パラメータを-1にすると、自動計算してくれます。
文字表示の矩形領域は、TRect型を与えます。

パラメータ 意味
1 hDC  デバイスコンテキストのハンドル。つまり、form2.Canvas.handle
2 lpString 文字列。Pchar(String)でゼロで終わる文字列に変換してます
3 nCount 文字数。-1を指定すると自動計算します
4 lpRect 文字を詰める矩形領域。TRect型
5 uFormat 書式。 DT_WORDBREAKで自動折り返し

※DrawTextを使用するときは、uses節に、System.Typesを書き足してください。

文字を描く矩形領域の計算は、選択円を描くときの計算を流用します。
つまり、選択円の中で10時と4時の座標を使って、選択円に内接する長方形を算出します。

座標の計算はこうなっています。

CenterX,CenterY がサークルメニュー全体の中心です。
さらに、CenterX,CenterYから半径LRadSizeに、30度刻みで12か所に、選択円の中心があります。
選択円は、半径SRadSizeの円を描きます。
選択円の位置は、時計と同じになるように調整しています。
また、マウスが乗っている選択円は、色を変えるようにしています。

リングメニュー座標と計算.jpg

マウスの位置判定をします。

選択円の番号(0~11) := AskCirCleMenuId(マウス座標 x,y); で判定します。
12個の選択円について、マウス座標のX,Y と 選択円の中心と半径を比較して、選択円の中か外かを判定しています。

リングメニュー座標と判定.jpg

選択円とマウス座標の計算.jpg

//問合せ、マウス座標 X,Y は選択円のどこにある?
//時計と同じ配置にしています。回答は、0~11
function  Tform2.AskCirCleMenuId(x,y:longint):longint;
var
  i:longint;
  Lx,Ly,L:longint;
begin
  //選択円12個についてマウス座標X,Yをチェック
  for i:=0 to 11 do
    with CirCleMenu[i] do
    begin
      Lx:=abs(CirX-x)*abs(CirX-x);  // 2点間の距離
      Ly:=abs(CirY-y)*abs(CirY-y);  // べき乗演算子がないので2回掛け
      L:=Trunc(Sqrt(Lx+Ly));        // L=Sqrt( (X2-X1)^2 + (y2-y1)^2 )

      if L<SRadSize then
      begin
        result:=i;  //中心から半径未満の距離に、マウスのX,Yがあるから、選択円の中
        exit;       //判定に通った選択円の番号をresultに返します
      end;
    end;

  result:=-1; //外れているときは-1を返します
end;

マウスの移動 (MouseMoveイベント) に合わせて選択円を描き直します

AskCirCleMenuId( マウス座標 x,y ); で得た選択円の位置 0~11 に合わせて、選択円を描き直します。
古い選択円を銀色で描き直してから、新しい選択円を描き直します。
最後に、古い選択円の番号を更新します。

//マウス移動イベントで選択円のどれに載せたか? 判定して描画します
procedure TForm2.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  pos:longint;
begin
  Pos:=AskCirCleMenuId(x,y);  //度の選択円内にマウスがいるか? を問合せ

  //載せたメニューを黄色に塗ります
  with form2.Canvas do
  begin
    if Pos=-1 then   //-1はすべての選択円の外の余白にマウスがいる
    begin
      DrawCirSub(oldpos,clSilver);
      exit;
    end;

    //古い位置の円を描き直す
    if OldPos>=0 then
      DrawCirSub(oldpos,clSilver);

    if pos>=0 then
      DrawCirSub(pos,clYellow);

    OldPos:=Pos;  //処理の最後で、古い位置を更新
  end;

end;

実行します。

くるっと12か所に、ネコと仲良くなるために必要な選択肢が表示されますか?
マウスを載せると色が黄色になりますか?
ポチれますか?

上手く行ったら、成功です。

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?