第7章 もっと動きを


この章では、アニメーション表示を行って動きのある画面を作ります。つまり、題材として以下のものをとりあげます。

◇動きのある画面表示、つまりアニメーションの描画
◇画面更新時のちらつきを抑えるための、オフスクリーン描画
◇二つのウィンドウで行われる処理のタイミングを合わせるのに必要な、イベント
スケジューリング

これらの題材をプログラミングするために、BeOSのAPIが提供しているクラスのうち、主に以下のものを利用します。

●BView(Interface Kit)
●BBitmap(Interface Kit)
●BLooper(Application Kit)

これらのクラスの間の階層関係を、図7.1に示します。

図 第7章で主に扱うクラス間の階層図
図[7.1] 第7章で主に扱うクラス間の階層図

プログラミングの説明に使うサンプルアプリケーションは、全部で二つ用意しています。7.1節では、画面の更新や再描画のことを深く考えないバージョンのアプリケーションを作り、次に7.2節でそれを改良します。サンプルで扱うアニメーションはとても簡単なものですが、この章の説明を読んでBeOSのAPIをどう利用すればよいのかを理解すれば、高度な処理にも応用できるでしょう。


7.1  時計じかけのアニメーション


7.1.1  サンプルの機能と構造

アニメーション表示を行うためのプログラミングを解説する前に、まずはサンプルアプリケーションについて見ておきましょう。図7.2が、この章で使う“ClockworkWave”という名前のサンプルを動かした様子です。

図 ClockworkWaveのスクリーンショット
図[7.2] ClockworkWaveのスクリーンショット

次に、このアプリケーションの機能、つまりClockworkWaveの外部仕様を述べます。

・起動すると二つのウィンドウを開く。どちらのウィンドウにもメニューバーがあり、"File"メニューから"Quit"項目を選んでアプリケーションを終了できる。また、アバウト項目を選ぶとアバウトダイアログを表示する。

・二つのウィンドウのうち、一方には現在時刻を秒単位で表示する。なお、表示はデジタル形式で行い、一秒ごとに表示内容を更新する。

・もう一方のウィンドウには、中心から外側に向けて拡がっていく波紋のアニメーションを表示する。また、波紋の色を4秒ごとに変化させる。

・ウィンドウを二つとも閉じると、アプリケーションが自動的に終了する。また、一方のウィンドウを閉じるか、または最小化して隠しても、もう一方のウィンドウのメニュー項目を選ぶと開き直すことができる。

上に書いた外部仕様と、図7.2のスクリーンショットからでは実際の動きが分からないかも知れませんね。その場合は、付録に付けたサンプルコード集からClockworkWaveのソースファイルやプロジェクトファイルをコピーし、アプリケーションを作成して動かしてみて下さい。サンプルコード集の中の“7.1_ClockworkWave”というのが、ClockworkWaveの最初のバージョンのフォルダです。また、“7.2_ClockworkWaveR”というのは、この次の7.2節で説明する改良バージョンのフォルダです。できれば両方一度にコピーし、動きの違いを観察してみて下さい。

なお、本章のサンプルからは、それぞれのサンプル間で共通に利用するソースをライブラリとしてフォルダにまとめています。“MyLib”というのが、そのライブラリを収録したフォルダです。ClockworkWaveアプリケーションを作成する場合は、このフォルダを一緒にコピーしないと作成できませんので注意して下さい。つまり“7.1_ClockworkWave”フォルダや“7.2_ClockworkWaveR”フォルダをコピーする時は、必ず“MyLib”フォルダも一緒にコピーする必要があります。

ClockworkWaveアプリケーションの動きが分かったら、次はその内部を見てみましょう。図7.3に、ClockworkWaveのモジュール構成を示します。

図 ClockworkWaveのモジュール構成図
図[7.3] ClockworkWaveのモジュール構成図

図7.3に示したモジュールのうち、中心的な役割を果たすのはClockViewクラスとWaveViewクラス、そしてTimeKeeperクラスです。ClockViewクラスは時刻表示を行うビュークラスで、WaveViewクラスは波紋のアニメーション表示を行うビュークラスです。また、TimeKeeperクラスはイベントスケジューリングを行うクラスで、BLooperのサブクラスとして実装しています。図7.3に示したそれぞれのクラスの概要を、以下に述べます。

■ClockworkWaveAppクラス
ClockworkWaveのアプリケーションクラス。BApplicationクラスのメソッドのうち、フック関数として提供されているReadyToRun()とMessageReceived()、それにAboutRequested()を実装し、またQuit()メソッドを再定義しています。自分以外のモジュールを生成し、それらを管理するのが主な役割です。ウィンドウが二つとも閉じられたとき、自動的にアプリケーションを終了するのは、ClockworkWaveAppクラスとRegularWindowクラスの連携によるものです。

■RegularWindowクラス
ウィンドウクラスです。BWindowクラスのメソッドのうち、Quit()メソッドを再定義しています。前の第6章の説明で使ったSingleWindowクラスやSmartWindowクラスを発展させたもので、ウィンドウが閉じる時にアプリケーションへ送るメッセージコードを、コンストラクタに渡す引数で設定できるのが特徴です。なお、これ以降に登場するサンプルアプリケーションは全てRegularWindowクラスを利用しています。このため、サンプルコード集では、RegularWindowクラスのソースをライブラリ用のフォルダ(“MyLib”)に収録しています。

■ClockViewクラス
時刻表示を行うビュークラスです。TimeKeeperオブジェクトを利用して、1秒ごとに表示内容を更新します。BViewクラスが提供しているフック関数のうち、AttachedToWindow()とDeatchedFromWinodw()、およびDraw()とMessageReceived()を実装しています。

