ナッキーの「Turbo Delphiはじめて奮戦記」 - 第10回 お絵かきソフトでオブジェクト生成

By: Hitoshi Fujii

Abstract: 前回は線を描画する、お絵かきソフトを作成しました。しかし、ほかの画面が上に重なると、描いた線が消えてしまいましたね。今回はこの不具合の改善と、メモリに関して勉強してみましょう。

Hide image
nacky75

ナッキー

なんとか、って感じだけど、お絵かきソフトが動いて楽しかったな。完成までは、まだ道のりが遠そう。Canvasで、コンポーネントの上に自由に描けるようになるぞ!

 

Hide image
takahashi75

高橋先生

今回は描いた絵が消えてしまう点を改善しよう。描画に必要な「ワザ」があるからマスターしてね。さらに「ワザ」に関連して、メモリの存在も意識してみよう。

    画面に描いた絵が消えちゃう!

画面に描いた絵が消えちゃうのよねぇ。高橋先生が、まずはその状態を再確認しなさいって言っていたわ。

では、さっそく確認してみます。前回保存したファイルを開きます。Turbo Delphiを起動して、画面中央の「ホームページ」で「Drawing.bdsproj」を選択。もし一覧に表示されていなければ、ツールバーの[プロジェクトを開く(Ctrl+F11)]ボタンをクリックします。「プロジェクトを開く」ダイアログボックスから「Drawing.bdsproj」を探します。

開くことができたら、実行してみます。ツールバーの[実行]ボタンをクリック。起動できたら、表示される画面に何か描いてみます。次にコードエディタなどTurbo Delphiの画面をクリックして手前に表示します。お絵かきソフトの画面はTurbo Delphiの画面の後に隠れちゃいますね。

Hide image
01お絵かきソフトに描画

図01 お絵かきソフトに描画 (リンゴを描いた)

Hide image
02TurboDelphiに切り替え

図02 Turbo Delphiに切り替え

次に、タスクバーに表示されている「Drawing」ボタンなどを使って、お絵かきソフトのフォームを手前に表示。すると…

Hide image
03お絵かきソフトに切り替え

図03 お絵かきソフトに切り替え (描いたリンゴが消えた)

ほら、画面を切り替える前に描いていた絵が見えなくなります。どういうことかな?教えて、高橋先生!

高橋先生:フォームの切り替えを行ったとき、描いた線が消えるのはどうしてか、ということだね。フォームを切り替えると、対象になったフォームが前面に出てきて操作できるようになる。この「前面に出て」くるときにWindows OSがフォームを書き直しているんだ。プロパティ設定を基にして、フォームを書き直すので線がなくなってしまう。描画はプロパティで定義されているわけじゃないからね。

ナッキー:フォームの書き直しは避けられないんですか?

高橋先生:Windows OSはペイントボックスだけではなくて、フォームの枠やタイトルバーなども一緒に書き直している。フォームの書き直しをやめてしまうとフォームが見えなくなってしまうね。だから書き直しはやめられない。でも書き直すと何も書かなかった状態にもどってしまう。

ナッキー:書き直しはやめられないのね。じゃあ、フォームの切り替えをできなくするとか。

高橋先生:…。そんなことしたら、ちょっと別のプログラムを使おう、というときに困るんじゃないかな。書き直しても、ちゃんと絵が描かれるように「ダブルバッファリング」という手法を使おう。プログラムの中にペイントボックスと同じようなものを隠しておくんだ。そして、フォームの書き直しを行った後で隠しておいた画像をペイントボックスに写せば、画面上変化がないように見えるよね。

Hide image
04ダブルバッファリング

図04 ダブルバッファリング

ナッキー:えー、隠しておいた画像もWindows OSに書き換えられちゃうんじゃないの?

高橋先生:画面上に表示されない、書き直しの対象じゃないものを使おう。ビットマップオブジェクトなら大丈夫。

ナッキー:ビットマップオブジェクト??でた、オブジェクトだ!

高橋先生:化け物じゃないんだから。オブジェクトなら今までだって、たくさん使っているんだよ。コンポーネントだってオブジェクトだよ。コードを書く上で「名前をつけて使うモノはオブジェクトだ」と考えてもらえれば近いかな。ビットマップオブジェクトは画像を扱うためのオブジェクトだよ。ただし今回使うのは、Turbo Delphiが準備してくれるものではないので、プログラマがやらなくちゃいけないことがいろいろある。

