ナッキーの「Turbo Delphiはじめて奮戦記」 - 第9回 お絵かきソフトを作ろう

By: Hitoshi Fujii

Abstract: 今回はお絵かきソフトを作ってみましょう。マウスでドラッグしたとおりに線を描いてみます。そのためにはマウスと描画について勉強していきましょう。

Hide image
nacky75

ナッキー

お絵かきソフトだなんて、何にもないところに、どうやって線を描くっていうんだろう?難しそうだけど、ちょっと楽しみ~。

 

Hide image
takahashi75

高橋先生

今回はお絵かきソフトに挑戦してもらおう。難しいって心配しているようだね。ちゃんと絵を描くための方法が用意されているよ。マウスの動きにあわせて線を描くので、マウスの動きにも注目しよう。

    お絵かきソフトの画面を作る

どんなコンポーネントを使うのか見当もつかないなぁ。絵を描く画面と、色や線の種類を変えるツールボックスのようなものが要るかしら?教えて高橋先生!

高橋先生:今回は「画面に線を描く」ということを目標にしよう。だからフォームの中央に描画用TPaintBoxというコンポーネントを置くだけにしておこう。TPaintBoxにはスクロールバーが出ないからTScrollBoxコンポーネントの上に置くといいよ。フォーム全体を使うからTScrollBox にはAlignプロパティでフォームいっぱいに広げてね。

ナッキー:TPaintBoxが絵を描くためのコンポーネントなんですね。

高橋先生:ほかにもいろいろあるけど、今回はTPaintBoxを使う。TPanelコンポーネントにだって線を描くことはできるんだよ。大抵のコンポーネントは描画するためのプロパティを持っているからね。描いた絵をファイルに書き出したり、読み込む機能を追加したいからTPaintBoxコンポーネントの方が今回は都合がいいんだ。

ナッキー:いろいろなコンポーネントで描画できるけど、画像を扱うのはTPaintBoxコンポーネントの方がいいってことね。


はじめに、プロジェクトを新規作成します。操作はいつもどおり、メニューバーの「ファイル(F)|新規作成(N)|VCLフォームアプリケーション-Delphi for Win32(V)」を選択。作成したフォームのプロパティも設定します。フォームにタイトルを表示します。画面左下部の「オブジェクトインスペクタ|プロパティ」ページの「ローカライズ対象」の「Caption」プロパティに、「お絵かきソフト」と入力します。フォームのタイトルバーの文字が、「お絵かきソフト」にかわります。フォームに名前も付けておいたほうがいいのね。「その他」の「Name」プロパティに「frmMain」と入れておきます。フォームのサイズも、絵が描きやすいように少し大きくします。次に、高橋先生の指示通りTScrollBoxの上にTPaintBoxを置きます。TScrollBoxコンポーネントから配置しましょう。画面右下部のツールパレット「Additional」で「TScrollBox」コンポーネントを1つフォームの中央あたりに配置します。このTScrollBoxコンポーネントのNameプロパティとAlignプロパティを設定します。Nameプロパティは「オブジェクトインスペクタ|プロパティ」ページ「その他」の「Name」で「sbxDraw」とします。Alignプロパティは「レイアウト」の「Align」を「alClient」に設定します。絵を描くんだから白いキャンバスがいいわよね。でも、これから上に乗せるTPaintBox では色は指定できないので、TScrollBox のColorプロパティを変更します。「表示」の「Color」を「clWhite」にします。これでTScrollBoxコンポーネントは完成。

ScrollBox1

カテゴリ名

プロパティ名

設定値

その他

Name

sbxDraw

レイアウト

Align

alClient

表示

Color

clWhite


