WINAPI ちらつき防止のダブルバッファが上手くできない

フォーラム(掲示板)ルール
フォーラム(掲示板)ルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
ふりかけ

WINAPI ちらつき防止のダブルバッファが上手くできない

#1

投稿記事 by ふりかけ » 14年前

WINAPI32を勉強しています。DirectXは分かりません。C++で書いています。
黒い玉がウィンドウの壁で跳ね返りながら動くプログラムです。

書籍のコードを改造してダブルバッファで描画させることで、ちらつきを無くしたいです。

しかし、相変わらずちらつきが起きます。ティアリングではないと思います。(瞬きするようにちらつく)
原因が分かりませんでした。
ディスプレイのリフレッシュに合うような書き方をしなくてはいけないのでしょうか?
そもそもダブルバッファになっていないかもしれません。お願いします。

コード:

#include <windows.h>

#define APP_NAME TEXT("Sample_MainWindow")
#define ELLIPSE_SIZE 50	//玉の直径

LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
	
	HDC hdc;
	PAINTSTRUCT ps;
	static RECT rect;	//ウィンドウサイズを格納するRECT
	static POINT pt;	//玉の位置
	static BOOL xIncrease, yIncrease;	//玉の跳ね返り用フラグ

	static HDC hSecondDC;	//裏画面用HDC
	static HBITMAP hSecondBmp;	//裏画面用HBITMAP

		switch(uMsg){
			case WM_DESTROY:
				DeleteDC(hSecondDC);
				DeleteObject(hSecondBmp) ;
				PostQuitMessage(0);
				return 0;

			case WM_CREATE:
				SetTimer(hWnd, 1, 20, NULL);	//タイマーセット 20ms毎にWM_TIMERが発生する
				GetClientRect(hWnd, &rect);
				hdc = GetDC(hWnd);
				hSecondDC = CreateCompatibleDC(hdc);	//メインウィンドウと互換性のあるデバイスコンテキストをメモリ上に生成
				hSecondBmp = CreateCompatibleBitmap(hdc, rect.right, rect.bottom);	//裏画面用のビットマップを生成
				ReleaseDC(hWnd, hdc);

				SelectObject(hSecondDC, hSecondBmp);

				return 0;

			case WM_TIMER:
				if(xIncrease) pt.x++;										//玉の動きを計算
				else pt.x--;												//ウィンドウの端にぶつかると
																			//跳ね返る
				if(yIncrease) pt.y++;										//
				else pt.y--;												//
																			//
				if(pt.x + ELLIPSE_SIZE > rect.right) xIncrease = FALSE;		//
				else if(pt.x < 0) xIncrease = TRUE;							//
				if(pt.y + ELLIPSE_SIZE > rect.bottom) yIncrease = FALSE;	//
				else if(pt.y < 0) yIncrease = TRUE;							//

				SelectObject(hSecondDC, GetStockObject(WHITE_BRUSH));	//裏画面を白で塗りつぶす
				Rectangle(hSecondDC, 0, 0, rect.right, rect.bottom);	//
				SelectObject(hSecondDC, GetStockObject(BLACK_BRUSH));						//玉を黒で描画する
				Ellipse(hSecondDC, pt.x, pt.y, pt.x + ELLIPSE_SIZE, pt.y + ELLIPSE_SIZE);	//

				InvalidateRect(hWnd, NULL, TRUE);	//メインウィンドウを無効状態にする、WM_PAINTが発生する
				return 0;

			case WM_PAINT:
				hdc = BeginPaint(hWnd, &ps);
				BitBlt(hdc, 0, 0, rect.right, rect.bottom, hSecondDC, 0, 0, SRCCOPY);	//裏画面を表画面にコピーする
				EndPaint(hWnd, &ps);
				return 0;
	}
	return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd){

	WNDCLASS wc;
	MSG msg;

	wc.style			= CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc		= WindowProc;
	wc.cbClsExtra		= 0;
	wc.cbWndExtra		= 0;
	wc.hInstance		= hInstance;
	wc.hIcon			= LoadIcon(NULL, IDI_APPLICATION);
	wc.hCursor			= LoadCursor(NULL, IDC_ARROW);
	wc.hbrBackground	= (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName		= NULL;
	wc.lpszClassName	= APP_NAME;

	if(!RegisterClass(&wc)) return 0;

	if(CreateWindow(
		APP_NAME, TEXT(__FILE__), WS_OVERLAPPEDWINDOW | WS_VISIBLE, 
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 
		NULL, NULL, hInstance, NULL) == NULL) return 0;

	while(GetMessage(&msg, NULL, 0, 0) > 0){
		DispatchMessage(&msg);
	}
	return msg.wParam;
}

アバター
tk-xleader
記事: 158
登録日時: 14年前
連絡を取る:

Re: WINAPI ちらつき防止のダブルバッファが上手くできない

#2

投稿記事 by tk-xleader » 14年前

WM_TIMERのたびにInvaridateRect関数を呼び出しているということは、毎回ウィンドウに直結したデバイスコンテキストに無効領域(=背景色で塗りつぶし)を発生させているわけですから、アニメーションがちらついてしまいます。
そこで、WM_TIMERでそもそも表バッファにコンパチDCをコピーしてしまいます。WM_PAINT以外では、デバイスコンテキストはGetDCで取得してReleaseDCで解放してやります。

以下は、補正したプログラムです。

コード:

#include <windows.h>
 
#define APP_NAME TEXT("Sample_MainWindow")
#define ELLIPSE_SIZE 50 //玉の直径
 
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
    
    HDC hdc;
    PAINTSTRUCT ps;
    static RECT rect;   //ウィンドウサイズを格納するRECT
    static POINT pt;    //玉の位置
    static BOOL xIncrease, yIncrease;   //玉の跳ね返り用フラグ
 
    static HDC hSecondDC;   //裏画面用HDC
    static HBITMAP hSecondBmp;  //裏画面用HBITMAP
 
        switch(uMsg){
            case WM_DESTROY:
                DeleteDC(hSecondDC);
                DeleteObject(hSecondBmp) ;
                PostQuitMessage(0);
                return 0;
 
            case WM_CREATE:
                SetTimer(hWnd, 1, 20, NULL);    //タイマーセット 20ms毎にWM_TIMERが発生する
                GetClientRect(hWnd, &rect);
                hdc = GetDC(hWnd);
                hSecondDC = CreateCompatibleDC(hdc);    //メインウィンドウと互換性のあるデバイスコンテキストをメモリ上に生成
                hSecondBmp = CreateCompatibleBitmap(hdc, rect.right, rect.bottom);  //裏画面用のビットマップを生成
                ReleaseDC(hWnd, hdc);
 
                SelectObject(hSecondDC, hSecondBmp);
 
                return 0;
 
			case WM_TIMER:
				hdc = GetDC(hWnd);
				if(xIncrease) pt.x++;                                       //玉の動きを計算
                else pt.x--;                                                //ウィンドウの端にぶつかると
                                                                            //跳ね返る
                if(yIncrease) pt.y++;                                       //
                else pt.y--;                                                //
                                                                            //
                if(pt.x + ELLIPSE_SIZE > rect.right) xIncrease = FALSE;     //
                else if(pt.x < 0) xIncrease = TRUE;                         //
                if(pt.y + ELLIPSE_SIZE > rect.bottom) yIncrease = FALSE;    //
                else if(pt.y < 0) yIncrease = TRUE;                         //
 
                SelectObject(hSecondDC, GetStockObject(WHITE_BRUSH));   //裏画面を白で塗りつぶす
                Rectangle(hSecondDC, 0, 0, rect.right, rect.bottom);    //
                SelectObject(hSecondDC, GetStockObject(BLACK_BRUSH));                       //玉を黒で描画する
                Ellipse(hSecondDC, pt.x, pt.y, pt.x + ELLIPSE_SIZE, pt.y + ELLIPSE_SIZE);   //

				BitBlt(hdc, 0, 0, rect.right, rect.bottom, hSecondDC, 0, 0, SRCCOPY);   //裏画面を表画面にコピーする

				ReleaseDC(hWnd,hdc);
                
                return 0;
 
            case WM_PAINT:
				ValidateRect(hWnd,NULL);
                return 0;
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
 
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd){
 
    WNDCLASS wc;
    MSG msg;
 
    wc.style            = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc      = WindowProc;
    wc.cbClsExtra       = 0;
    wc.cbWndExtra       = 0;
    wc.hInstance        = hInstance;
    wc.hIcon            = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor          = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground    = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName     = NULL;
    wc.lpszClassName    = APP_NAME;
 
    if(!RegisterClass(&wc)) return 0;
 
    if(CreateWindow(
        APP_NAME, TEXT(__FILE__), WS_OVERLAPPEDWINDOW | WS_VISIBLE, 
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 
        NULL, NULL, hInstance, NULL) == NULL) return 0;
 
    while(GetMessage(&msg, NULL, 0, 0) > 0){
        DispatchMessage(&msg);
    }
    return msg.wParam;
}

ISLe
記事: 2650
登録日時: 15年前
連絡を取る:

Re: WINAPI ちらつき防止のダブルバッファが上手くできない

#3

投稿記事 by ISLe » 14年前

質問のコードでちらつくのは、裏画面が転送される直前に、クライアントの背景が消去されているのが見えるからです。
裏画面が覆い尽くすので無駄ですから、ウィンドウクラスの背景ブラシをNULLに設定するか、WM_ERASEBKGNDメッセージに対して1を返すようにして背景消去が行われないようにしてください。

わたしはWM_PAINTで描画するのが好みですね。
何かの拍子に背景色で塗り潰されたウィンドウが表示されるのは嫌ですし、UpdateWindowでWM_PAINTを速攻呼び出すこともできます。

ふりかけ

Re: WINAPI ちらつき防止のダブルバッファが上手くできない

#4

投稿記事 by ふりかけ » 14年前

tkmakwins15さん ISLeさん ありがとうございました。
裏から表に転送しているだけなのにどうして一瞬白くなってしまうのかと悩みましたが、無効領域が背景色で
再描画されていることを分かっていませんでした。

InvalidateRect(hWnd, NULL, TRUE);

InvalidateRect(hWnd, NULL, FALSE);
とすることで改善されました。ですが
ISLe さんが書きました: ウィンドウクラスの背景ブラシをNULLに設定するか、WM_ERASEBKGNDメッセージに対して1を返すようにして背景消去が行われないようにしてください。
このようにした方が良いのでしょうか?
それとUpdateWindow関数を知らなかったのでリファレンスを見たところ キューを通さずにWM_PAINTを送ることができる
ということが分かりました。これは、他の処理よりも描画を優先させて、フレームレートを維持する時に使うと良いのでしょうか?
使いどころを知りたいです。

beatle
記事: 1281
登録日時: 14年前
住所: 埼玉
連絡を取る:

Re: WINAPI ちらつき防止のダブルバッファが上手くできない

#5

投稿記事 by beatle » 14年前

描画って時間が余ったときだけにすれば良いと思います。
それよりロジックの処理を優先させたほうがいいのでは?

ISLe
記事: 2650
登録日時: 15年前
連絡を取る:

Re: WINAPI ちらつき防止のダブルバッファが上手くできない

#6

投稿記事 by ISLe » 14年前

InvalidateRect(hWnd, NULL, FALSE);
は背景を消去しないように指定しますけど、WM_PAINTが発生するのはプログラムが要求したときだけではありませんよ。

MSDNのリファレンスによるとWM_PAINTは他のどのウィンドウメッセージよりも優先順位が低いです。
なので、更新した裏画面が表示されないこともあり得ます。
安定したフレームレートを得たいならUpdateWindowで即座に再描画するのが良いと思います。
ちなみにWM_TIMERは安定しないのでこのコードで神経質になる必要はありません。

ふりかけ

Re: WINAPI ちらつき防止のダブルバッファが上手くできない

#7

投稿記事 by ふりかけ » 14年前

確かにInvalidateRect関数以外からWM_PAINTが送られると背景色で塗りつぶされてしまいますね。
そういう状況は思い浮かびませんが、気をつけておきます。

今後スレッドを学んでそこに描画を担当させようと考えているのでその時にUpdateWindowを使ってみます。

beatleさんの意見はわたしも考えていたのですが、UpdateWindowでFPSを維持して、処理が重いときは
FPSを低くするように工夫できそうです。描画が間に合わないようなプログラムになるのは当分先ですが。

ありがとうございました。

ISLe
記事: 2650
登録日時: 15年前
連絡を取る:

Re: WINAPI ちらつき防止のダブルバッファが上手くできない

#8

投稿記事 by ISLe » 14年前

ふりかけ さんが書きました:そういう状況は思い浮かびませんが、気をつけておきます。
ウィンドウを最初に開いたときとか、最小化から元に戻したとき、ウィンドウサイズを変更したとき、他のウィンドウに隠れていた部分が現れたときなど、デスクトップを操作したときにウィンドウズから再描画要求が飛んできます。
他のウィンドウをドラッグして横切らせると触っていなくても下に置いたウィンドウがチラつきますよ。
Aeroテーマだとウィンドウマネージャがバックバッファを確保するので再描画要求の頻度がぐっと減りますけど。

ふりかけ

Re: WINAPI ちらつき防止のダブルバッファが上手くできない

#9

投稿記事 by ふりかけ » 14年前

ゲーム画面は頻繁に更新されますし、無効リージョンの処理という本来のWM_PAINTは考えなくても良いかなと。
その上でWM_PAINTが他から送られることは思いつかないという意味でした。自分しか触らないですし。

トピックを解決にしましたが、マルチスレッドを試したところまたちらつきが発生しました。

640×480 ピクセルのエリアが黒から白へと少しずつ変化していくプログラムです。

前に質問した問題点は大丈夫だと思うのですが、どうやらAlphaBlend関数がうまくいってないようです。

問題があると思う部分(全コードは最後につけます)

コード:

		SelectObject(hdc, hBitmap2);			//裏画面に白ビットマップを
		if(AlphaBlend(							//アルファブレンドで重ねる
			gameWnd->hScreenDC,					//
			0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,	//
			hdc,								//
			0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,	//
			blendFunc) == FALSE){				//アルファブレンドが失敗したら
				TextOut(gameWnd->hScreenDC, 10, 10, TEXT("AlphaBlend: FALSE"), 16);
				if(MessageBox(gameWnd->hWnd, TEXT("続行しますか?"), NULL, MB_YESNO) == IDNO){
					SendMessage(gameWnd->hWnd, WM_CLOSE, 0, 0);
				}
		}
AlphaBlendが失敗したとき、TextOut で文字を表示 MessageBox で処理の一時停止&プログラムの終了をさせています。
プログラムを走らせると、しばしばAlphaBlendが失敗しているようです。さらに、失敗していない時でもちらつきは起きます。

疑問点のまとめ
1:なぜ画面がちらつくのか
2:なぜAlphaBlendが失敗してしまうのか
3:MessageBox が起動したときに TextOut が働いてない時があるのはなぜか
4:文字の色を赤色に設定したが、黒くなるはずの部分も赤くなるのはなぜか

多いですが特に1、2で困っています。

コード:

#include <windows.h>

#define APP_NAME TEXT("Sample_MainWindow")
#define WINDOW_WIDTH	640	
#define WINDOW_HEIGHT	480
#define FPS	20

typedef struct _GameWindow{	//描画スレッドにメインウィンドウの情報を渡す為の構造体
	HWND hWnd;	//メインウィンドウのハンドル
	HDC hScreenDC;	//裏画面用のデバイスコンテキスト
} GameWindow;

DWORD WINAPI ThreadFunc(LPVOID vdParam){	//描画スレッドの定義
	GameWindow * gameWnd;
	DWORD nowTime, beforeTime;	//一定のFPSになるようにSleep時間を計算するための変数
	int imgAlpha = 255;	//半透明の割合を入れる変数
	HDC hdc;
	HBITMAP hBitmap1, hBitmap2;	//黒、白用のビットマップ
	BLENDFUNCTION blendFunc = {AC_SRC_OVER, 0, 0, 0};
	BOOL blTurn = TRUE;	//半透明の割合が0~255を往復するためのフラグ

	hdc = CreateCompatibleDC(NULL);
	hBitmap1 = CreateCompatibleBitmap(hdc, WINDOW_WIDTH, WINDOW_HEIGHT);
	hBitmap2 = CreateCompatibleBitmap(hdc, WINDOW_WIDTH, WINDOW_HEIGHT);

	SelectObject(hdc, hBitmap1);						//一つ目のビットマップを黒く塗りつぶす
	SelectObject(hdc, GetStockObject(BLACK_BRUSH));		//
	Rectangle(hdc, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);	//

	SelectObject(hdc, hBitmap2);						//二つ目のビットマップを白く塗りつぶす
	SelectObject(hdc, GetStockObject(WHITE_BRUSH));		//
	Rectangle(hdc, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);	//

	gameWnd = (GameWindow *)vdParam;
	beforeTime = timeGetTime();

	while(IsWindow(gameWnd->hWnd)){


		SelectObject(hdc, hBitmap1);			//裏画面に黒ビットマップを転送
		BitBlt(									//					
			gameWnd->hScreenDC,					//
			0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,	//
			hdc, 0, 0, SRCCOPY);				//

		SelectObject(hdc, hBitmap2);			//裏画面に白ビットマップを
		if(AlphaBlend(							//アルファブレンドで重ねる
			gameWnd->hScreenDC,					//
			0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,	//
			hdc,								//
			0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,	//
			blendFunc) == FALSE){				//アルファブレンドが失敗したら
				TextOut(gameWnd->hScreenDC, 10, 10, TEXT("AlphaBlend: FALSE"), 16);
				if(MessageBox(gameWnd->hWnd, TEXT("続行しますか?"), NULL, MB_YESNO) == IDNO){
					SendMessage(gameWnd->hWnd, WM_CLOSE, 0, 0);
				}
		}

		if(blTurn){									//半透明の割合を変化させる計算
			imgAlpha--;								//
		}else{										//
			imgAlpha++;								//
		}											//
		if(imgAlpha >= 255) blTurn = TRUE;			//
		else if(imgAlpha <= 0) blTurn = FALSE;		//
													//
		blendFunc.SourceConstantAlpha = imgAlpha;	//

		nowTime = timeGetTime();																//描画タイミングを待って
		if((nowTime - beforeTime) < (1000 / FPS)) Sleep((1000 / FPS) - (nowTime - beforeTime));	//WM_PAINTを送信
		beforeTime = nowTime;																	//
		InvalidateRect(gameWnd->hWnd, NULL, FALSE);												//
		UpdateWindow(gameWnd->hWnd);															//
	}
	DeleteDC(hdc);
	DeleteObject(hBitmap1);
	DeleteObject(hBitmap2);

	return TRUE;
}

LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){

	HDC hdc;
	PAINTSTRUCT ps;
	DWORD dwID;
	static HBITMAP hBitmap;
	static GameWindow gameWnd;

	switch(uMsg){
		case WM_DESTROY:
			DeleteDC(gameWnd.hScreenDC);
			DeleteObject(hBitmap);
			PostQuitMessage(0);
			return 0;

		case WM_CREATE:
			gameWnd.hWnd = hWnd;

			hdc = GetDC(hWnd);
			hBitmap = CreateCompatibleBitmap(hdc, WINDOW_WIDTH, WINDOW_HEIGHT);	//裏画面用ビットマップ
			gameWnd.hScreenDC = CreateCompatibleDC(hdc);	//裏画面用デバイスコンテキスト
			SelectObject(gameWnd.hScreenDC, hBitmap);

			SetTextColor(gameWnd.hScreenDC, RGB(0xFF, 0, 0));	//文字の色を赤に設定

			ReleaseDC(hWnd, hdc);

			CreateThread(NULL, 0, ThreadFunc, (LPVOID)&gameWnd, 0, &dwID);	//描画スレッドを作成
			return 0;

		case WM_PAINT:
			hdc = BeginPaint(hWnd, &ps);
			BitBlt(hdc, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, gameWnd.hScreenDC, 0, 0, SRCCOPY);	//裏画面を表画面に転送
			EndPaint(hWnd, &ps);
			return 0;
	}
	return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd){

	WNDCLASS wc;
	MSG msg;

	wc.style			= CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc		= WindowProc;
	wc.cbClsExtra		= 0;
	wc.cbWndExtra		= 0;
	wc.hInstance		= hInstance;
	wc.hIcon			= LoadIcon(NULL, IDI_APPLICATION);
	wc.hCursor			= LoadCursor(NULL, IDC_ARROW);
	wc.hbrBackground	= NULL; //(HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName		= NULL;
	wc.lpszClassName	= APP_NAME;

	if(!RegisterClass(&wc)) return 0;

	if(CreateWindow(APP_NAME, TEXT(__FILE__), WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) == NULL) return 0;

	while(GetMessage(&msg, NULL, 0, 0) > 0){
		DispatchMessage(&msg);
	}
	return msg.wParam;
}