ナッキー:ふぇ~ん。


    絵が消えないようにするテクニック

このままだと、描いた絵が消えちゃうっていうのは、わかったわ。でも「オブジェクト」なんて言われると心配だなぁ。

ナッキー:高橋先生。「いろいろ」って難しいの?

高橋先生:やっていることは簡単だよ。「いろいろ」のなかで重要なのは生成と破棄だね。「生成」とはメモリに読み込むこと。「破棄」とはメモリから消し去ること。オブジェクトは処理をしたりデータを保存したりするためにメモリを使うんだ。メモリは、パソコンを買うときのスペック表に「512MB」などと載っている記憶装置。そのメモリの中に、保存するための場所を確保するのが「生成」だ。そして、使っていたメモリを、ほかのオブジェクトが使えるようにするのが「破棄」になる。今まで、意識しないで使っていたオブジェクトでは、自動的に生成と破棄をやってくれていた。プログラマが作る場合は、どちらも忘れると、うまく動かないから気をつけてね。

Hide image
05オブジェクト生成

図05 オブジェクトの生成

ナッキー:今までも生成とか破棄ってしてたんだぁ。

高橋先生:全部自動的に行われていたんだ。変数は宣言しないと使えないよね。変数の種類によっては、宣言をすると生成も行われるんだよ。

ナッキー:意識してないけど、使っていたのねぇ。じゃあ、プログラマが生成をする場合は、いつ生成や破棄をやるの?

高橋先生:基本的に使う前に生成して、使い終わったら破棄すればいいよ。まずは生成からね。今回の場合を考えてみよう。フォームの切り替えはいつ発生するか、わからないから、生成はフォームの初期処理でやっておこう。

ナッキー:OnMouseMoveイベントじゃだめなの?

高橋先生:画面の状態を、フォームが表示されている間中保持しておきたい。OnMouseMoveで毎回用意したのでは無駄だね。

ナッキー:あと、初期処理ってなんでしたっけ?前にやった覚えはあるんですけど…。

高橋先生:プロジェクトの初期処理は、フォームのOnCreateイベントにコードを記述するんだったよね。ちょっとずつでいいから覚えてね。

ナッキー:そっか。OnCreateで初期処理ね。OnCreateイベントでビットマップオブジェクトの生成をするってことでいいのかしら?

高橋先生:そういうこと。オブジェクトの生成は「Create」メソッドで行う。オブジェクトの種類によってはパラメータが必要なものもあるけれど、ビットマップオブジェクトにはいらない。コードの記述は

変数 := TBitmap.Create;

こんな感じ。変数はTBitmap型で宣言しておいてね。ほかのオブジェクトの場合も「型名.Create」で生成ができるよ。

ちなみにOnCreateイベントって、フォームが画面に表示されるちょっと前って説明したね。もうちょっと厳密にいうと、フォームのCreateメソッドが実行されたとき、つまりフォームを生成した直後っていう意味なんだよ。

ナッキー:Createってオブジェクトを生成するメソッドだったのかぁ。そのイベントだからOnCreateイベントなんだね。


では、TBitmap型の変数を宣言して生成してみます。実行中だったので終了して、コードエディタを表示します。FormCreateイベントハンドラを探します。

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

これで生成ができますよね。

高橋先生:うーん@@。変数の宣言する場所によって有効範囲(スコープ)が変わってくるって話は前回したと思うけど。忘れちゃった?

ナッキー:あははは。ちょっと難しかったんですよねぇ。

高橋先生:…。じゃあ、もう1回説明するね。まず、確認したいのは今回bmpBuf変数をどこで使いたいのかということ。画面の状態は、フォームを表示している間中保存しておきたいんだ。だから変数のスコープは、フォームがある間ずっと必要だね。必要なのは、イベントハンドラの中で宣言するローカル変数じゃなくて、コードエディタの上半分にあるインターフェース部で宣言するグローバル変数なんだ。グローバル変数を宣言する場所は2つ。

  • Type節private宣言
  • Type節public宣言

今回のbmpBuf変数はどっちだと思う?

ナッキー:えーっと、じゃあpub…いや、priv…やっぱりpublic宣言!

高橋先生:今のは一か八かだな。ちゃんと使い分けよう。正解はprivate宣言。フォームがいくつもあって、変数をほかのフォームから直接参照する必要がなければ、public宣言はいらないんだ。フォームが動いている間だけ必要で、ほかのフォームから直接参照しない変数は、private宣言に書いてね。