■WaveViewクラス
波紋のアニメーション表示を行うクラスです。TimeKeeperオブジェクトを利用して、4秒ごとに波紋の色を変更します。アニメーション表示は、BWindowクラスとBViewクラスの連携機構を利用し、十分短い間隔でPulse()メソッドを呼び出させて行います。BViewクラスが提供しているフック関数のうち、AttachedToWindow()とDeatchedFromWindow()、およびPulse()を実装しています。なお、Pulse()メソッドについては7.1.3で詳しく説明します。

■TimeKeeperクラス
イベントスケジューリング機能を提供するクラスです。監視用のスレッドを使って、イベントを発生させる時刻に達しているかどうかを定期的に調べ、その時刻に達したらイベントを起こします。TimeKeeperクラスでは、イベントを起こすというのは前もって指定されたターゲットにメッセージを送ることです。前述したClockViewクラスとWaveViewクラスは、それぞれTimeKeeperオブジェクトにイベントを登録し、自分が望む周期でメッセージを送ってもらいます。そして、送られたメッセージに従って時刻の表示を更新したり、また波紋と水面の色を変更するのです。


7.1.2  サンプルのソースコード

前述した、ClockworkWaveアプリケーションを構成するクラスのうち、TimeKeeper以外のもののソースをリスト7.1~7.8に示します。TimeKeeperクラスの説明は7.3節で行うため、ここではソースを示しません。ビュークラスやアプリケーションクラスなど、まずは目に見える部分の動きを司っているものの働きを見ておきましょう。

[リスト7.1] ClockworkWaveApp.h
#ifndef _CLOCKWORK_WAVE_APP_H_
#define _CLOCKWORK_WAVE_APP_H_

#include <app/Application.h>

/* 関連クラス・構造体 */
class	RegularWindow;
class	TimeKeeper;
class	BMenuBar;


/*
 * ClockworkWaveAppクラスの定義
 */
class ClockworkWaveApp : public BApplication {
// メソッド
public:
    // 初期化と解放
    ClockworkWaveApp(void);
    ~ClockworkWaveApp(void);

private:
    // 起動と終了
    void		ReadyToRun(void);
    void		Quit(void);

    // ビュー部品の生成
    void		MakeClockWin(void);
    void		MakeWaveWin(void);
    BMenuBar*	MakeMenuBar(bool forClock);

    // メニューの応答
    void		MessageReceived(BMessage* message);
    void		AboutRequested(void);
    void		CheckWindow(BMessage* message);
    void		ShowClock(void);
    void		ShowWave(void);
	
    // ウィンドウ操作
    void		ShowWindow(BWindow* inWindow);

// データメンバ
private:
    RegularWindow* fClockWin;   /* 時計表示のウィンドウ */
    RegularWindow* fWaveWin;    /* 水面表示のウィンドウ */
    TimeKeeper*    fTimeKeeper; /* タイミング動作用 */
};


#endif  /* _CLOCKWORK_WAVE_APP_H_ */
[リスト7.2] ClockworkWaveApp.cp
#include "ClockworkWaveApp.h"
#include "ClockView.h"
#include "WaveView.h"
#include "TimeKeeper.h"
#include "GUI/RegularWindow.h"
#include "KGUtility/kgDebug.h"

#include <interface/Alert.h>
#include <interface/MenuBar.h>
#include <interface/MenuItem.h>
#include <support/Autolock.h>

/* メッセージのコード */
enum {
    WINDOW_CLOSED = 'wcld',
    SHOW_CLOCK    = 'sclk',
    SHOW_WAVE     = 'swav'
};

/* アバウトダイアログのメッセージ */
const char	kAboutMsg[]	= "ClockworkWave\n\n"
    "Copyright " B_UTF8_COPYRIGHT " 1997-1998 Fort Gunnison, Inc.\n"
    "Author: Shin'ya Koga (koga@ftgun.co.jp)";


/* ClockworkWaveAppクラスの非公開メソッド */
/*
 * 起動と終了; ClockworkWaveApp
 */
void
ClockworkWaveApp::ReadyToRun (void)
{
    /* TimeKeeperオブジェクトを生成して起動 */
    fTimeKeeper = new TimeKeeper();
    fTimeKeeper->Run();
	
    /* 二つのウィンドウを生成して表示 */
    this->MakeClockWin();
    this->MakeWaveWin();
	
    return;
}

void
ClockworkWaveApp::Quit (void)
{
    /* TimeKeeperオブジェクトを停止して解放 */
    (void)fTimeKeeper->Lock();
    fTimeKeeper->Quit();
	
    /* 親クラスのメソッドを実行 */
    BApplication::Quit();
	
    return;
}

/*
 * ビュー部品の生成; ClockworkWaveApp
 */
void
ClockworkWaveApp::MakeClockWin (void)
{
    BMenuBar* aMenuBar;
    BView*    aView;
	
    /* 時計を表示するウィンドウを生成して表示 */
    fClockWin = new RegularWindow(
        BRect(50, 50, 200, 140),
        "Clock Window",
        B_TITLED_WINDOW,
        0,
        WINDOW_CLOSED
    );
    aMenuBar = this->MakeMenuBar(true);
    aView = new ClockView(
        BRect(0, 0, 0, 0), B_FOLLOW_ALL_SIDES, fTimeKeeper
    );
    aView->SetViewColor(0xD0, 0xD0, 0xD0);
    fClockWin->InitContent(aMenuBar, aView);
	
    return;
}

void
ClockworkWaveApp::MakeWaveWin (void)
{
    BMenuBar* aMenuBar;
    BView*    aView;

    /* 水面を表示するウィンドウを生成して表示 */
    fWaveWin  = new RegularWindow(
        BRect(250, 200, 450, 400),
        "Wave Window",
        B_TITLED_WINDOW,
        B_NOT_RESIZABLE|B_NOT_ZOOMABLE,
        WINDOW_CLOSED
    );
    aMenuBar = this->MakeMenuBar(false);
    aView = new WaveView(
        BRect(0, 0, 0, 0), B_FOLLOW_ALL_SIDES, fTimeKeeper
    );
    fWaveWin->InitContent(aMenuBar, aView);

    return;
}

