S-JIS[2004-09-01/2005-12-17]

描画に関するWindowsXPのバグっぽく見えたもの

Windows95,98,2000では動いていたのに、WindowsXPで誤動作(非常にちらつく)する自作アプリがあったので調査してみましたが…。
当初は、根本的な原因と対処法がよく分かりませんでした。

とりあえずの解決策としては、exeファイルのプロパティを開き、互換モードを旧バージョンに合わせるというものです。
それでも完全には直らないですが、何もしないよりはマシでした。

原因が分かってみれば、全然別の問題でしたけどね…。まぁバグってのは現象と原因は違うもんですが。


現象1.CDC::GetOutputTextExtent

CDC::GetOutputTextExtent()の中で、APIのGetOutputTextExtent()を呼んで、その戻り値をVERIFYでチェックしている。
で、そのチェックがWindowsXPでは たまにエラーになる。
GetLastError()でエラーを表示してみると、ERROR_INVALID_HANDLE(6)。

ここでハンドルと言えばCDC::m_hDCしか考えられないが、このエラーが出てたときにCDC::TextOut()を呼んでみると ちゃんと文字が描かれる!

「APIのGetOutputTextExtent32()を使え」と書いてあるHPがあったのでそちらに変えてみたけど、やはり同じ。

無限ループ覚悟でGetOutputTextExtent32()を呼び続けてみると、たまにうまく動く時が出てくる…。

CSize CPhiView::GetOutputTextExtent(CDC *pDC,LPCTSTR str)
{
	ASSERT(pDC->m_hDC != NULL);
	SIZE size={0,0};
	int ret = ::GetTextExtentPoint32(pDC->m_hDC, str, strlen(str), &size);
	if(ret==0){
		TRACE("v:%d(%d,%d)\n",::GetLastError(),size.cx,size.cy);

//		::SetLastError(0);
//		pDC->TextOut(0,0,"test!");
//		TRACE("%p:%d:%s\n",pDC->m_hDC,::GetLastError(),str);

		while(ret==0){
			ret = ::GetTextExtentPoint32(pDC->m_hDC, str, strlen(str), &size);
			TRACE("loop:%d\n",::GetLastError());
		}
	}
	VERIFY(ret);

	return size;
}

なぜハンドルが無効と言われるのか、どういったときに有効になるのか、それをどうやって判断するのか、さっぱり分かりません。


現象2.CDC::CreateCompatibleDC

WindowsXPでは、CreateCompatibleDCに失敗する場合が頻発する模様。
おかげで、本来描かれるはずのキャラが表示されずに黒くなり、画面が点滅して乱れて見える。

しかも、GetLastError()の戻り値は0なので、何の参考にもならない。。。

	CDC tmpDC;
	BOOL ret = tmpDC.CreateCompatibleDC(pDC);
	if(!ret){
		TRACE("CreateCompatibleDC失敗! %d\n",::GetLastError());
		goto end;
	}

この現象はWindows95,98,2000では起きてなかった(少なくとも頻発はしてない)し、互換モードに変えれば頻度が減っているように見えるから、WindowsXP特有のプログラミング法があるのかも…。


[2005-12-17]

原因

このアプリでは、タイマー割込みの処理ルーチンで裏のDCに描画しておき、OnDraw()でそれを実際に表示している。
タイマー処理では、キャラを移動する度に 裏DCに描くと同時にInvalidateRect()を呼び出してクリッピングを更新していた。
Invalidate()が呼ばれることによってOSが画面を描画する必要があることを知り、OnDraw()が呼び出される。

この方式では、タイマー処理が終わるまでOnDraw()が呼ばれないことを期待している。(一応フラグを用意して、同時には動かないようにしていたつもりだった)
WindowsXPより前のOSでこの方式がうまくいっていた(ように見えた)のは、ただ単にマシン速度が遅いので、期待していた動作になっていただけだと思われる。

WindowsXPではマシン速度が上がり(あるいはマルチスレッド機能が向上したのかもしれないが)、タイマー処理とOnDraw()が並列で実行されることが頻繁に起こったようだ。
タイマー処理の中でキャラや文字を移動・変更する度にInvalidate()を何度も呼び出しているので、ほぼその都度OnDraw()が呼ばれたとすれば、キャラと文字のちらつきが目に付いた理由の説明がつく。

また、1つのDCを二箇所以上で同時に扱うことも出来ないため、CreateCompatibleDC()等でDCを使おうとしても片方が占有していて使えない状態だったのだろう。
タイマー処理でDCを占有していれば、OnDraw()では描画したい範囲でDCが使えず、結果として何も描画できず黒くなってしまうようだ。これがちらつきの原因と思われる。


対処法

タイマー割込みとOnDraw()が並列(マルチスレッド)で動くと分かった以上、対処方法は排他制御するしかない。
(というより、マルチスレッドなら排他をかけるのは当たり前。タイマー割込みがマルチスレッドだと思ってなかったのが敗因…)

この方針でロック処理を行った結果、ちらつかなくなったので、ひとまずこれで解決したと思われる。


VC++ページへ戻る / 技術メモへ戻る
メールの送信先:ひしだま