今度はTPaintBoxコンポーネントを配置します。ツールパレット「System」の「TPaintBox」をsbxDrawスクロールボックスの上に配置します。こちらはAlignプロパティを設定しないんですって。最大サイズがフォームの大きさと同じになってしまって、大きな画像を表示しきれないっていうのが理由。なんでもAlignプロパティを設定していいわけじゃないのね。では、ほかのプロパティを設定しましょう。Nameプロパティで名前をつけます。オブジェクトインスペクタ|プロパティ」ページ「その他」の「Name」を「pbxDraw」にします。次に外観を整えます。大きさを「480×640」にします。「レイアウト」の「Height」を「480」に、同じ「レイアウト」の「Width」を「640」にします。フォームの左上にpbxDrawの端を揃えますが、ドラッグでうまくいかない場合はプロパティを使います。LeftプロパティとTopプロパティで設定。「レイアウト」の「Left」を「0」、「Top」も「0」にします。

PaintBox1

カテゴリ名

プロパティ名

設定値

その他

Name

pbxDraw

レイアウト

Height

480

レイアウト

Width

640

レイアウト

Left

0

レイアウト

Top

0


最後にツールパレット「Win32」カテゴリの下のほうにある「TXPManifest」コンポーネントを配置します。フォームの上ならどこに配置してもよかったのよね。これで画面は完成です。ここまでは簡単ね。

保存しておきます。ツールバーの[すべて保存]ボタンをクリックして保存します。ユニット名は「FormDrawing」、プロジェクト名は「Drawing」とします。

Hide image
01画面完成

図01 画面完成

    API関数を使う

さあ、いよいよ絵を描く部分をつくるわよ。お絵かきソフトって、マウスの動きにあわせて線を描くわね。マウスの動きに合わせて、線を描くっていうプロシージャがあればいいのにな。教えて、高橋先生!

高橋先生:プロシージャ1つではちょっと難しいね。はじめにイベントから考えるよ。pbxDrawペイントボックスの上で、マウスのボタンを押したままマウスを動かすと、線を描くようにしよう。pbxDrawペイントボックスがイベントハンドラを設定するコンポーネント。この一文をプログラムに置き換えると、「マウスのボタンを押したまま」は処理の条件、「マウスを動かす」というのがイベント、「線を描く」が処理だね。「マウスを動かす」というイベントは「OnMouseMove」になる。

ナッキー:「OnClick」じゃないのかな?

高橋先生:絵を描くときはマウスのボタンは押したままマウスを動かす。つまりドラッグする。OnClickはマウスのボタンを押した場所から、移動しないでマウスのボタンを離したときのイベントだよ。ドラッグの場合はOnMouseMoveイベントハンドラ内で「マウスのボタンを押したまま」かどうか調べて、マウスのボタンが押されていれば、処理を行う。

ナッキー:OnMouseMoveはドラッグじゃなくても発生するんですね。

高橋先生:マウスのボタンを押していなくても発生するんだ。マウスのボタンが押されているかどうかは、Windows OSのAPI関数を呼び出して調べてみよう。

ナッキー:エーピーアイ関数??

高橋先生:まだ使ったことがなかったか。APIとはApplication Programming Interface の頭文字をとったもの。Windowsが処理している、いろいろな値をプログラマが参照できるように、Windows OSが提供している。その手段がAPI関数なんだ。実は今までにも、API関数を使っている。普段はDelphiがプログラマの代わりにプロシージャやメソッドなどの中で、呼び出していて間接的に使っているんだ。今回のようにプログラマが直接呼び出すこともできる。使い方はTurbo Delphiの関数と同じ。関数名とパラメータを記述すればいい。特に何か定義する必要はないんだよ。「APIの呼び出しが簡単」というのはDelphiの特徴と言える。API関数を使うときの注意点としてはパラメータの型をあわせること。通常使う型とAPIの型とに互換性がない場合もあるから気をつけてね。

変数:=API関数名(パラメータ1,パラメータ2,…);

今回使って欲しいAPI関数はマウスのボタン

GetKeyState(int型パラメータ)

パラメータにはint型という整数型の数値が必要だよ。マウスの左ボタンが「01」などのようにボタンやキーと数値がリンクしている。関数の結果は整数型の一種Short型。パラメータで指定したキーを押していれば、マイナスの数値が返るよ。