BMenuBar*
ClockworkWaveApp::MakeMenuBar (bool forClock)
{
    BMenuBar* theMenuBar;
    BMenu*    theFileMenu;
	
    /* メニューバーと「File」メニューを生成 */
    theMenuBar = new BMenuBar(BRect(0, 0, 0, 0), B_EMPTY_STRING);
    theFileMenu = new BMenu("File");
    theMenuBar->AddItem(theFileMenu);
	
    /* about項目 */
    (void)theFileMenu->AddItem(
        new BMenuItem(
        "About ClockworkWave" B_UTF8_ELLIPSIS,
        new BMessage(B_ABOUT_REQUESTED)
    )
    );
	
    /* -- */
    (void)theFileMenu->AddSeparatorItem();
	
    /* ウィンドウ表示項目 */
    if (forClock) {
        (void)theFileMenu->AddItem(
            new BMenuItem(
                "Show Wave",
                new BMessage(SHOW_WAVE)
            )
        );
    } else {
        (void)theFileMenu->AddItem(
            new BMenuItem(
                "Show Clock",
                new BMessage(SHOW_CLOCK)
            )
        );
    }
	
    /* -- */
    (void)theFileMenu->AddSeparatorItem();
	
    /* Quit項目 */
    (void)theFileMenu->AddItem(
        new BMenuItem(
            "Quit",
            new BMessage(B_QUIT_REQUESTED),
            'Q'
        )
    );
	
    /* メニューのターゲットを設定 */
    theFileMenu->SetTargetForItems(this);
	
    return theMenuBar;
}

/*
 * メニュー応答; ClockworkWaveApp
 */
void
ClockworkWaveApp::MessageReceived (BMessage* message)
{
    switch (message->what) {
    case WINDOW_CLOSED:
        this->CheckWindow(message); break;
    case SHOW_CLOCK:
        this->ShowClock();          break;
    case SHOW_WAVE:
        this->ShowWave();           break;
    default:
        BApplication::MessageReceived(message);
    }
	
    return;
}

void
ClockworkWaveApp::AboutRequested (void)
{
    BAlert*	alertPanel;
	
    alertPanel = new BAlert("about box", kAboutMsg, "OK");
    (void)alertPanel->Go(NULL);
	
    return;
}

void
ClockworkWaveApp::CheckWindow (BMessage* message)
{
    status_t	sts;
    BWindow*	sender;

    /* 終了したウィンドウを記録 */	
    sts = message->FindPointer(kSender, &sender);
    if (sts != B_OK)
        goto err;
    if (sender == fClockWin)
        fClockWin = NULL;
    else if (sender == fWaveWin)
        fWaveWin = NULL;
    else {
        sts = B_ERROR;
        goto err;
    }
	
    /* 全てのウィンドウが閉じられた場合は終了 */
    if (fClockWin == NULL && fWaveWin == NULL)
        (void)this->PostMessage(B_QUIT_REQUESTED);
	
    return;
err:
    ::Error("ClockworkWaveApp::CheckWindow", sts);
    return;
}

void
ClockworkWaveApp::ShowClock (void)
{
    if (fClockWin == NULL)
        this->MakeClockWin();
    else
        this->ShowWindow(fClockWin);
	
    return;
}

void
ClockworkWaveApp::ShowWave (void)
{
    if (fWaveWin == NULL)
        this->MakeWaveWin();
    else
        this->ShowWindow(fWaveWin);
	
    return;
}

/*
 * ウィンドウ操作; ClockworkWaveApp
 */
void
ClockworkWaveApp::ShowWindow (BWindow* inWindow)
{
    BAutolock	lock(inWindow);
	
    if (inWindow->IsHidden())
        inWindow->Show();
    else if (! inWindow->IsActive())
        inWindow->Activate();
	
    return;
}


/*
 * ClockworkWaveAppクラスの公開メソッド
 */
ClockworkWaveApp::ClockworkWaveApp (void)
    : BApplication("application/x-vnd.FtGUN-ClockworkWave")
{
    fClockWin = fWaveWin = NULL;
    fTimeKeeper = NULL;
}

ClockworkWaveApp::~ClockworkWaveApp (void)
{  /* do nothing */  }
[リスト7.3] RegularWindow.h
#ifndef _REGULAR_WINDOW_H_
#define _REGULAR_WINDOW_H_

#include <interface/Window.h>

/* 文字列定数 */
extern const char	kSender[];


/*
 * RegularWindowのクラス定義
 */
class RegularWindow : public BWindow {
// メソッド
public:
    // 初期化と解放
    RegularWindow(BRect frame, const char* title,
        window_type type, uint32 flags, uint32 quitMsg);
    virtual ~RegularWindow(void);
    virtual void	InitContent(BView* inContent);
    virtual void	InitContent(BMenuBar* inMenuBar, BView* inContent);
	
    // 終了時処理
    void	Quit(void);

// データメンバ
private:
    uint32	fQuitMsg;	/* 終了通知メッセージのコード */
};


#endif  /* _REGULAR_WINDOW_H_ */
[リスト7.4] RegularWindow.cp
#include "RegularWindow.h"
#include <app/Application.h>
#include <interface/MenuBar.h>
#include <support/Debug.h>

/* 文字列定数 */
const char	kSender[]	= "sender";


/*
 * RegularWindowクラスの公開メソッド
 */
RegularWindow::RegularWindow (BRect frame, const char* title,
        window_type type, uint32 flags, uint32 quitMsg)
    : BWindow(frame, title, type, flags, B_CURRENT_WORKSPACE)
{
    fQuitMsg = quitMsg;
}

RegularWindow::~RegularWindow (void)
{  /* do nothing */  }

