スクリーンセーバー

●スクリーンセーバーって何なの?

スクリーンセーバーとは、そもそも画面の焼き付きが起きないように、一定時間がくると画面を消去したり、画面上の特定箇所だけに表示されないようにするためのものですが、現在のディスプレイはほとんど焼きつきを起こしませんので、今や単なる楽しみ、あるいは新しいメディア、といった感じで利用されています。

プログラム的にみると、スクリーンセーバーは、ファイル名が .exe ではなく .scr になっているという違うがあるぐらいで、実は通常のWindowsプログラムと変わりません。 ただし、スクリーンセーバーは通常画面いっぱいに表示されます。 また、マウスやキーボードを操作すると終了します。 このために、スクリーンセーバーのプログラムでは、最初に画面いっぱいのウィンドウを用意しますし(ウィンドウの枠やボタンは非表示)、実行中にマウスやキーボードのイベントが発生したら、ただちにプログラムを終了するように作っておく必要があります。 さらに、スクリーンセーバーの場合には、「画面のプロパティ」を表示したときに、小さい画面にプレビューができたり、スクリーンセーバの設定が行えたりします。


スクリーンセーバーは「画面のプロパティ」に対応させる必要がある

●ScreenSaver用ミニスケルトン

本サイトで用意した「ScreenSaver用ミニスケルトン」は、「やってTRYミニFMCウィザード」と中味はほとんど同じですが、上で説明したように、実行したときに画面いっぱいのウィンドウを表示する処理と、マウスの左ボタンをクリックするとプログラムを終了する処理が追加されています。 さらに「画面のプロパティ」に表示される小さい画面でプレビューする機能も含まれています(なお、スクリーンセーバーの設定については省略されています)。

以上を理解しておけば、あとは「やってTRYミニFMCウィザード」でプログラミングようにしてスクリーンセーバーを作ることができます。 このあたりは「ScreenSaver用ミニスケルトン」の配布ファイルに同梱されているサンプルプログラムをご覧になってください。 これらは「やってTRYミニFMCウィザード」で作ったプログラムのソースを1行も変更せずに、そのまま「ScreenSaver用ミニスケルトン」に移植したものとなっています。

sv001 (サンプル1 ウィンドウの隅で円が反射)
sv002 (サンプル2 円と対角線を描画)
sv003 (サンプル3 ビットマップを描画)
sv004 (サンプル4 時計)

●ラインアート

いかにもスクリーンセーバーといったものを作ろうというわけで、Windowsに標準添付されているスクリーンセーバー「ラインアート」と同様のアニメーションを「ScreenSaver用ミニスケルトン」で作成してみましたので、興味ある人はダウンロードページから入手して、ソースコードを見てみてください。 ラインアートの基本的なアルゴリズムは次のようなものです。

  1. 任意の頂点を持つ多角形を描画する
  2. 各頂点はXY方向に移動する。このときXY移動量は各頂点で異なる
  3. 頂点はウィンドウの端にくると反射する
  4. 描画した多角形は任意の数の残像を残す

これは、第4問で解説した直線を引く知識、第7問で解説した乱数の知識、第19問で解説したアニメーションの知識、第21問で解説したウィンドウの背景を塗りつぶす知識、第24問で解説した配列変数の知識、第27問で解説したウィンドウ枠で反射させる知識などを持ち寄れば作成できます。


残像なしバージョン(lineart_s)

残像ありバージョン(lineart)

●ラインアート(残像なし版)ソースコード

本書では説明していない知識について解説します。

// CLineart_sWndクラス メッセージハンドラ

const int ALL_POINT = 4;  //頂点の数

int x[ALL_POINT];         //線のX座標
int y[ALL_POINT];         //線のY座標

int xmov[ALL_POINT];      //線のX移動量
int ymov[ALL_POINT];      //線のY移動量

int win_xsize;            //ウィンドウの幅
int win_ysize;            //ウィンドウの高さ

「const int ALL_POINT = 4;」(赤色部分)は4をALL_POINTという名称で扱えるようにするための宣言です。 constは値が変更されない変数を宣言するためのものです。 もちろん、これを使わずに以降の文を「int x[4];」「int y[4];」のように直接4の値を記述してもかまいませんが、4を10に変えたい場合、リストのあちこちを直さなくてはなりません。 const宣言をすることで「const int ALL_POINT = 10;」とするだけで済むというわけです。

それ以降は、このプログラム全体で使う変数宣言です。 たとえば、「int x[ALL_POINT];」は、線のX座標を記憶するための配列変数で、これによってx[0]〜x[3]が使えるようになります。