GetKeyState関数のパラメータには数値が必要だと説明したけれど、実際には定数を使用する。定数とは値に名前を付けてコード上でわかりやすくしたもの。「01」に「左ボタン」のように名前を付けて使う。実際には全角文字は使えないので「VK_ LBUTTON」と付いている。ほかにも「VK_」が付く定数はいろいろ用意されている。ヘルプで「Virtual-Key Codes」を検索すると、たくさん出てくるから時間があるとき参照してみてもいいね。ただし、キーボードの配列が日本で販売しているキーボード配列の規格ではないので一覧になかったり、異なるキーがあったりする場合がある。確認しながら使用してね。

ナッキー:やってることは難しそうだけど、用意された関数とパラメータを使うだけだから簡単なのね。


では、イベントハンドラを作ってコードを考えてみます。pbxDrawペイントボックスを選択して、「オブジェクトインスペクタ|イベント」ページで「入力」の「OnMouseMove」をダブルクリックします。イベントハンドラが作成できたら「マウスのボタンが押されたら~」のif文を記述します。太字部分を追加します。

procedure TfrmMain.pbxDrawMouseMove(Sender: TObject; Shift: TShiftState; X,   Y: Integer);
begin
  if GetKeyState(VK_LBUTTON) < 0 then
  begin
  …
  …
  end;

end;

これが、線を描くタイミングなのね。

    Canvasプロパティ

いろいろ、やることがあるんだぁ。イベントハンドラは作ったけど、次は何をするのかしら?高橋先生~!

高橋先生:描画のために「Canvas」というプロパティがあるよ。多くのコンポーネントにCanvasプロパティがあるんだ。もちろんTPaintBoxにもあるよ。Canvasプロパティはそれ自身もプロパティやメソッドを持つプロパティ。描く道具としてCanvasの「Pen」プロパティをはじめに設定する。Penプロパティもプロパティを持っている。

ナッキー:プロパティのプロパティのプロパティ??

高橋先生:そう言ってしまうと混乱するけど、絵を描く道具セットとして、Canvasがあってその中のPenについて、設定できるプロパティもあるということだよ。まずはPenプロパティのプロパティを紹介しよう。

  • Mode

モード 

  • Width

ペンの太さ(単位はピクセル)

  • Color

ペンの色

Modeプロパティは色変更できるように「pmCopy」に、Widthプロパティは「5」に、Colorプロパティはお好みで設定してみて。決められなければ「clBlack」にしよう。これらのプロパティが設定できたら準備は完了。

ナッキー:ここまでは準備なんだぁ。


まだ、線は描けないのね。では準備の部分を記述します。太字部分を追加します。

procedure TfrmMain.pbxDrawMouseMove(Sender: TObject; Shift: TShiftState; X,   Y: Integer);
begin

  if GetKeyState(VK_LBUTTON) < 0 then
  begin
    pbxDraw.Canvas.Pen.Mode  := pmCopy;
    pbxDraw.Canvas.Pen.Width := 5;
    pbxDraw.Canvas.Pen.Color := clBlack;
  end;

end;

これで、ペンの準備ができたわね。いよいよ描く部分ね。

高橋先生:書く道具が用意できたら、Canvasが持っているメソッドを使って、線の起点にペンを移動しよう。選んだペンを紙の上に置く動作と同じだね。使うメソッドは「MoveTo」。パラメータは2つで、コンポーネントの左からの距離、上からの距離を示す。「0,0」だと一番左上端を意味する。

    pbxDraw.Canvas.MoveTo(0, 0);

ペンを紙の上におろしたら、今度は線を引っ張るよね。その動作をするのが「LineTo」メソッド。パラメータはMoveToと同じように、コンポーネントの左上からの位置を示す。

    pbxDraw.Canvas.LineTo(X, Y);

今回は、マウスの位置を使おう。イベントハンドラのパラメータに、マウスの左からの位置「X」と、上からの位置「Y」が自動的に入ってくる。そのままLineToのパラメータにつかってみよう。