void
RegularWindow::InitContent (BView* inContent)
{
#if DEBUG
    this->Lock();
    ASSERT(this->IsHidden());
    ASSERT(this->CountChildren() == 0);
    this->Unlock();
#endif

    BRect	myBounds = this->Bounds();
	
    inContent->ResizeTo(myBounds.Width(), myBounds.Height());
    this->AddChild(inContent);
    this->Show();
	
    return;
}

void
RegularWindow::InitContent (BMenuBar* inMenuBar, BView* inContent)
{
#if DEBUG
    this->Lock();
    ASSERT(this->IsHidden());
    ASSERT(this->CountChildren() == 0);
    this->Unlock();
#endif

    BRect	myBounds = this->Bounds();
    BRect	mbarFrame;
	
    this->AddChild(inMenuBar);
    mbarFrame = inMenuBar->Frame();
    inContent->ResizeTo(
        myBounds.Width(), myBounds.Height() - mbarFrame.Height() - 1);
    inContent->MoveBy(0, mbarFrame.Height() + 1);
    this->AddChild(inContent);	
    this->Show();
	
    return;
}

/*
 * 終了時処理; RegularWindow
 */
void
RegularWindow::Quit (void)
{
    BMessage	quitMessage(fQuitMsg);
	
    /* アプリケーションに終了を通知 */
    quitMessage.AddPointer(kSender, this);
    (void)be_app->PostMessage(&quitMessage);
	
    /* 親クラスのメソッドを実行 */
    BWindow::Quit();
	
    return;
}
[リスト7.5] ClockView.h
#ifndef _CLOCK_VIEW_H_
#define _CLOCK_VIEW_H_

#include <interface/View.h>
#include <time.h>

/* 関連クラス・構造体 */
class	TimeKeeper;


/*
 * ClockViewクラスの定義
 */
class ClockView : public BView {
// メソッド
public:
    // 初期化と解放
    ClockView(BRect frame, uint32 resizeMask, TimeKeeper* inTimeKeeper);
    ~ClockView(void);
	
private:
    // 描画処理
    void	AttachedToWindow(void);
    void	DetachedFromWindow(void);
    void	Draw(BRect updateRect);
	
    // メッセージ処理
    void	MessageReceived(BMessage* message);
    void	UpdateTime(void);
	
// データメンバ
private:
    TimeKeeper* fTimeKeeper; /* スケジューラ */
    tm          fTime;       /* 現在時刻 */
};


#endif  /* _CLOCK_VIEW_H_ */
[リスト7.6] ClockView.cp
#include "ClockView.h"
#include "TimeKeeper.h"
#include "KGUtility/kgDebug.h"

#include <support/Debug.h>
#include <stdio.h>

/* メッセージのコード */
enum {
    UPDATE_TIME	= 'uptm'
};


/* ClockViewクラスの非公開メソッド  */
/*
 * 描画処理; ClockView
 */
void
ClockView::AttachedToWindow (void)
{
    status_t     sts;
    ScheduleInfo theSchedule;

    /* スケジュールを登録 */
    theSchedule.client     = this;
    theSchedule.message    = new BMessage(UPDATE_TIME);
    theSchedule.period     = 1;
    theSchedule.first_time = ::real_time_clock();
    sts = fTimeKeeper->InsertSchedule(theSchedule);
    if (sts != B_OK)
        goto err;
	
    /* 現在の時刻を取得 */
    this->UpdateTime();
	
    return;
err:
    ::Error("ClockView::AttachedToWindow", sts);
    return;
}

void
ClockView::DetachedFromWindow (void)
{
    status_t	sts;
	
    sts = fTimeKeeper->RemoveSchedule(this, UPDATE_TIME);
    if (sts != B_OK)
        goto err;
	
    return;
err:
    ::Error("ClockView::DetachedFromWindow", sts);
    return;
}

void
ClockView::Draw (BRect /* updateRect */)
{
    char       timeStr[64];
    rgb_color  orgColor;

    /* 全体をビューの色で塗りつぶしてクリア */
    orgColor  = this->HighColor();
    this->SetHighColor(this->ViewColor());
    this->FillRect(this->Bounds());
    this->SetHighColor(orgColor());

    /* 時刻表示文字列を描画 */
    this->SetFontSize(30);
    sprintf(timeStr, "%02d:%02d:%02d",
        fTime.tm_hour, fTime.tm_min, fTime.tm_sec);
    this->DrawString(timeStr, BPoint(18, 40));
	
    return;
}

/*
 * メッセージ処理; ClockView
 */
void
ClockView::MessageReceived (BMessage* message)
{
    switch (message->what) {
    case UPDATE_TIME:
        this->UpdateTime();		break;
    default:
        BView::MessageReceived(message);
    }
	
    return;
}

void
ClockView::UpdateTime (void)
{
    time_t	curTime = time(0);

    /* 時刻情報を更新 */
    fTime = *(localtime(&curTime));
	
    /* 新しい時刻を描画 */
    this->Draw(this->Bounds());
	
    return;
}


/*
 * ClockViewクラスの公開メソッド
 */
ClockView::ClockView (
        BRect frame, uint32 resizeMask, TimeKeeper* inTimeKeeper)
    : BView(frame, "ClockView", resizeMask, B_WILL_DRAW)
{
    ASSERT(inTimeKeeper != NULL);

    fTimeKeeper = inTimeKeeper;
    fTime.tm_hour = fTime.tm_min = fTime.tm_sec = 0;
}

ClockView::~ClockView (void)
{  /* do nothing */  }
[リスト7.7] WaveView.h
#ifndef _WAVE_VIEW_H_
#define _WAVE_VIEW_H_

#include <interface/View.h>

/* 関連クラス・構造体 */
class	TimeKeeper;


/*
 * WaveViewクラスの定義
 */
class WaveView : public BView {
// メソッド
public:
    // 初期化と解放
    WaveView(BRect frame, uint32 resizeMask, TimeKeeper* inTimeKeeper);
    ~WaveView(void);

private:
    // 描画処理
    void	AttachedToWindow(void);
    void	DetachedFromWindow(void);

