Programming Tips
主にWinに偏ったTipsみたいなのを綴っていく意気込み。
6.WindowsキーをサポートしたHOTKEYコントロール:WindowsNT4SP3以降/2000/XP
5.Quaternion(クォータニオン)を用いたMatrix(マトリクス)の逆行列の求め方:ゲーム向け
4.Direct3DとUpdateLayeredWindowの併用:Windows2000/XP
3.UpdateLayeredWindowの使い方:Windows2000/XP
今回は趣向を変えて日常業務におけるtipsです。
基本的には「自分がやられて嫌な事をしない」という当たり前の事なんですが、極限状態に追い込まれるとそうもいかないのが人間。
・バグを憎んで人を憎まず
「これはお前のバグだ!」
なんて責め立てるような事は言ってはいけません。
バグを見つけた時の正しいソフト屋の姿勢は、まず自分を疑う。
ざっとでも良いので該当しそうな箇所を確認すること。
大騒ぎした挙句自分のバグだった、という経験を死ぬほどしてる経験者の談です。ああ、いっそ死にたい・・・。問題の箇所を見つけて担当者に報告するのがベストですが、急を要する場合はとりあえずチーフや上に報告。
「○○の箇所で××の現象が起こります。もう少し調べてみますが、とりあえず報告いたします」
みたいな感じでしょうか。ほぼ100%他の人のバグだからといって放置しないこと。
余力があるならバグの再現方法の絞込みやバグ箇所等の追跡をすること。
自分が困っている時に助けて貰えなくなりますよ。
集団開発における孤立は死に等しいです。そして、バグを出した人を責めない。責めても意味が無いからです。
バグは大なり小なり必ず出ます。いちいち責任追及みたいな事してる時間があるなら、問題解決に当てるべきです。
頭ごなしに責めてもやる気が失われるだけで、バグは減りません。
「責任」なんてもんは偉い人に取らせればいいのです。そもそもそのために居るんですから、偉い人は。明らかにコーディング方法や業務の進め方に問題がありバグが出ている場合は「注意」ではなく「アドバイス」に留めるべきです。
・・・そうは言っても聞いてくれない人が居るのも事実で、難しい問題です。
・考えるんじゃない、働くんだ
ソフト屋の日常業務で一番無駄な時間は会議してる時間です。(断言)
コード吐くにしろ、仕様書書くにしろ、モニタ睨んでキーボード叩いてナンボな業種です。「話聞いてればキーボード叩いてても、資料読んでてもいいよ」
なんて現場は恐らく無いでしょう。
短時間で要点だけまとめられた会議ならまだしも、聞いてようがいまいが構わないような内容の薄い会議の時間はホント無駄です。次に無駄なのは「悩んでる時間」です。
悩んで解決する問題なら悩むべきですが、そうでない物に関しては一考の余地ありです。・必ずしも最善手である必要の無いもの
・考えても結論の出ないもの
・実験した方が早いものこれらは考えてる時間で手を動かせば結論が得られるでしょう。
ここでちょっと知人の体験談をご紹介しましょう。
チーフ 「あのさ、この部分を××に変更したいんだけど・・・」
知人 「はぁ」
チーフ 「変えてもいいかな?」
知人 「いや、チーフが決めたんならいいですけど」
チーフ 「変えたら嫌がるかな、と思って一応聞いてみたんだ」
知人 「じゃあ変えないでください」
チーフ 「でも変えたいんだけど・・・」無駄です。
チーフの存在そのものが。
この話を聞いたとき、ただの嫌がらせじゃないのかと思いました。
地球にはであった事の無い未知の生物がまだまだ沢山いるようで戦々恐々としてしまいます。願わくば一生会いませんように・・・とは言っても「他人のソースの解析&バグ取り」のような考えるだけで事足りるような場合もあるでしょうし、逆に「考えない設計屋」なんてのも問題なので、手を抜くべき箇所を考え・・・るのは時間の無駄ですかね?
デバイス/ライン/コントロールの列挙は適当に行ったあと、
・MIXERCONTROL_CONTROLTYPE_EQUALIZER
・MIXERCONTROL_CONTROLTYPE_MIXER
・MIXERCONTROL_CONTROLTYPE_MULTIPLESELECT
・MIXERCONTROL_CONTROLTYPE_MUX
・MIXERCONTROL_CONTROLTYPE_SINGLESELECT
これらの dwControlType のコントロールに対して、以下の関数をコールします。
hmo はミキサーハンドル、mc は上記属性を持つコントロールの構造体、selLineIDは選択したいラインです。
void SetSelMux( HMIXEROBJ hmo, MIXERCONTROL *mc, DWORD selLineID )
{
MIXERCONTROLDETAILS mcd;
mcd.cbStruct = sizeof( mcd );
mcd.dwControlID = mc->dwControlID;
mcd.cChannels = 1;
mcd.cMultipleItems = mc->cMultipleItems;
MIXERCONTROLDETAILS_LISTTEXT *mcdl = new MIXERCONTROLDETAILS_LISTTEXT[ mcd.cMultipleItems ];
MIXERCONTROLDETAILS_BOOLEAN *mcdb = new MIXERCONTROLDETAILS_BOOLEAN[ mcd.cMultipleItems ];
mcd.cbDetails = sizeof( MIXERCONTROLDETAILS_LISTTEXT );
mcd.paDetails = mcdl;
mixerGetControlDetails( hmo, &mcd, MIXER_GETCONTROLDETAILSF_LISTTEXT );
int i;
for( i = 0; i < (int)mcd.cMultipleItems; i++ )
mcdb[i].fValue = mcdl[i].dwParam1 == selLineID;
mcd.cbDetails = sizeof( MIXERCONTROLDETAILS_BOOLEAN );
mcd.paDetails = mcdb;
mixerSetControlDetails( hmo, &mcd, MIXER_GETCONTROLDETAILSF_VALUE );
delete []mcdb;
delete []mcdl;
}
どうという事の無いソースです。
ところが、一部環境で上記コードのままだと録音デバイスの選択が上手くいかない事があります。
現象的には、「選択したラインのミュートコントロールがONになり、入力自体がミュートされる」というものです。
#設定したコントロール、では無い点に注意
そこで、selLineID のライン内に、MIXERCONTROL_CONTROLTYPE_MUTE のタイプを持つコントロールが存在する場合は、上記関数実行後に、ミュートスイッチをOFFにするコードを入れておきます。
恐らくノイズ対策だと思いますが、現象の発生した環境は
XP Embedded + クリエイティブのUSBオーディオ
の組み合わせでした。
ラインにミュートスイッチが存在する場合のみ発生するのか、Embedded特有なのかまでは追ってませんが、こういう現象が存在するというtipsっつーこって。
【6.WindowsキーをサポートしたHOTKEYコントロール】
CHotKeyCtrl は配置するだけで RegisterHotKey() 用のキー設定を行える、便利なコントロールですが、いくつか致命的な問題点が存在します。
1.拡張キー情報(wModifiers) のパラメータが
RegisterHotKey() のパラメータと互換が無い
2.既に他のアプリ等で登録済みのホットキーを押してしまうと、そのメッセージが飛んでしまう
3.Windowsキーが組み合わせに存在しない
今回、これらの問題点を解決したコントロールを作成してみた。
「いいからサンプルとコードをよこせ!」という方はこちら。
(1)の拡張キー情報に互換性が無いのは、継承して変換する部分をクラス内に持てば済む話なので細かい説明はしない。
ソースコードを見れば一目瞭然であろう。
(2)と(3)は結果的に同じ処理で片が付くのでまとめて説明する。
元々 CHotKeyCtrl クラスは Windows のコモンコントロールを使用しているので、そのままの内部実装ではどう足掻こうが Windows キーの入力を取得できない。
そこで、SetWindowsHookEx( WH_KEYBOARD_LL, ... )
を使用して、ローレベルな段階でキー入力を取得し、OSに渡さないようにする。
こうすることで、Windows
キーを押してもタスクバーにフォーカスが移らなくなるし、他のアプリへのホットキーメッセージを送出を防ぐことが可能である。
#ただし、WH_KEYBOARD_LL
が使用できる環境は WindowsNT4SP3以降、らしい。
フックを仕掛けるタイミングはコントロールにフォーカスが移った時、フックを切り離すのも同様にフォーカスを失った時が良いだろう。
WM_SETFOCUS と ON_WM_KILLFOCUS
の時にフックの組み込みと削除を行う。
フック中は、基本的にTABキー等もWindowsに渡さないので、キーボードからフォーカスを動かされる心配はしなくて良い。
フック内では、WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP の4つのメッセージを監視し、これらのメッセージが来た時に、
#define WM_KEYDOWN2 ( WM_USER + WM_KEYDOWN )
#define WM_KEYUP2 ( WM_USER + WM_KEYUP )
のような、擬似的なメッセージでコントロール自身に
PostMessage() してやる。
コントロールは受け取ったキーボード情報を元に、現在どのキーが押されているかの情報を保存しておく。
なぜなら、OSにメッセージを渡さないので、GetKeyState()
等の関数は使用できない。
Windows
キーも擬似メッセージとして送信されるので、他のShift,Control,
Alt 等と同様に処理出来る。
キー状態の表示も自前で行う必要がある。
というのも、Windows
キーをサポートしないコモンコントロールには値を一切渡さないからである。
渡したところで Windows キーの表示はされない。
普通、コントロールをオーナー描画に設定し、WM_DRAWITEM
で処理するが、HOTKEYコントロールはオーナー描画のフラグが無い。
仕方無いので、WM_PAINT
メッセージで全て自前で描画する。
とは言うものの、周りの枠はOSが描画してくれるので、内側の文字とカーソルのカレットの位置だけ設定すればよい。
内側の「Ctrl + Alt + A」のような文字列は、.NETでは
GetHotKeyName()というメンバが追加されており、そのまま取得出来る。
今回VC++6.0で作るにあたり、.NETのコードを丸々パクった。サンプルでこの部分だけコーディングスタイルが違うのはこのせいである。
コントロールの文字を描画するときに使用するフォントを
SystemParametersInfo( SPI_GETNONCLIENTMETRICS, ... ) で取得し、テキトーな位置にテキトーに描画して、カレットをその後ろに移動する。
まぁ、この辺は本物のコモンコントロールと位置を合わせるだけの話なので、そうたいした問題じゃない。
一番面倒なのは、キーを押したときの動作をコモンコントロールと同じにすることで、ここだけはフラグなんかを使ってなるべく同じになるように処理したが、一箇所動作が同じにならなかった。たぶん気付かれないので問題無いが。
サンプルコードはこちら。
VC++6.0で使用する場合、
stdafx.h内の afxwin.h を include しているよりも上に
#define _WIN32_WINNT 0x0500
を追加する必要がある。上記が無いと KBDLLHOOKSTRUCT 構造体が定義されない。
また、Alt + TAB 等はフックによりOSに処理を渡さなく出来るが、Ctrl + Alt + DEL はフック出来ない。OSの仕様である。
【5.Quaternion(クォータニオン)を用いたMatrix(マトリクス)の逆行列の求め方:ゲーム向け】
他人のソースからの引用だらけなので詳しい解説は出来ません。(弱ッ!
しかも数学的な使用を前提にしていないので、制限が多いです。
ようは、Gauss-Jordan法で特定Matrixの逆行列が求められない、LU分解を用いると速度面で不安、っつー場合に効果が得られると思います。
ただし、三次元ベクトル(というかDirect3D)の演算で使用する事を前提にしていますので、4x4行列かつ、
m[0][3] = m[1][3] = m[2][3] = 0, m[3][3] = 1 固定です。
とりあえずソースを晒します。
ソース:matrix_invert.cpp
Matrixの逆行列を求める場合、ループが多くなり計算量がかさみます。
Quaternionの逆数は3要素の符号を反転させるだけで済みますが、Matrix→Quatrnion→Matrixの変換が必要になります。
あとは実際に値を突っ込んで、トレースなり時間の計測なりしてみてください。
【Direct3DとUpdateLayeredWindowの併用:Windows2000/XP】
Direct3DとUpdateLayeredWindow() APIを併用して、3Dデータをレイヤードウィンドウで表示する方法を考察します。
おいらがDirectX7までしか使ってないので、それ以降のVerを使う場合はFlip周りを相応の方法に置き換えてください。
− サーフィスからHBITMAPへの転送 −
通常D3Dを描画する場合、プライマリバッファへバックバッファをFlipしますが、この部分をHBITMAPへの転送に変更します。
LPDIRECTDRAWSURFACE7 BackBuffer;
BOOL Flip_HBMP( HBITMAP hbmp )
{
if( !BackBuffer )
return FALSE;
BITMAP bm;
GetObject( hbmp, sizeof( bm ), &bm );
if( bm.bmWidth != ScreenW || bm.bmHeight != ScreenH ) // 画面とビットマップのサイズが同じかチェック
return FALSE;
HDC bdc = ::CreateCompatibleDC( NULL );
SelectObject( bdc, hbmp );
HDC ddc;
BackBuffer->GetDC( &ddc );
BitBlt( bdc, 0,0, ScreenW, ScreenH, ddc, 0,0, SRCCOPY );
BackBuffer->ReleaseDC( ddc );
DeleteDC( bdc );
return TRUE;
}
こんな感じですか。
これで、半透明オブジェクトの存在しない(もしくは、半透明オブジェクトが背景に干渉しない)レンダリング結果の取得が可能です。
背景色を緑とかモデルデータが使用していない色(しなそうな色)で初期化し、その色を透過色にすればOKです。
HBITMAPが32bitビットマップの場合、環境によってはアルファ値まで転送してくれます。
が、ちょっと調べたんですが、どういう環境で転送されるのかまではわかりませんでした。
なので、半透明オブジェクトが存在する場合、アルファ値は自分で求めないといけません。
− アルファ値の求め方 −
とは言ってもレンダリング後のバックバッファにアルファ値が無いんじゃどうにもならないので、似非計算になります。
まず、背景を黒でレンダリングし、その結果を32bitカラーのHBITMAP
BMP1にコピーします。
次に、背景を白でレンダリングし、結果を上記と同じくBMP2にコピーします。
BMP1、BMP2それぞれの任意の点pを、BMP1.p、BMP2.pとあらわします。
点pの色情報をそれぞれ、p.r、p.g、p.b、p.a、とあらわし、アルファ情報以外の色情報を特別にp.rgbと表記します。
任意の点pが非透過の場合、BMP1.pとBMP2.pは等しい値になります。(透過してないので、背景色に左右されないため)
任意の点pが完全透過の場合、BMP1.pは黒、BMP2.pは白になります。(物体が何も無い状態なので、背景色にだけ左右される)
では、任意の点pが半透明オブジェクトだった場合はどうなるのか?
25%の透過率の黒いオブジェクトの場合、BMP1.p.rgbは0x000000、BMP2.p.rgbは0xBFBFBF(0xC0C0C0?)に、それぞれなるはずです。
つまり、二つの点の色の差分値からアルファ値を求めることが出来ます。
BMP1.p.a
= 255 - | BMP2.p.rgb - BMP1.p.rgb |
= 255 - ( ( BMP2.p.r - BMP1.p.r )2 + ( BMP2.p.g -
BMP1.p.g )2 + ( BMP2.p.b - BMP1.p.b )2 ) /
1.732
こんな感じでしょうか。
でも、実は冷静に考えれば、アルファ値は各色に均等に作用するので、
BMP1.p.a = 255 - ( BMP2.p.r - BMP1.p.r )
で事足ります。
これを全ての点に対して処理してやれば、ある程度正確なアルファ値を持ったHBITMAPを作成することが出来ます。
この方法の欠点は2度レンダリングするので通常の倍重いという点と、
環境にも寄りますがバックバッファからHBITMAPへの転送は決して速く無いという点です。
リアルタイムでそれなりの処理を考えるのなら、転送面積を小さくするとか、もっと効率的な方法を模索する等、工夫が必要そうです。
【UpdateLayeredWindowの使い方:Windows2000/XP】
Windows2000
から導入された、半透明ウィンドウの使い方を解説します。
SetLayeredWindowAttributes()
を使うと、ウィンドウを均一な透明度で透かす事が出来ますが、
UpdateLayeredWindow()
を使えば、任意のピクセルを任意の透明度で透かす事が出来るようになります。
いくつかの特殊な処理が必要になります。
・LoadLibrary() / GetProcAddress() でAPIのアドレスを取得する必要がある。
・対象のウィンドウの拡張属性に WS_EX_LAYERED (0x80000) を付加する必要がある。
・32bitカラーの HBITMAP を生成する必要がある。
・WM_PAINT で描画処理出来ない。
− LoadLibrary() / GetProcAddress() −
こんな部分は、いちいち考える必要はありません。
以下のコードを適当に自分用に修正してコピペして使ってください。
解らないAPIがあれば、MSDNとかで適当に調べてください。
// Layered.cpp
BOOL ( WINAPI *SetLayeredWindowAttributes ) ( HWND, DWORD, BYTE, DWORD );
BOOL ( WINAPI *UpdateLayeredWindow ) ( HWND, HDC, POINT*, SIZE*, HDC, POINT*, COLORREF, BLENDFUNCTION*, DWORD );
bool LayerdInit()
{
HMODULE hDll;
hDll = LoadLibrary( "user32" );
if( !hDll )
return false;
SetLayeredWindowAttributes = ( BOOL ( WINAPI * )( HWND, DWORD, BYTE, DWORD ) )
GetProcAddress( hDll, "SetLayeredWindowAttributes" );
UpdateLayeredWindow = ( BOOL ( WINAPI * )( HWND, HDC, POINT*, SIZE*, HDC, POINT*, COLORREF, BLENDFUNCTION*, DWORD ) )
GetProcAddress( hDll, "UpdateLayeredWindow" );
FreeLibrary( hDll );
return true;
}
// Layered.h #ifndef WS_EX_LAYERED #define WS_EX_LAYERED 0x80000 #define LWA_COLORKEY 0x01 #define LWA_ALPHA 0x02 #ifndef AC_SRC_OVER #define AC_SRC_OVER 0x00 #endif #define AC_SRC_ALPHA 0x01 #define AC_SRC_NO_PREMULT_ALPHA 0x01 #define AC_SRC_NO_ALPHA 0x02 #define AC_DST_NO_PREMULT_ALPHA 0x10 #define AC_DST_NO_ALPHA 0x20 #define LWA_COLORKEY 0x01 #define LWA_ALPHA 0x02 #define ULW_COLORKEY 0x01 #define ULW_ALPHA 0x02 #define ULW_OPAQUE 0x04 #endif extern BOOL (WINAPI *SetLayeredWindowAttributes) (HWND,DWORD,BYTE,DWORD); extern BOOL (WINAPI *UpdateLayeredWindow) (HWND,HDC,POINT*,SIZE*,HDC,POINT*,COLORREF,BLENDFUNCTION*,DWORD);
− WS_EX_LAYERED (0x80000) の付加 −
ここも定型句なので、コピペで十分です。
LONG style = GetWindowLong( hwnd, GWL_EXSTYLE ); style |= WS_EX_LAYERED; // 属性を外す場合は style &= ~WS_EX_LAYERED; とする。 SetWindowLong( hwnd, GWL_EXSTYLE, style );
− 32bitカラーのビットマップハンドルの生成 −
いく通りかの方法があると思いますが、ここでは私がいつも使ってる方法を紹介します。
ただし、エラー処理をいくつか飛ばしています。
「他の方法で32bitカラービットマップを生成するからいい」という人は読み飛ばしてください。
HBITMAP CreateRGBBitmap( int x, int y, int bpp )
{
if( bpp <= 8 )
return NULL;
BITMAPINFOHEADER bh;
memset( &bh, 0, sizeof( bh ) );
bh.bmiHeader.biSize = sizeof( bh );
bh.bmiHeader.biWidth = x;
bh.bmiHeader.biHeight = y;
bh.bmiHeader.biBitCount = bpp;
bh.bmiHeader.biPlanes = 1;
UCHAR *bitptr;
HDC dc = ::GetDC( NULL );
HBITMAP hbmp = CreateDIBSection( dc, (BITMAPINFO*)&bh, DIB_RGB_COLORS, (void**)&bitptr, NULL, 0 );
::ReleaseDC( NULL, dc );
return hbmp;
}
この方法で生成すると、bitptrがビットマップのメモリーへのポインタになるので、直接操作する事が出来ます。
32bitで生成した場合、1ラインのデータサイズが4の倍数になるので、DWORDアラインは考慮する必要はありません。
また、ビットマップはメモリ上に上下逆さまのイメージで置かれるので注意してください。
終了時にビットマップオブジェクトを削除する場合は、DeleteObject() を使ってください。
− UpdateLayeredWindow() −
適当なタイミングで UpdateLayeredWindow()
を呼んでやります。
例えば、OnInitDialog() で WS_EX_LAYERED
をセットした直後や、タイマー割り込み、各種イベント等、任意のタイミングで更新出来ます。
ただし、SetLayeredWindowAttributes()
を使用してない事が絶対条件です。
既に SetLayeredWindowAttributes()
で半透明化しているウィンドウに対して
UpdateLayeredWindow() を呼ぶと、必ず失敗するようです。
以下は、ラッピング関数です。
void UpdateLayeredImage( HWND hwnd, HBITMAP hbmp, int x, int y )
{
HDC hdcScreen = ::GetDC( NULL );
HDC hdcMemory = ::CreateCompatibleDC( hdcScreen );
SelectObject( hdcMemory, hbmp );
CRect rect;
GetWindowRect( rect );
CPoint pos( rect.TopLeft() );
CSize szWindow( x, y );
CPoint ptSrc( 0, 0 );
BLENDFUNCTION bf = { AC_SRC_OVER, 0, 0xff, AC_SRC_ALPHA };
int ret = UpdateLayeredWindow( hwnd, hdcScreen, &pos, &szWindow, hdcMemory,
&ptSrc, 0, &bf, ULW_ALPHA );
ASSERT( ret );
::DeleteDC( hdcMemory );
::ReleaseDC( NULL, hdcScreen );
}
ASSERT( ret ); で引っかかる場合は、SetLayeredWindowAttributes() を既に呼んでいるか、32bitカラーのビットマップに問題がある事が多いです。
なんだか再帰構造は世間で嫌われてるようなので、逆に再帰構造を勧めてみる。
再帰が敬遠される主な理由は、
・開始条件が解らない
・終了条件が解らない
・エラー処理が解らない
・使い所が解らない
こんな所だろうか?
上記の理由を四則演算と括弧 ( )
のソースを交えつつ、一つずつ潰していってみよう。
−四則演算の構文解析−
いきなり全てを書いてもアレなので、とりあえず加減算から始めてみる。
入力される文字列は、'0'〜'9'の整数と、'+'、'-'の記号、とする。
char *src = "12-3+5";
char *str = src;
int GetValue()
{
int x = atoi( str );
while( isdigit( *str ) ) // 数字読み飛ばし
str++;
return x;
}
int AddSub()
{
int x = GetValue();
while( true )
{
switch( *str )
{
case '+':
str++; // '+'読み飛ばし
x += GetValue();
break;
case '-':
str++; // '-'読み飛ばし
x -= GetValue();
break;
default:
return x;
}
}
return 0;
}
str に式の文字列のポインタをセットして、AddSub()
を呼ぶ。
GetValue()
で値を取得して、値同士が結合されてる記号によって加減算に振り分けているだけ。
御覧の通り、単純な加減算だけなら再帰構造は必要無いです。
これに括弧を追加して、括弧内を再帰的に処理させてみます。
char *src = "12-(3+5)";
char *str = src;
bool err = false;
int GetValue()
{
int x = atoi( str );
while( isdigit( *str ) ) // 数字読み飛ばし
str++;
return x;
}
int AddSub();
int Parenthesis()
{
if( *str != '(' ) // 括弧でない場合は数字
return GetValue();
str++; // '('読み飛ばし
int x = AddSub(); // 括弧の中の加減算を処理
if( *str != ')' ) // エラー
err = true;
str++;
return x;
}
int AddSub()
{
int x = Parenthesis();
while( true )
{
switch( *str )
{
case '+':
str++; // '+'読み飛ばし
x += Parenthesis();
break;
case '-':
str++; // '-'読み飛ばし
x -= Parenthesis();
break;
default:
return x;
}
}
return 0;
}
AddSub() は Parenthesis() を呼び、
Parenthesis() は
括弧でなければ数字を返し、括弧ならば括弧内の処理を
AddSub() に行わせ、その返り値を返す。
AddSub() 内の一行目で GetValue() では無く Parenthesis()
を呼んでいるのは、"(1+2)*5" のように括弧で始まる式がある為。
Parenthesis()
は数字、式に関わらず、評価済みの値が返ってくる点に注目してください。
err が true だった場合は、呼び出し元でエラー処理を適当にしときます。
最後に乗除算の処理を追加します。
char *src = "12/2*(5*(4+3)-7)*4";
char *str = src;
bool err = false;
int GetValue()
{
int x = atoi( str );
while( isdigit( *str ) ) // 数字読み飛ばし
str++;
return x;
}
int AddSub();
int Parenthesis()
{
if( *str != '(' ) // 括弧でない場合は数字
return GetValue();
str++; // '('読み飛ばし
int x = AddSub(); // 括弧の中の加減算を処理
if( *str != ')' ) // エラー
err = true;
str++;
return x;
}
int MulDiv()
{
int x = Parenthesis();
while( true )
{
switch( *str )
{
case '*':
str++;
x *= Parenthesis();
break;
case '/':
str++;
x /= Parenthesis();
break;
default:
return x;
}
}
return 0;
}
int AddSub()
{
int x = MulDiv();
while( true )
{
switch( *str )
{
case '+':
str++; // '+'読み飛ばし
x += MulDiv();
break;
case '-':
str++; // '-'読み飛ばし
x -= MulDiv();
break;
default:
return x;
}
}
return 0;
}
・開始条件が解らない
開始条件は特に無いです。各関数内で処理出来る演算子/文字列を、それぞれが処理するだけです。
・終了条件が解らない
終了条件は、AddSub() と MulDiv() は自分が処理できない演算子/文字を見つけた時です。
・エラー処理が解らない
エラー処理はグローバル変数のフラグで処理してます。C++ならクラス化してメンバで持っても良いし、関数に構造体のポインタを渡すようにしてもいいかもしれません。
実際は、もっと想定外の入力を考慮しないと実用的ではないです。(空白の読み飛ばし等)
・使い所が解らない
入力データが不定で、多重ネストを許しているようなデータに対して非常に有効です。
例えば、ワーカースレッドを完全に非同期で動かせるのなら、それに越した事は無い。
ところが、重い処理などを別スレッドに処理させるわけで、当然途中で中断したい事も出てくる。
Windowsは基本的にはスレッドは自分自身で終了させないといけない。
#TerminateThread()で殺すと、リークやその他の問題が起こる可能性アリ
例えば、数秒で終わるスレッドなら、終わるまで待ってもいい。
WaitForSingleObject( threadHandle, INFINITE );
これで、スレッドが死ぬまで待つ。
ところが、死ぬはずのワーカースレッドが死なない時がある。
ワーカースレッド内で、SendMessage()
等を呼んでいる時だ。
SendMessage()
は、処理が完了するまで帰って来ないので、WaitForSingleObject()
で待ち状態になってしまったスレッドにメッセージを投げると、その時点で両方のスレッドがロックしてしまう。
スレッドの進捗状況を知るため、程度なら PostMessage() でもいいが、PostMessage() は稀にメッセージが届かない事がある。絶対に取り逃しが許されないメッセージの場合には致命的過ぎる。
この状況を回避する方法としては、
while( ::WaitForSingleObject( threadHandle, 50 ) == WAIT_TIMEOUT )
{
MSG msg;
::GetMessage( &msg, NULL, 0, 0 );
if( !IsDialogMessage( &msg ) ) // ダイアログ内の場合
{
::TranslateMessage( &msg );
::DispatchMessage( &msg );
}
}
こんな感じで、メッセージを適当に処理してやると良い。
PostMessage()
だと、スタックメモリ内の文字列や構造体を渡すときに、いちいち別にメモリを確保してやんなきゃならないけど、
SendMessage()
ならそのままポインタを渡す事も出来るので、デッドロックだけ回避できれば、使い勝手はいい。
ただ、同期動作なのでPost系の方が軽いが。
「スレッドが死ぬまでなんて、悠長に待ってられるか!」
という、気の短い江戸っ子気質のあなたには、次のような感じがオススメ。
// ワーカースレッド
UINT __cdecl hogeThread( LPVOID pParam )
{
MSG msg;
int i;
for( i = 0; i < 10000000; i++ )
{
if( ::PeekMessage( &msg, NULL, NULL, NULL, PM_REMOVE ) )
if( msg.message == WM_USER + 40 )
break;
// なんか処理
}
return 0;
}
// メインスレッド
void foo()
{
PostThreadMessage( WM_USER + 40, 0, 0 );
WaitForSingleObject( threadHandle, INFINITE );
}
ワーカースレッドなんざ、終了メッセージだけ受け取れりゃいいわけです。
その他のメッセージなんか、しらんぷりんしてりゃいいんです。(ホントか?)