ナッキー:このメソッドで線がかけるんだ。イベントハンドラのパラメータを流用するって言うのは、イベントハンドラの呼び出しで、Senderパラメータを使ったのと同じね。


イベントハンドラにメソッドを追加します。

procedure TfrmMain.pbxDrawMouseMove(Sender: TObject; Shift: TShiftState; X,   Y: Integer);
begin

  if GetKeyState(VK_LBUTTON) < 0 then
  begin
    pbxDraw.Canvas.Pen.Mode  := pmCopy;
    pbxDraw.Canvas.Pen.Width := 5;
    pbxDraw.Canvas.Pen.Color := clBlack;
    pbxDraw.Canvas.MoveTo(0, 0);
    pbxDraw.Canvas.LineTo(X, Y);
  end;

end;

これで、線が描けるのね。保存して実行してみましょう。ツールバーの[すべて保存]ボタンで保存して、[実行]ボタンをクリックします。じゃあ、マウスでドラッグしてみよう。うぎゃ!なにこれ。変な線がいっぱいできちゃいました。

Hide image
02実行テスト

図02 実行テスト

    グローバル変数

実行してみても線がマウスどおりに引けません。教えて、高橋先生!

高橋先生:左上から放射線状に線が引かれますね。これはMoveToメソッドのパラメータが「0,0」だからです。線の起点が毎回コンポーネントの左上隅に設定されているんだよ。マウスの位置に合わせて移動しよう。

ナッキー:全然うまく動かなくって、びっくりしちゃいました。線の起点をマウスの位置に合わせて移動するのね。OnMouseMoveイベントハンドラのマウスの左ボタンが押されているかどうか判断するif文の前の部分で、マウスの位置を取得すればいいのかな?

高橋先生:そうすると、毎回点になってしまうよ。OnMouseMoveイベントハンドラが動く前に、線の起点を取得しよう。

ナッキー:じゃあ、OnMouseMoveの前のイベントってことかな?

高橋先生:マウスを動かす前に何かしてる?「マウスを動かす」だけに注目すると、その前には何もしていない。イベントは発生しないかも知れないね。だからOnMouseMoveイベントの最後にマウスの位置を取得して、次のOnMouseMoveイベントに備えるんだ。

ナッキー:マウスの位置は、イベントの最後で保存しておくんですね。

高橋先生:OnMouseMoveイベントハンドラで、現在のマウスの位置を別の変数に保存しておくことにしよう。この変数を次に線を描く時の起点にすればいいはずだよ。

ナッキー:XとYパラメータが現在のマウスの位置でしたね。簡単簡単。

高橋先生:じゃあ、コード書いてみて。

ナッキー:いいですよぉ。太字部分を追加します。

procedure TfrmMain.pbxDrawMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
var
  preX : Integer;
  preY : Integer;
begin

  if GetKeyState(VK_LBUTTON) < 0 then
  begin
    pbxDraw.Canvas.Pen.Mode  := pmCopy;
    pbxDraw.Canvas.Pen.Width := 5;
    pbxDraw.Canvas.Pen.Color := clBlack;
    pbxDraw.Canvas.MoveTo(preX, preY);
    pbxDraw.Canvas.LineTo(X, Y);
  end;

  preX := X;
  preY := Y;

end;

これでバッチリでしょ。

高橋先生:せっかくpreXとpreYに代入してくれたけど、代入した値っていつ使うの?

ナッキー:えっとー。次にOnMouseMoveイベントが起こった時でしたっけ?

高橋先生:そうだね。でもこれじゃあ、せっかくpreXやpreYに代入した値が使えないよ。

ナッキー:なんで?

高橋先生:イベントハンドラの中で定義した変数っていつまで使えると思う?

ナッキー:プログラムが動いてる間は使えると思う。

高橋先生:…。デバックツールの勉強をしたとき、一時停止するとコードエディタに表示される緑の矢印があったね。

Hide image
03ステップ実行

図03 ステップ実行