    // メッセージ処理
    void	MessageReceived(BMessage* message);
    void	Pulse(void);
    void	UpdateColor(void);
	
// データメンバ
private:
    TimeKeeper* fTimeKeeper;	/* スケジューラ */
    uint8       fCurrColor;		/* 水面の色 */
    float       fWaveRadius;	/* 波紋の半径 */
    BPoint      fWaveOrigin;	/* 波紋の中心 */
};


#endif  /* _WAVE_VIEW_H_ */
[リスト7.8] WaveView.cp
#include "WaveView.h"
#include "TimeKeeper.h"
#include "KGUtility/kgDebug.h"

#include <interface/Screen.h>
#include <interface/Window.h>
#include <support/Debug.h>

/* メッセージのコード */
enum {
    UPDATE_COLOR = 'upcl'
};

/* その他の定数 */
const float	kInitialRadius	= 10;	/* 波紋半径の初期値 */


/* WaveViewクラスの非公開メソッド */
/*
 * 描画処理; WaveView
 */
void
WaveView::AttachedToWindow (void)
{
    status_t     sts;
    ScheduleInfo theSchedule;
    BRect        myBounds = this->Bounds();
	
    /* スケジュールを登録 */
    theSchedule.client     = this;
    theSchedule.message    = new BMessage(UPDATE_COLOR);
    theSchedule.period     = 4;
    theSchedule.first_time = ::real_time_clock() + 4;
    sts = fTimeKeeper->InsertSchedule(theSchedule);
    if (sts != B_OK)
        goto err;
	
    /* 水面の色の初期値を設定 */
    this->UpdateColor();
    this->Window()->SetPulseRate(100 * 1000);
	
    /* 波紋の原点座標を決定 */
    fWaveOrigin.x = myBounds.Width() / 2;
    fWaveOrigin.y = myBounds.Height() / 2;
	
    return;
err:
    ::Error("WaveView::AttachedToWindow", sts);
    return;
}

void
WaveView::DetachedFromWindow (void)
{
    status_t	sts;
	
    sts = fTimeKeeper->RemoveSchedule(this, UPDATE_COLOR);
    if (sts != B_OK)
        goto err;
	
    return;
err:
    ::Error("WaveView::DetachedFromWindow", sts);
    return;
}

/*
 * メッセージ処理; WaveView
 */
void
WaveView::MessageReceived (BMessage* message)
{
    switch (message->what) {
    case UPDATE_COLOR:
        this->UpdateColor();	break;
    default:
        BView::MessageReceived(message);
    }
	
    return;
}

void
WaveView::Pulse (void)
{
    BScreen   myScreen(this->Window());
    rgb_color orgRGBColor = this->HighColor();
    uint8     waveColor;

    /* 波紋の色を決定 */	
    waveColor = myScreen.InvertIndex(fCurrColor);
    if (fCurrColor == 247)
        waveColor += 1;
    else if (fCurrColor == 16)
        waveColor -= 2;

    /* 波紋を現在位置に描画 */
    this->SetHighColor(myScreen.ColorForIndex(waveColor));  /* 波紋の色 */
    if (fWaveRadius == kInitialRadius) {  /* 初期状態 */
        this->FillEllipse(fWaveOrigin, fWaveRadius * 2, fWaveRadius * 2);
    } else {  /* 拡がった状態 */
        this->StrokeEllipse(fWaveOrigin,
        kInitialRadius + fWaveRadius, kInitialRadius + fWaveRadius);
        this->SetHighColor(myScreen.ColorForIndex(fCurrColor));
        this->FillEllipse(fWaveOrigin,
        fWaveRadius - kInitialRadius, fWaveRadius - kInitialRadius);
    }
    this->SetHighColor(orgRGBColor);  /* 元のペンカラーを復元 */

    /* 波紋の半径値を更新 */
    if ((fWaveOrigin.x * sqrt(2.0)) <= (fWaveRadius - kInitialRadius))
        fWaveRadius = kInitialRadius;
    else
        fWaveRadius += 1;
	
    return;
}

void
WaveView::UpdateColor (void)
{
    ASSERT(this->Window() != NULL);

    BScreen   myScreen(this->Window());
    rgb_color newColor;

    /* 波紋の色を算出 */
    newColor = myScreen.ColorForIndex(fCurrColor--);
	
    /* 使う色がパレットを一周したら最初に戻す */
    if ((int16)fCurrColor < 0)
        fCurrColor = 255;
	
    return;
}


/*
 * WaveViewクラスの公開メソッド
 */
WaveView::WaveView (
        BRect frame, uint32 resizeMask, TimeKeeper* inTimeKeeper)
    : BView(frame, "Wave", resizeMask, B_PULSE_NEEDED)
{
    ASSERT(inTimeKeeper != NULL);
	
    fTimeKeeper = inTimeKeeper;
    fCurrColor  = 255;
    fWaveRadius = kInitialRadius;
}

WaveView::~WaveView (void)
{  /* do nothing */  }

最初はアプリケーションクラスです。リスト7.2を見て下さい。ClockworkWaveAppクラスでは、ReadyToRun()メソッドでTimeKeeperオブジェクトを生成し、Quit()メソッドでそれを解放しています。

ClockworkWaveAppクラスのメソッドのうち、MakeClockWin()とMakeWaveWin()、それからMakeMenuBar()については特に説明しません。これらはウィンドウやメニューバーを生成するものですが、前の章の説明で使ったサンプル(TinyApp)と大体同じだからです。ただし、ウィンドウオブジェクトとしてRegularWindowクラスのインスタンスを生成する際、コンストラクタの第五引数に“WINDOW_CLOSED”というメッセージコードを渡していることに着目して下さい。RegularWindowクラスの概要説明でも述べたように、このメッセージコードは、ウィンドウが閉じる時にアプリケーションオブジェクトへ送られるメッセージに使われます。