ナッキー:重要なのは「だれが使いたいか」なのね。


変数宣言の部分から書き直しましょう。太字部分を追加します。

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

var
  frmMain: TfrmMain;

implementation

{$R *.dfm}

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

これで絵を写す準備はできたかな。

高橋先生:bmpBuf変数をもうちょっと初期化したいんだ。

ナッキー:何するんですか?

高橋先生:画像の設定とサイズを決めておこう。画像の設定は「PixelFormat」プロパティ、サイズは「SetSize」メソッドでやっておく。PixelFormatは「pf」ではじまる設定値があって好みに合わせて選ぶことができるよ。今回の設定値は「pf32bit」で、1 ピクセル当たり 32 ビットを使用する。この数値は、Windowsの画面プロパティなんかに出てくるけど、要するにフルカラーということだよ。その他の設定値については、「TPixelFormat 型」をヘルプで参照できる。時間があるときに勉強してみてね。SetSizeメソッドのパラメータはペイントボックスと同じ大きさに設定する。パラメータの1つ目は横幅で「640」、2つ目は縦幅で「480」にしよう。これらのコードをCreateのあとに続けて記述してね。


続きを記述してみます。

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  preX := -1;
  preY := -1;
  bmpBuf := TBitmap.Create;
  bmpBuf.PixelFormat := pf32bit;
  bmpBuf.SetSize(640, 480);
end;

これで初期処理はバッチリね。

    作ったオブジェクトはちゃんと消すこと

うーん、できたみたいだけど、まだあるの?

高橋先生:次はオブジェクトの破棄だよ。オブジェクトの破棄はメモリ解放ともいう。終了処理なんだけど、メモリ解放のためのイベントがある。通常の終了処理はどのイベントだったかな?

ナッキー:フォームのぉ、OnCloseなんとか…。

高橋先生:通常の終了処理はOnCloseQueryだよ。でもメモリ解放はフォームのOnDestroyでやろう。OnDestroyはフォームのOnCreateの逆。フォームが破棄されるときのイベントなんだ。

ナッキー:通常の終了処理とは別に、メモリ解放をまとめた方がいいんだ。

高橋先生:メモリ解放をするには「FreeAndNil」プロシージャを使う。似た処理に「Free」メソッドがあるけれど、二重に解放してエラーになってしまう場合がある。二重解放というのは、メモリが解放されたオブジェクトに対して、再度メモリ解放を要求すること。nilが入っていれば、意図せず解放しようとしても、回避できるよ。Freeメソッドのあと通常の代入文で「nil」を代入してもいいんだけど、FreeAndNilプロシージャなら、一文で解放したあとnilを代入できるよ。

ナッキー:でも「nil」ってはじめて聞く言葉だわ。

高橋先生:nilはメモリを空にする値。ほかの言語では「null」と表されていることが多いと思うよ。FreeAndNilプロシージャには、パラメータにメモリ解放したいオブジェクトが必要。書き方はこんな感じ。

FreeAndNil(bmpBuf);


オブジェクト名をそのまま書けばいいのね。文は短いから簡単ね。では、イベントハンドラを作成します。画面をフォームデザイナに切り替えて、構造ペインからfrmMainフォームを選択。オブジェクトインスペクタ、イベントページの「OnDestroy」をダブルクリックします。表示されるコードエディタでメモリ解放のコードを記述します。太字部分を追加します。

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  FreeAndNil(bmpBuf);
end;

高橋先生:いや、これだけじゃ不十分。もし、ビットマップオブジェクトの生成に失敗していて、できてないのにメモリを解放しようとするとエラーになるからね。オブジェクトがあるかどうかチェックしてからメモリ解放をしよう。

ナッキー:オブジェクトがあるかどうかを調べることができるの。プロパティとかで持っているのかしら?

高橋先生:プロパティの値を調べようとして、もしオブジェクトがなかったらエラーになるよ。プロパティの持ち主はオブジェクトだからね。Assaignedという関数を使おう。オブジェクトがあったらTrue、なかったらFalseの値を返すよ。パラメータは調べたいオブジェクト。

変数 := Assigned(bmpBuf);

関数だから、変数で受けた場合のコードになっている。変数は、TrueかFalseのどちらかの値を持つBoolean型ね。

ナッキー:え?Assigned関数って、調べるだけ?