ステップ実行すると1つ1つ矢印が移動する。その矢印が「begin」に入って変数が使えるようになる。そして、「end;」まで来ると変数はなくなってしまうんだ。一回のイベントの間だけ有効な値なんだよ。次のイベントの時には変数はリセットされてしまう。

ナッキー:そんなぁ。じゃあ、どうやって値をとっておくの?

高橋先生:もっと長い間使える変数があるんだよ。有効期間(スコープ)が長い変数のことをグローバル変数というんだ。一方イベントハンドラなどプロシージャ内だけで有効な変数をローカル変数という。お絵かきソフトではグローバル変数を使おう。

ナッキー:今まで使っていたのはローカル変数だったのかぁ。グローバル変数を使えば、値を保存したり、共有したりできるのね。

高橋先生:グローバル変数には有効期間に2種類ある。まず、フォーム内だけで有効な変数、次にプログラムが動いている間有効な変数。例えばフォームだけで有効な変数の場合、ほかのフォームから値を参照することはできないけれど、プログラムが動いている間有効な変数の場合、ほかのフォームから参照することができる。

ナッキー:へぇ、フォームのイベントハンドラから参照するときと、違うフォームから参照するときで違う種類の変数がいるんだね。でもどうやって作るの?

高橋先生:今回使うのはフォーム内だけで有効なグローバル変数だよ。種類が決まったら、宣言してみよう。グローバル変数はコードの上のほう「type」節で宣言する。

ナッキー: type節ってどこかな?

高橋先生:作りかけのコードでいいから、コードエディタで全体を見てみよう。「interface」から「implementation」の前までの「インターフェース部」は、ほかのフォームからも参照できる部分。「implementation」から下の「実装部」は、ほかのフォームからは参照できない部分に分かれている。type節はインターフェース部にある「type」で始まり、「end;」で終わる部分。フォームに乗せているコンポーネントや、イベントハンドラ名などが記述されているところだね。

type節の中にコメントで「{Private宣言}」とか「{Public宣言}」とか書かれた部分がある。ここに変数を宣言するんだ。フォーム内だけで有効な変数は「{Private宣言}」の下に宣言する。プログラムが動いている間有効な変数は「var」の下に宣言する。今回はフォーム内で有効な変数が欲しいので「{Private宣言}」の下に宣言しよう。type節の中に変数名と型名を宣言するよ。

Hide image
04グローバル変数

図04 グローバル変数

ナッキー:インターフェース部って、ほかのフォームから参照できる部分なのに、「{Private宣言}」はフォーム内だけでしか参照できないんですか?

高橋先生:「{Private宣言}」の部分だけ、特別にほかのフォームから隠されているんだ。だから安心して記述してね。


では、マウスの位置を保存しておく変数「preX」、「preY」をInteger型で宣言します。太字部分を追加します。

type
  TfrmMain = class(TForm)
    sbxDraw: TScrollBox;
    pbxDraw: TPaintBox;
    procedure pbxDrawMouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure pbxDrawMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
    preX: Integer;
    preY: Integer;
  public
    { Public 宣言 }
  end;

次にOnMouseMoveイベントハンドラに宣言したpreX変数とpreY変数を消しておきます。グローバル変数よりローカル変数を優先するので、残しておくとうまく動かないんですって。コードエディタでOnMouseMoveイベントハンドラを探して、preX変数とpreY変数を宣言しているところを削除します。

procedure TfrmMain.pbxDrawMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
Var節を削除>
begin

  if GetKeyState(VK_LBUTTON) < 0 then
  begin
    pbxDraw.Canvas.Pen.Mode  := pmCopy;
    pbxDraw.Canvas.Pen.Width := 5;
    pbxDraw.Canvas.Pen.Color := clBlack;
    pbxDraw.Canvas.MoveTo(preX, preY);
    pbxDraw.Canvas.LineTo(X, Y);
  end;

  preX := X;
  preY := Y;

end;

これでOnMouseMoveイベントハンドラの内容が、正しく機能するようになります。

ナッキー:これで、完成だわ!