ClockworkWaveAppクラスはのMessageReceived()メソッドでは、システムで定義されている以外のメッセージ、すなわちClockworkWaveアプリケーションで独自に定義した三つのメッセージを処理します。引数に受け取ったメッセージが自分で定義したものではない場合、親クラスであるBApplicationのメソッドを呼び出して処理を引き継ぎます。これを忘れると、システムで定義されたものを処理することができません。BApplicationクラスの場合ですと、第8章で説明するスクリプティング用のメッセージが処理されなくなってしまいます。

ClockworkWaveAppクラスで定義した三つのメッセージについて、その意味や働きを以下に示します。

○WINDOW_CLOSED
ウィンドウが閉じた時に、RegularWindowクラスの働きによって送られます。メッセージには、送り主のウィンドウオブジェクトを指すポインタを記録した"sender"(kSender)という名前のデータ項目が含まれており、これを使って送り主を特定します。

○SHOW_CLOCK
"File"メニューから"Show Clock"項目を選んだ時に送られます。

○SHOW_WAVE
"File"メニューから"Show Wave"項目を選んだ時に送られます。

上に示した各々のメッセージを処理するメソッド、すなわちメッセージハンドラについて、その働きを以下に示します。

○CheckWindow()
WINDOW_CLOSEDメッセージに応答し、どのウィンドウが閉じられたかを記録します。ウィンドウが二つとも閉じられた場合は、自身に対して終了要求メッセージを送ってアプリケーションを終了します。

○ShowClock()
SHOW_CLOCKメッセージに応答し、時刻を表示するウィンドウを開きます。ウィンドウオブジェクトが存在していなければ生成し、そうでない場合はShowWindow()メソッドを呼び出します。

○ShowWave()
SHOW_WAVEメッセージに応答し、波紋のアニメーションを表示するウィンドウを開きます。ShowClock()と同様。

ShowClock()とShowWave()から呼び出されるShowWindow()メソッドは、ClockworkWaveAppクラスで独自に定義したものです。このメソッドでは、Hide()メソッドによってウィンドウが隠されていればウィンドウオブジェクトに対してShow()メソッドを呼び出し、再表示します。また、最小化して隠されていた場合や、現在フロントウィンドウになっていない場合には、Activate()メソッドを呼び出して表示します。以下に、ここで使っているウィンドウ操作用のAPIを示します。なお、これらのAPIの詳細についてはBeOSのAPIリファレンスを参照して下さい。

○IsHidden()
ウィンドウオブジェクトがHide()メソッドによって隠されているか、または生成直後でShow()メソッドが呼び出されていない場合、trueを返します。

○IsActive()
ウィンドウがアクティブであるかどうか、すなわちフロントウィンドウかどうかを返します。

○Show()
Hide()メソッドによって隠されたウィンドウを、再び画面に表示します。ただし、ウィンドウが生成された後で最初に呼び出された場合は特別で、ウィンドウスレッドを生成してメッセージループの開始処理を行います。

○Activate()
ウィンドウをフロントウィンドウにするか、またはフロントウィンドウの状態を解除します。引数にtrueを与えると、ウィンドウをフロントウィンドウにします。このメソッドの引数はオプションであり、何も指定しない場合はtrueが与えられます。なお、IsHidden()の戻り値がtrueである状態では、このメソッドを呼び出してもフロントウィンドウにすることはできません。

IsHidden()やIsActive()メソッドを呼び出す場合、ウィンドウオブジェクトがアプリケーションのメインスレッドとは独立したスレッドによって動作しているため、ウィンドウにロックをかけないと正しい結果が得られない場合があります。リスト7.2では、BAutolockクラスを利用してウィンドウにロックをかけていることに注意して下さい。

アプリケーションクラスの次は、ウィンドウクラスです。リスト7.3と7.4を見て下さい。リストの内容を見ると分かるように、ここで使っているRegularWindowクラスは、前の章の説明で使ったSingleWindowクラス(リスト6.3, 6.4)と大体同じです。違うのは、ウィンドウが閉じた時に送るメッセージのコードを記憶しておく“fQuitMsg”というデータメンバを追加したのと、またメニューバーとビューオブジェクトを受け取るバージョンのInitContent()メソッドを追加したことです。ただし、追加されたバージョンのInitContent()メソッドも、前の章のSmartWindowクラス(リスト6.15,6.16)で説明済みです。したがって、RegularWindowクラスについてはこれ以上説明しません。

アプリケーションクラスとウィンドウクラスの説明が終わったので、次はメインとなるビュークラスを説明します。リスト7.5と7.6に示したClockViewクラスのソース、およびリスト7.7と7.8に示したWaveViewクラスのソースを見て下さい。これらのクラスのメソッドのうち、主なものの働きを以下に示します。

(1)ClockViewクラスの主要メソッド
○AttachedToWindow()
TimeKeeperオブジェクトに対し、1秒おきに時刻更新イベントを起こすように登録します。この結果、以後は1秒に1回“UPDATE_TIME”メッセージが送られてくるようになります。また、UpdateTime()メソッドを呼び出し、画面に表示されたら直ちに現在時刻を表示できるようにします。

○DeatchedFromWindow()
TimeKeeperオブジェクトから、登録したイベントを削除します。

○Draw()
DrawString()メソッドを使って、現在時刻をデジタル表示します。時刻表示に使う文字列のフォントサイズを大きなものにするために、SetFontSize()メソッドでフォントサイズを変更しています。

○MessageReceived()
UPDATE_TIMEメッセージを受け取ったら、UpdateTime()メソッドを呼び出します。それ以外の場合は、BViewクラスのメソッドを呼び出して処理を引き継ぎます。

○UpdateTime()
localtime()を使って現在時刻を取得し、データメンバにセットします。その後、Draw()メソッドを呼び出して再描画を行います。