void CLineart_sWnd::OnPaint()
{
    CPaintDC dc(this);

    CPen myPen;  //CPen型の変数を作成
    myPen.CreatePen(PS_SOLID,1,RGB(255, 255, 255));  //ペン情報を設定

    // ウィンドウのサイズを得る
    CRect sikaku;
    GetClientRect(sikaku);

    CBitmap uragamen;  //仮想画面を用意(ちらつき防止用)
    uragamen.CreateCompatibleBitmap(&dc, sikaku.right, sikaku.bottom);

    // 裏画面全体を背景色で塗りつぶす
    CBitmapDC dc_ura(uragamen);
    dc_ura.FillSolidRect(sikaku, RGB(0, 0, 0));

    dc_ura.SelectObject(&myPen);          //ペンとしてmyPenを選択する
    dc_ura.MoveTo(x[0], y[0]);            //開始点の描画

    for (int i=1; i<ALL_POINT; i++) {
        dc_ura.LineTo(x[i], y[i]);        //間の線の描画
    }

    dc_ura.LineTo(x[0], y[0]);            //最後は開始点まで線を引く
    dc_ura.SelectStockObject(BLACK_PEN);  //myPenを解放する

    // 裏画面をウィンドウにコピーする
    dc.StretchBlt(0, 0, sikaku.right, sikaku.bottom,
        &dc_ura, 0, 0, sikaku.right, sikaku.bottom, SRCCOPY);
}

このスクリーンセーバーで動く線は白です。したがって、最初にペンを白に設定しています。 また、ウィンドウのサイズもここで得ています。このサイズの中で線を動かします。もちろんウィンドウが起動したとき、画面いっぱいのウィンドウが表示されるので、画面いっぱいのサイズが得られます。 また、このプログラムでは、ちらつき防止のために、まず線を裏画面に描画し、それをウィンドウに転送するという方法を用いています。ちらつき防止の方法は第26問の解説で紹介した方法をそのまま用いています。 裏画面は、まず画面全体を黒で塗りつぶします。それから多角形を描きます。

多角形を描くには、MoveTo()関数で最初の点を描きます(dc_ura.MoveTo(x[0], y[0]);)。 次に、LineTo()関数で頂点の数だけ線を描いていきます(その次のfor文)。 そして最後に最後の点と最初の点をLineTo()関数で結びます(dc_ura.LineTo(x[0], y[0]);)。 これで、閉じた多角形が描けます(赤色部分)。

void CLineart_sWnd::OnActivateApp(BOOL bActive, DWORD dwThreadID)
{
    if (!bActive) {   // 他のアプリケーションがアクティブになったことを示す
        ExitSaver();  // スクリーンセーバを終了する
    }
}
void CLineart_sWnd::OnLButtonDown(UINT nFlags, CPoint point)
{
    ExitSaver();  // マウスの左ボタンを押したのでスクリーンセーバを終了する
}
int CLineart_sWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    SetTimer(1, 10, NULL);  //タイマーを開始する(10ミリ秒間隔)

    srand(GetTickCount());  //乱数初期化

    return 0;
}
void CLineart_sWnd::OnSize(UINT nType, int cx, int cy)
{
    if (nType == SIZE_MINIMIZED) {  //ウィンドウが最小化された場合は、
        return;  //正しいサイズが得られないのですぐに終了
    }

    win_xsize = cx;  //ウィンドウの幅を記録しておく
    win_ysize = cy;  //ウィンドウの高さを記録しておく

    for (int i=0; i<ALL_POINT; i++) {
        x[i] = rand() % win_xsize;  //線のX座標初期化
        y[i] = rand() % win_ysize;  //線のY座標初期化
        xmov[i] = (rand() % 5 + 2); //線のX移動量初期化
        ymov[i] = (rand() % 5 + 2); //線のY移動量初期化
    }
}

赤色部分は、最初の多角形の各頂点の位置を、乱数で設定しています。 もちろん乱数は、画面サイズの範囲内の値で生成します。 また、XY移動量は2〜6の範囲で値を生成しています。 「rand() % 5」で0〜4の値が得られますから、それに2を足すことで2〜6の値になります。