高橋先生:グローバル変数に「-1」を代入しておこう。使う前にリセットすることを「初期化」という。変数は宣言したときに初期化されて、Integer型の場合「0」が入ってる。それ以外の値で初期化したいときには、フォームのOnCreateで記述しよう。


もうちょっとだから、がんばろっと。では、フォームデザイナの画面でフォームを選択します。マウスで選択しにくいときは、画面左上の構造ペインで「frmMain」を選択すればいいんだったわね。「オブジェクトインスペクタ|イベント」ページで「その他」の「OnCreate」をダブルックリックします。preXとpreYに、線の起点として無効な値「-1」を代入します。太字部分を追加します。

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  preX := -1;
  preY := -1;
end;

これでグローバル変数の初期化ができました。

高橋先生:線を描くコードはOnMouseMoveイベントハンドラにしか記述していないのでマウスを動かさずクリックしただけでは何も描けないんだ。OnMouseDownでマウスの左ボタンが押されていれば、OnMouseMoveイベントハンドラを呼び出そう。こうすれば、長さ1の線(点)も引けるようになるよ。

ナッキー:パラメータは「Sender」のときと同じように流用すればいいんですね。

高橋先生:覚えたてた?OnMouseDownの場合はパラメータがたくさんあるから、書き落としがないように気をつけて。

ナッキー:パラメータは「Sender」だけって限らないんですね。わあ、いっぱいあるー。イベントによって、パラメータもいろいろなんですね。


では、フォームデザイナの画面でpbxDrawペイントボックスを選択します。「オブジェクトインスペクタ|イベント」ページで「入力」の「OnMouseDown」をダブルックリックします。if文でマウスの左ボタンが押されているかどうかチェックして、押されていたらOnMouseMoveイベントハンドラを呼び出します。太字部分を追加します。

unit FormDrawing;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls;

type
  TfrmMain = class(TForm)
    sbxDraw: TScrollBox;
    pbxDraw: TPaintBox;
    procedure pbxDrawMouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure pbxDrawMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
    preX: Integer;
    preY: Integer;
  public
    { Public 宣言 }
  end;

var
  frmMain: TfrmMain;

implementation

{$R *.dfm}

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  preX := -1;
  preY := -1;
end;

procedure TfrmMain.pbxDrawMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  if GetKeyState(VK_LBUTTON) < 0 then
  begin
    pbxDrawMouseMove(Sender, Shift, X, Y);
  end;
end;

procedure TfrmMain.pbxDrawMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
begin

  if GetKeyState(VK_LBUTTON) < 0 then
  begin
    pbxDraw.Canvas.Pen.Mode  := pmCopy;
    pbxDraw.Canvas.Pen.Width := 5;
    pbxDraw.Canvas.Pen.Color := clBlack;
    pbxDraw.Canvas.MoveTo(preX, preY);
    pbxDraw.Canvas.LineTo(X, Y);
  end;

  preX := X;
  preY := Y;

end;

end.

記述できたら保存して実行してみましょう。ツールバーの[すべて保存]ボタンで保存して、[実行]ボタンをクリックします。うまく実行できたら、マウスでドラッグして線を描いてみよう!

Hide image
05完成図

図05 完成図

ナッキー:ちょっと難しかったけれど、自分でお絵かきソフトが作れるなんて、びっくりです。これなら、子供にも使わせてあげられるわ。

高橋先生:Canvasプロパティの使い方を覚えればもっといろいろなことができるよ。

ナッキー:じゃあ、次回はもっと加工するんですね。

高橋先生:実は、このお絵かきソフトの上に違うウィンドウが表示されると、描いた絵が消えちゃうんだ。

ナッキー:せっかく描いたのがなくなっちゃうのは困るなぁ。

高橋先生:だからそうならないように、いろいろやってみよう。

ナッキー:「いろいろ」がちょっと気がかりだけど、次回もがんばってやってみよう!


ナッキーの「Turbo Delphiはじめて奮戦記」

Prev | Next | Index


Server Response from: ETNASC04