(2)WaveViewクラスの主要メソッド
○AttachedToWindow()
TimeKeeperオブジェクトに対し、4秒おきに色の更新イベントを起こすように登録します。この結果、以後は4秒に1回“UPDATE_COLOR”メッセージが送られてくるようになります。また、水面の色の初期値を設定するためにUpdateColor()メソッドを呼び出します。その他、円形の波紋を表示する際の原点座標の算出と、Pulse()メソッドの呼び出しレート設定を行っています。

○DeatchedFromWindow()
TimeKeeperオブジェクトから、登録したイベントを削除します。

○MessageReceived()
UPDATE_COLORメッセージを受け取ったら、UpdateColor()メソッドを呼び出します。それ以外の場合は、BViewクラスのメソッドを呼び出して処理を引き継ぎます。

○Pulse()
波紋の表示を更新します。波紋は水面、つまりウィンドウの中心から外側に向けて拡がっていきます。リスト7.8では、前回の呼び出しよりも1ポイント(注7-1)だけ大きい円弧を描いた後、同じく前回よりも1ポイント大きい円を水面の色で塗りつぶしています。この結果、一定の幅を保った波紋が、Pulse()の呼び出しごとに1ポイントずつ半径を拡げながら水面を伝わっていくのです。この波紋描画アルゴリズムは本章の説明の中心ではありませんから、これ以上の説明は省略します。詳細はリスト7.8を参照して下さい。

○UpdateColor()
水面の表示に使う色を変更します。WaveViewでは256色表示を行い、0番目の色から255番目の色まで、UpdateColor()が呼ばれるたびに一つずつ色を変えていきます。また、波紋の色は水面の色を逆転させたものにしているのですが、そのためにBScreenクラスのInvertIndex()メソッドを利用しています。

ここで、WaveViewクラスが利用しているBScreenクラスについて簡単に説明しておきましょう。このクラスは、スクリーンのサイズや色深度など、モニタやビデオカードの特性情報にアクセスするためのもので、Interface Kitで提供されています。リスト7.8では、256色の中からインデックスで指定したものを取得するColorForIndex()、および逆転色のインデックスを取得するInvertIndex()メソッドを使っています。なお、BeOSは1998年2月の時点でマルチスクリーンに対応していませんが、BScreenクラスのインタフェースはマルチスクリーンを意識したものになっています。スクリーンの指定はBScreenのコンストラクタに渡す引数を使って行い、スクリーンのIDかまたはウィンドウによって指定します。何も引数を与えない場合は、メインスクリーンのIDを指定したことになるのですが、リスト7.8では念のためにウィンドウを使ってスクリーンを指定しています。詳細はAPIリファレンスを参照して下さい。

(注)7-1
BeOSでは、描画システムの座標単位は1/72インチになっており、他のOSの場合と変わりません。ただし、APIが扱う座標値の型がintではなくfloatなのが特徴的です。


7.1.3  Pulse()のどこがいけないの

ClockworkWaveアプリケーションの改良バージョンに話を進める前に、WaveViewクラスで再定義しているPulse()メソッドの話をしておくことにします。このメソッドは一定の間隔で呼び出されるフック関数で、BViewクラスのコンストラクタに渡す第四引数のフラグを使って呼び出しを制御します。つまり、フラグの値に“B_PULSE_NEEDED”を含めると呼び出しが行われ、含めない場合は呼び出しが行われません。リスト7.8でWaveViewクラスのコンストラクタを見ると、たしかにBViewクラスのコンストラクタの第四引数に“B_PULSE_NEEDED”を渡しています。

Pulse()メソッドが呼び出される間隔は、BWindowクラスのSetPulseRate()を使って指定します。このメソッドの引数には、呼び出し間隔をマイクロ秒単位で指定した値を渡します。つまり、引数に1000*1000を渡せば1秒ごとにPulse()メソッドが呼び出されるのです。あれ?ちょっと待って下さい。SetPulseRate()で指定した間隔でPulse()メソッドが繰り返し呼び出されるのなら、わざわざTimeKeeperなんていうクラスを作らずに、全部Pulse()メソッドを利用して済ませれば良かったのではないでしょうか。

そうしなかった理由は、実はWaveViewクラスにあります。WaveViewは波紋のアニメーションを表示しますが、その波紋の色は4秒ごとに変化します。つまり、アニメーション表示のためのタイミング処理の他に、波紋の色を変えるという、別のタイミング処理を行なう必要があるのです。しかし、Pulse()メソッドだけでは、複数のタイミング処理を同時に扱うことはできません。ビュークラスのPulse()メソッドは、そのビューオブジェクトが所属するウィンドウオブジェクトに対しSetPulseRate()メソッドを呼び出して設定した、一定の繰り返し間隔で呼び出されるだけだからです。つまり、タイミング処理の設定はウィンドウごとに一種類しかできないのです。これが、TimeKeeperクラスを導入した最大の理由です。

Pulse()メソッドは、一定間隔で行う繰り返し処理向けであり、指定した時刻に一回だけイベントを起こしたいという場合には利用できません。結局、Pulse()メソッドが適しているのは、WaveViewが行うアニメーション表示のように、比較的簡単な繰り返し処理なのです。繰り返しの間隔に正確さを要求される場合や、また時刻を定めて行う処理には使えません。逆に、単純な繰り返し処理であれば、とても簡単に利用できます。アニメーション表示とは少し違いますが、プログレスバーの表示を行う場合に利用すると、表示の更新処理がとても簡単になるでしょう。

7.3節で見るように、TimeKeeperクラスの方は汎用のイベントスケジューラであり、Pulse()メソッドには適していない処理にも利用することができます。


7.2  ちらつきを無くすには

ClockworkWaveアプリケーションの内部を理解したら、次は改良バージョンに進みましょう。どこを改良したのか分からない人は、7.1.1に戻って実際の動きを確認して下さい。二つのバージョンを動かして比べてみれば、その違いが分かるはずです。