YuO
記事: 947
登録日時: 15年前
住所: 東京都世田谷区

Re: WINAPI ちらつき防止のダブルバッファが上手くできない

#10

投稿記事 by YuO » 14年前

(1) について,直感ですが……。
UIスレッドとワーカースレッドで,裏画面のデバイスコンテキストを共有しているようですが,UpdateWindowの実行とWM_PAINTをハンドルするタイミングに時間差があるため,
ワーカースレッドで裏画面の更新を行っている最中にWM_PAINTで裏画面の内容を表画面に転送する,というような状況が起きているのだと思います。

ワーカースレッド側でも更新用のデバイスコンテキストを用意して,更新終了時にデバイスコンテキストを入れ替える,というような処理をする必要があるかと思います。

ふりかけ

Re: WINAPI ちらつき防止のダブルバッファが上手くできない

#11

投稿記事 by ふりかけ » 14年前

なるほど、WM_PAINTの処理中にも描画スレッドは動いてるので同じハンドルはまずいですね。
マルチスレッドに慣れてなくてかなり危ないことしてたみたいです。
初めてつかったAlphaBlendがおかしいと思いこんで全く気付いていませんでした。
とりあえず描画スレッドを僅かずつSleepさせてみると、エラーが出なくなりました。
WM_PAINTの処理が終わるまで描画スレッドを待たせるように変えてみます。ありがとうございました。

閉鎖

“C言語何でも質問掲示板” へ戻る