void CLineart_sWnd::OnTimer(UINT nIDEvent)
{
    int left   = 0;          //左の限界
    int right  = win_xsize;  //右の限界
    int top    = 0;          //上の限界
    int bottom = win_ysize;  //下の限界

    for (int i=0; i<ALL_POINT; i++) {
        if (x[i] + xmov[i] < left) {        //左はみ出しチェック
            x[i] = left;
            xmov[i] = -xmov[i];             //X方向の移動量を逆転
        }
        else if (x[i] + xmov[i] > right) {  //右はみ出しチェック
            x[i] = right;
            xmov[i] = -xmov[i];             //X方向の移動量を逆転
        }

        if (y[i] + ymov[i] < top) {         //上はみだしチェック
            y[i] = top;
            ymov[i] = -ymov[i];             //Y方向の移動量を逆転
        }
        else if (y[i] + ymov[i] > bottom) { //下はみだしチェック
            y[i] = bottom;
            ymov[i] = -ymov[i];             //Y方向の移動量を逆転
        }

        x[i] = x[i] + xmov[i];  //X座標を更新
        y[i] = y[i] + ymov[i];  //Y座標を更新
    }
    Invalidate();
}

この部分は、第27問で説明したウィンドウの枠で反射させる方法をそのまま用いているだけです。 ただし、第27問は円だったため、円の幅と高さを考える必要がありましたが、こちらは点なので、右の限界と下の限界は、ウィンドウの幅と高さそのものになってます。 XY座標が配列なのでちょっと複雑に見えますが、for文によって頂点すべてについて反射のチェックを行っているだけのことです。

BOOL CLineart_sWnd::OnEraseBkgnd(CDC* pDC)
{
    return FALSE;  //再描画時の背景消去を停止
}

●ラインアート(残像あり版)ソースコード

本書では説明していない知識について解説します。

// CLineartWndクラス メッセージハンドラ

const int ALL_POINT = 4;   //頂点の数
const int ZANZOU = 5;      //残像の数

int x[ALL_POINT][ZANZOU];  //線のX座標
int y[ALL_POINT][ZANZOU];  //線のY座標
 
int xmov[ALL_POINT];       //線のX移動量
int ymov[ALL_POINT];       //線のY移動量

int win_xsize;             //ウィンドウの幅
int win_ysize;             //ウィンドウの高さ

残像なし版との違いは、1度描画した頂点の情報をしばらく記憶しているかどうかです。 そのために、残像なし版ではXY座標の記憶を、配列を使って記憶しましたが、残像あり版では「2次元配列」を使って記憶します。 2次元配列は、配列を2次元的に記憶できる変数です。 2次元配列の宣言は、「int y[5][4];」のように、配列要素を2つ続けて記述します。


1次元配列 int x[5];
x[0] x[1] x[2] x[3] x[4]

2次元配列 int x[5][4];
x[0][0] x[1][0] x[2][0] x[3][0] x[4][0]
x[0][1] x[1][1] x[2][1] x[3][1] x[4][1]
x[0][2] x[1][2] x[2][2] x[3][2] x[4][2]
x[0][3] x[1][3] x[2][3] x[3][3] x[4][3]

void CLineartWnd::OnTimer(UINT nIDEvent)
{
    int left   = 0;            //左の限界
    int right  = win_xsize;    //右の限界
    int top    = 0;            //上の限界
    int bottom = win_ysize;    //下の限界

    for (int i=0; i<ALL_POINT; i++) {
        if (x[i][0] + xmov[i] < left) {        //左はみ出しチェック
            x[i][0] = left;
            xmov[i] = -xmov[i];                //X方向の移動量を逆転
        }
        else if (x[i][0] + xmov[i] > right) {  //右はみ出しチェック
            x[i][0] = right;
            xmov[i] = -xmov[i];                //X方向の移動量を逆転
        }

        if (y[i][0] + ymov[i] < top) {         //上はみだしチェック
            y[i][0] = top;
            ymov[i] = -ymov[i];                //Y方向の移動量を逆転
        }
        else if (y[i][0] + ymov[i] > bottom) { //下はみだしチェック
            y[i][0] = bottom;
            ymov[i] = -ymov[i];                //Y方向の移動量を逆転
        }

        for (int j=ZANZOU-2; j>=0; j--){
            x[i][j+1] = x[i][j];
            y[i][j+1] = y[i][j];
        }

        x[i][0] = x[i][0] + xmov[i];    //X座標を更新
        y[i][0] = y[i][0] + ymov[i];    //Y座標を更新
    }
    Invalidate();    //ウィンドウを再描画
}

最後の行で、最新のXY座標をx[0][0]とy[0][0]に入れますから、それより先に、x[頂点n][4]の内容をx[頂点n][5]へ、x[頂点n][3]の内容をx[頂点n][4]へ、x[頂点n][2]の内容をx[頂点n][3]へ、x[頂点n][1]の内容をx[頂点n][2]へ、x[頂点n][0]の内容をx[頂点n][1]へと内容を移し変えていきます(赤色部分)。これで残像となる座標を記憶しておきます。

以後、スクリーンセーバ「センスいい版」へと続く……