7.2.1  改善目標

念のために書いておくと、最初のバージョンには以下の欠陥があります。

・時刻表示を行うウィンドウ("Clock Window")で、表示を更新するたびに画面がちらつく。

・波紋のアニメーション表示を行うウィンドウ("Wave Window")は、他のウィンドウを上に置いて隠したあと、再描画させると隠れていた部分が白くなってしまう。

"Clock Window"で画面がちらつくのが分からない場合は、ウィンドウをリサイズして拡げてみて下さい。ちらつきが大きくなるので、分かりやすくなります。また、WaveWindowでの再描画の問題については、その様子を撮ったスクリーンショットを図7.4に示します。

図 再描画されずに一部が白くなったWave Windowの画面
図[7.4] 再描画されずに一部が白くなったWave Windowの画面

これから説明する改良バージョンでは、上に挙げた二つの欠陥を改善します。改良バージョンのソースコードを見る前に、どのようにして改善するのかを説明することにします。その説明を読めば、BeOSのAPIが提供するクラスの一つを利用するだけで、二つの欠陥を克服できることが分かるでしょう。


7.2.2  オフスクリーン描画とBBitmap

まず、"Clock Winodw"で画面がちらつく原因を説明します。ちらつきが起きるのは、現在時刻、つまりウィンドウの表示内容を更新する際に、描画処理の途中経過が見えてしまうからなのです。"Clock Window"ではグレーの背景に黒で現在時刻を表示しますが、この表示を更新する時は、いったん背景色のグレーで塗りつぶし、それから時刻を示す黒い文字列を描き直します(注7-2)。背景色で塗りつぶすのは、単純に文字列を重ね描きしたのでは更新前の文字列が残ってしまうからです。つまり、一度背景色を使って文字列を消し、それから文字列を描き直します。そして、この様子がそのまま見えるために、ちらつきが起きるのです。

では、どうすればいいのでしょうか?答えは簡単。途中経過を見せなければよいのです。もしあなたが料理番組のファンなら、ヒントはそこに転がっています。料理番組では、シチューの作り方を説明するのに、テレビカメラの前で5時間煮込んで見せたりはしません。下ごしらえが済んだ材料を鍋に入れて火にかけたら、次は、前もって煮込んでおいた別のシチュー鍋を取り出し、それから説明を続けるでしょう。これと同じことをすればよいのです。

そう、目に見えるスクリーンとは別の場所で描画処理を済ませてしまい、それから結果だけをスクリーンに送れば、途中経過は見えません。これは「オフスクリーン描画」と呼ばれるやり方で、ちらつきを無くすために使われる一般的なものです。BeOSでは専用のAPIが提供されており、これを使えば、オフスクリーン描画を簡単に行うことができます。

BeOSでオフスクリーン描画を行うには、BBitmapクラスとBViewクラスを利用します。BBitmapは、オフスクリーン描画に使うビットマップデータ領域(一般的に、これは「オフスクリーンバッファ」と呼ばれます)を内部に持っており、まずその中に描画内容を書き込んで保存します。次に、BViewクラスのDrawBitmap()メソッドを呼び出してBBitmapオブジェクトを渡せば、それで終わりです。あとは、BViewクラスの働きによってオフスクリーンバッファの内容がスクリーンに転送されます。図7.5に、BBitmapクラスを使ったオフスクリーン描画処理の流れを示します。

図 BBitmapによるオフスクリーン描画の流れ
図[7.5] BBitmapによるオフスクリーン描画の流れ

さて、BBitmapクラスを利用してオフスクリーン描画を行えば、"Clock Window"で画面がちらつくのを直せることが分かりました。次は、"Wave Window"で表示内容が消えてしまう問題です。"Wave Window"を他のウィンドウで隠した時に表示内容が消えてしまうのは、一回の描画処理では表示内容の一部しか描画していないせいです。

リスト7.8で見たように、WaveViewクラスでは画面全体を描き直すのではなく、一部のみを描き直し、それを繰り返すことで滑らかなアニメーション表示を行っています。しかし、それだけでは再描画の時に問題が起きてしまうというわけです。どうすれば良いのでしょうか?「"Clock View"と同じように、一度オフスクリーンに描画してからスクリーンに転送するよう変更する。」惜しい、もう一工夫です。せっかくオフスクリーン描画を使わずに滑らかなアニメーション表示ができていたのですから、それを活かしたいと思います。

ここでは、表示内容のバックアップとしてオフスクリーンバッファを利用します。つまり、スクリーンに描画するのと同時に、同じ内容をオフスクリーンにも描画しておき、再描画の時にそれを利用するのです。実際の処理手順については、サンプルを使って説明します。

(注)7-2
再描画を行う際、直接Draw()メソッドを呼び出すのではなく、Invalidate()メソッドを使って間接的にDraw()メソッドを呼び出させることもできます。その場合、Invalidate()メソッドで指定された再描画領域がウィンドウオブジェクトの働きによってクリアされます。このため、Invalidate()メソッドを使って再描画を行うと、文字列を描画する前に背景の塗りつぶしを行う必要がなくなります。ただし、画面のちらつきが起こるのは変わりません。


7.2.3  改良版ClockworkWave

リスト7.9~7.12に、ClockworkWaveアプリケーションの改良版のソースを示します。なお、アプリケーションクラスやウィンドウクラスについては変更を加える必要がありませんでしたので、ここでは省略しています。必要であればリスト7.1~7.4を参照して下さい。

[リスト7.9] ClockView.hの内容(改訂版)
#ifndef _CLOCK_VIEW_H_
#define _CLOCK_VIEW_H_

#include "TimedView.h"
#include <time.h>

/*
 * ClockViewクラスの定義
 */
class ClockView : public TimedView {
// メソッド
public:
    // 初期化と解放
    ClockView(BRect frame, uint32 resizeMask, TimeKeeper