高橋先生:そういうこと。Assigned関数を使って結果がTrueだったら、オブジェクトはまだ破棄されていない。だから、If文の条件にAssigned関数を使って、TrueのときFreeAndNilプロシージャで破棄することにするんだ。


なるほど。では、FreeAndNilプロシージャをIf文に入れます。太字部分を追加します。

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  if Assigned(bmpBuf) then
    FreeAndNil(bmpBuf);
end;

これで、オブジェクトの破棄ができますね。

    ビットマップオブジェクトにも絵を描く

はぁー、なんかひと仕事した感じ。オブジェクトの生成と破棄ができたわ。OKですよね、高橋先生。

高橋先生:これだけで、完成したわけじゃないよ。せっかく使えるようにしたビットマップオブジェクトを使わなくっちゃね。

ナッキー:そっかぁ、大変だったからこれで終わりかと思っちゃいました。まだ使っていなかったんですね。

高橋先生:今は生成と初期化、破棄ができただけだね。ペイントボックスに画像を描くのと同時に、ビットマップオブジェクトにも画像を描いておこう。書き方はペイントボックスコンポーネントもビットマップオブジェクトも同じ。どちらもCanvasプロパティを持っているので、Penプロパティを設定して、MoveToメソッドとLineToメソッドで線を描こう。

ナッキー:pbxDrawのコードをコピーして使えるの?

高橋先生:うん、コピーした方が早いね。pbxDrawMouseMoveイベントハンドラで、API関数のGetKeyStateを条件文に使ったIf文の中に、pbxDrawのコードをコピーして使ってね。


コードエディタでpbxDrawMouseMoveイベントハンドラを探します。If文の中のコードをコピーして、コードを追加します。ペイントボックスコンポーネントをビットマップオブジェクトに書き換えます。太字部分を追加します。

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);

    bmpBuf.Canvas.Pen.Mode  := pmCopy;
    bmpBuf.Canvas.Pen.Width := 5;
    bmpBuf.Canvas.Pen.Color := clBlack;
    bmpBuf.Canvas.MoveTo(preX, preY);
    bmpBuf.Canvas.LineTo(X, Y);
  end;

  preX := X;
  preY := Y;

end;

これでpbxDrawと同じ画像がbmpBufにも記述できました。

高橋先生:もうちょっとだから、がんばってね。今回の問題は、画面の切り替えのとき描いた画像が消えてしまうことだったね。そこで、表示しない画像にpbxDrawと同じ絵を描いておいて、フォームが切り替わったあとで取っておいた画像を表示するよ。「画像を取っておく」ところまでできているから、今度はフォームの切り替えのとき、「取っておいた画像を出す」ところを作ろう。

ナッキー:フォーム切り替え時のイベントを探して、「取っておいた画像を出す」処理を記述すればいいのね。

高橋先生:フォームの切り替えにはペイントボックスの「OnPaint」イベントを使う。似たタイミングのイベントにフォームの「OnActivate」イベントがあるんだけど、こっちは同一プログラムの中でのフォーム切り替え時にしかイベントが発生しない。違うプログラムに切り替えたときには発生しないんだ。OnPaintイベントは、同一プログラムかどうかにかかわらず、隠れていた部分が表示されるとき発生する。描画にかかわる処理をしているとき、再描画のイベントはOnPaintと覚えておこう。

ナッキー:とにかくOnPaintにコードを書くって覚えます。

高橋先生:次に「取っておいた画像を出す」っていうコードだね。先に、ペイントボックスのCanvasの「CopyMode」プロパティを「cmSrcCopy」に設定しておく。CopyModeプロパティは画像を写すときの手法を決める。cmSrcCopyは上書きするってこと。そして、Canvasプロパティの「Draw」メソッドでbmpBufの画像を描き写す。Drawメソッドのパラメータは①描き始めの左からの位置、②上からの位置、③画像。これら三つを入れよう。一番左上から描くから①も②も「0」、画像はビットマップオブジェクトだね。

ナッキー:プロパティ設定してから、CanvasプロパティのDrawメソッドで絵を写すんですね。


画面をフォームデザイナに切り替えて、pbxDrawペイントボックスを選択します。オブジェクトインスペクタ、イベントページでOnPaintを探してダブルクリックしてイベントハンドラを作成。太字部分を追加します。

procedure TfrmMain.pbxDrawPaint(Sender: TObject);
begin
  pbxDraw.Canvas.CopyMode := cmSrcCopy;
  pbxDraw.Canvas.Draw(0, 0, bmpBuf);
end;

これで、一応完成です。ツールバーの[すべて保存]ボタンで保存して、[実行]ボタンで実行テストします。表示されるペイントボックスに何かを描いてから、コードエディタなどにフォームを切り替えます。ふたたび「お絵かきソフト」プログラムを表示してみます。描いた絵は消えてませんでしたか?終了して、オブジェクト破棄までうまく動くか確認します。

    メモリリーク

ナッキー:オブジェクトの破棄まで作ってみました。「オブジェクト」っていうとちょっとビクビクしちゃうけど、うまく動いてよかったぁ。でもよく考えると、どうせプログラム終了するんだからわざわざ破棄しなくても良さそうですよね。

高橋先生:オブジェクトを使ったら、ちゃんと破棄するのがマナーだよ。もし破棄しないとメモリがほかのことに使えなくなってしまう。1回だけ使うのならそれほど影響はないかもしれないけど、何回もオブジェクトを生成していると、使えない領域が増えてしまうね。これを「メモリリーク」という。そうだ、メモリリークが起こっているかどうか調べる方法がある。「ReportMemoryLeaksOnShutdown」変数にTrueを代入してみよう。

ReportMemoryLeaksOnShutdown := True;

と、書いておくとメモリリークがあったとき、メッセージが表示されるんだ。試しにやってみる?

ナッキー:へぇ。どんなメッセージが出るかみてみたいな。どこに書くんですか?

高橋先生:オブジェクト破棄しているフォームのOnDestroyに書こう。オブジェクト破棄する文はコメントにしておく。コメントは行頭に「//」か「{}」でくくるんだよ。


コードエディタでFormDestroyイベントハンドラを探します。見つかったら、すでに記述しているコードをコメント行にします。最後にメモリリークをチェックするコードを追加。太字部分を追加します。

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  ReportMemoryLeaksOnShutdown := True;
//  if Assigned(bmpBuf) then
//  begin
//    bmpBuf.FreeImage;
//  end;
end;

できたら保存して、実行してみます。そのままフォームの閉じるボタンで終了してみます。わっ、メッセージが出るとわかっていてもドキッとしますね。

Hide image
06メモリリーク

図06 メモリリーク

高橋先生:残っているのが、どんなオブジェクトかがメッセージに現れている。オブジェクトを生成したときには、1度はメモリリークを調べてみるといいね。プログラムに残す必要はないから元の状態に戻そう。メモリリークチェックをコメントに、以前のコードのコメントは取り除いておこう。

ナッキー:せっかく作ったからもったいない気もするけど、メモリリークしちゃダメなんだもんね。元に戻します。


コードエディタでFormDestroyイベントハンドラを探します。見つかったら、メモリリークチェックをコメントに、If文のコメントをはずします。

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
//  ReportMemoryLeaksOnShutdown := True;
  if Assigned(bmpBuf) then
  begin
    bmpBuf.FreeImage;
  end;
end;

もう一度保存して実行します。お絵かきソフトを終了しても、メッセージは出てこなくなりました。

ナッキー:オブジェクトの生成と破棄をやってみましたね。決まったコードを書くだけなんで覚えれば簡単かも。

高橋先生:そうだね。手順は決まりきっているけど、概念はなかなかとっつきにくい。だから、一度に全部理解できなくても、やりながら感覚をつかんでもらえるといいな。

ナッキー:じゃあ、そろそろご飯作らなくっちゃ。今日は肉ジャガなんだー。

高橋先生:まだ、問題を解決しただけで改良はしてないんだけど。

ナッキー:改良するんですかぁ。今回も大変だったんだけどなぁ。

高橋先生:次回はもうちょっと使いやすいように、お絵かきプログラムに手を加える。

ナッキー:改良するなら、色やペンを選べるようにしたいなぁ

高橋先生:そんなふうに、具体的に改良点を想像できると勉強しやすいよ。次回もがんばってね。


※ ビットマップオブジェクトの破棄の説明で、bmpBuf.FreeImage の呼び出しが必要であると記述していましたが、FreeAndNil(bmpBuf)によってイメージの破棄も実行されるため、この記述は不要です。お詫びして訂正します。また、ご指摘いただきありがとうございました。


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

Prev | Next | Index


Server Response from: ETNASC02