スレッドとポップアップメニューについて

ActiveBasicでのプログラミングでわからないこと、困ったことなどがあったら、ここで質問してみましょう(質問を行う場合は、過去ログやWeb上であらかじめ問題を整理するようにしましょう☆)。
返信する
メッセージ
作成者
jacoby
記事: 106
登録日時: 2006年6月02日(金) 18:20

スレッドとポップアップメニューについて

#1 投稿記事 by jacoby »

 一つのスレッドを持つプログラムで
マウスの右クリックからポップアップメニューを
表示させたいと思っているのですが、
そのメイン・スレッド内で「TrackPopupMenu」
を書いてもポップアップが表示されません。

イベント・コードの「RButtonDown」(Sub MainWnd_RButtonDown())
からTrackPopupMenuを実行するときちんと表示
されるのですが、どうしてスレッド内から呼び出せない
のか分からず悩んでいます。

もちろん上記のようにイベントから呼び出すことは
出来たのですが、それだとそのメニューの選択によって
スレッド内で使う変数に変化を加えようとすると
スレッドとの「同期」をさせなければならなくなるのかなとも
思ったりして、自分には大変だなと。なので出来れば
メインのスレッド内から呼び出して、メニューの選択が終わるまでは
スレッドも待機させておきたいと思うのですが
そのようなことは可能でしょうか?
よろしければ教えてください。

(AB ver4.04を使っています。)

※過去ログを調べていたら
No.1381 「TrackPopUpMenu」が同様の質問かな
とも思いました。ただ解決は分かりません。
jacoby
記事: 106
登録日時: 2006年6月02日(金) 18:20

スレッドとポップアップメニューについて

#2 投稿記事 by jacoby »

 自己レスです。

とりあえず言っていたことは以下のようにして実行することができました。

 まずメインのスレッド内でGetAsyncKeyState()で
右クリックの状態を取得。
 右クリックが押されていたら、
  「SendMessage(hMainWnd,WM_NULL,wp,lp)」
として、"WM_NULL"を送る。(このメッセージはもしかして
勝手に使ってもいいんじゃないかと思ったので…。確信はありません。)
 MainWndProc()内でそのメッセージを受け取ったら、
「TrackPopUpMenu」でポップアップメニューを表示させる。

 SendMessage()という命令は
「プロシージャが終了するまで制御が戻りません。
つまり、完全に同期がとれていると考えることができます。」
と説明にあったので、これで出来るのではないかと思って
やってみました。
あまりいいやり方ではないかも知れません。
もっと良い方法があればまた教えてください。

 報告を兼ねて、自己レスでした。


 ※ただこの方法だとメインスレッドのループで
(GetAsyncKeyState()で)タイミングが悪いと
右クリックの判定を拾い損ねることがあって、
クリックしてもポップアップが表示されないことがあります。

 あとこの方法と直接関係はないのですが、
ポップアップメニューが表示されている状態で、
マウスポインタを動かして、そこから更にもう一度右クリックすると
エクスプローラなどでは改めて新しい位置でポップアップが
立ち上がるのですが、自分の作ったプログラムでは、
TrackPopUpMenuだけではそうはならないみたいで
元の位置に表示されたままです。
これはどうすればいいのかな、と思っています。
イグトランス
記事: 899
登録日時: 2005年5月31日(火) 17:59
お住まい: 東京都
連絡する:

#3 投稿記事 by イグトランス »

うーん,いまいち状況が掴めません。
差し支えない範囲でソースコードを見せてほしいです。
Tomorrow
記事: 72
登録日時: 2005年6月04日(土) 10:09

Re: スレッドとポップアップメニューについて

#4 投稿記事 by Tomorrow »

>  あとこの方法と直接関係はないのですが、
> ポップアップメニューが表示されている状態で、
> マウスポインタを動かして、そこから更にもう一度右クリックすると
> エクスプローラなどでは改めて新しい位置でポップアップが
> 立ち上がるのですが、自分の作ったプログラムでは、
> TrackPopUpMenuだけではそうはならないみたいで
> 元の位置に表示されたままです。
> これはどうすればいいのかな、と思っています。
TrackPopUpMenu関数の第2引数にTPM_RIGHTBUTTONを指定してみてください。
右クリックでのメニュー選択・解除を受け付けるようになります。
jacoby
記事: 106
登録日時: 2006年6月02日(金) 18:20

レスありがとうございます。

#5 投稿記事 by jacoby »

 イグトランスさん、レスありがとうございます。リストの整理に
手間取ってしまい返信を遅らせてしまいました。すみません。
 メイン部分のソースを書きます。

 プログラムは「マウスで左クリックした所を中心に、同心円を
外側へ向かって20個、さざ波が広がるようにゆっくり描いていく」
というものです。
 右クリックでポップアップメニューを表示して、その選択により
描画色を変える。(ポップアップには「赤に変更」、「緑に変更」、
「青に変更」、の3つのメニュー項目)
 ただし、もし20個の同心円の描画中に色が変えられた場合、
今描画している20個はそのままの色で描き、色の変更は次回から
反映させるものとする。
(プログラムはAB 4.04のプロジェクトで作っています)
 主な流れは、
1,メインのスレッド「MainOperation」でループ
 マウスの位置、クリックを取得し、もし左クリックが
押されたらその位置に同心円を序々に表示。
 右クリックならポップアップメニューを表示する
為に「WM_NULL」をSendMessage。

2,コールバック関数「MainWndProc()」で
そのWM_NULLを拾ったらShowPopUpMenu()をコール。

3,ShowPopUpMenu()ではTrackPopupMenuで
ポップアップメニューを表示し、その戻り値を
popUpMenuRetというグローバル変数に入れ、
SelectCasePopUpMenu()でその値を元に
それぞれの色に変更。

4,色の変更処理が終わったらメインスレッドの
SendMessageをした次の命令からメインループ再開。

となります。

 この方法でとりあえずは出来たのですが、
問題は「For-Nextで同心円を20個描画している最中は
右クリックの取得判定をしていないのでポップアップが
表示されない」ということです。

 もちろん単にFor-Nextのループ中にも
GetAsyncKeyState()などを置いてやってキメ細かく取れば
いいようなものですが、やっぱりそれでは根本的な解決に
ならないので他のやり方を試すことにしました。
 それが次のプログラムです。
右クリックはイベントとして取得し、右クリックが
あったときはメインスレッドの状態にかかわらず、
必ずMainWnd_RButtonDown()へ飛ぶ。

 そこでメインスレッドを一時停止させて
ポップアップメニューを表示させる。

 ポップアップメニューの戻り値はpopUpMenuRetに入れ、
また右クリックが押されたということをスレッド内で
把握できるように「myMainWndMsg」というグローバル変数を
用意し、それにWM_RBUTTONDOWNを入れておく。

 スレッドを再開させる。

 再開されたスレッドの中では、myMainWndMsgを参照し
何かメッセージがあればSelectCaseMyMainWndMsg()へ飛ぶ。

 メッセージがWM_RBUTTONDOWNであれば
ポップアップの戻り値の分岐であるSelectCasePopUpMenu()へ。

 SelectCasePopUpMenu()で色を変更。

 すべて変更が終わってメインループへ戻る。

 となっています。


 これで、初めの指針に沿った動作はしてくれるのですが、
一つ気になるのが「右クリックを押してポップアップメニューが
表示されたとき、(マウスポインタを動かして)更にそこで右クリックを
押した時の動作」です。
エクスプローラなどでは新しい位置で改めてポップアップが立ち上がりますが、
このプログラムではそうはならず、ポップアップメニューは元の位置に
表示されたままです。
 イベントでMainWnd_RButtonDown()へ飛んだあと、そのサブ内で
処理中にさらに右クリック・ダウンが発生した場合、処理はどうなるのでしようか?
 (自分で試したみたところでは、改めてMainWnd_RButtonDown()が呼ばれる
ているようです。)
その場合、このプログラムではSuspendThreadでスレッドを止めています。
 サスペンドカウンタはどうなるのでしょうか。
 (これも実際に取って見ましたが、まず最初の右クリックで
呼び出し前のサスペンド カウントの値=0が返ります。それから更に
右クリックすると今度は1が返ります。ただしそれ以降何度右クリック
してもサスペンドカウンタは1のままです。)


 このあたりが良く分からずにいます。宜しければ御教授下さい。
プログラムのほうでも、「こう組んだ方がいい」というのがありましたら
是非教えてください。

 長くなってすみません。よろしくお願いします。

----------------------------------------------------------

 この返信を書き終えた後にTomorrowさんのレクを確認してしまったので
ちょっと間が抜けた感じになってしまいましたが、改めて、

 Tomorrowさん、レスありがとうございます。
教えていただいた通り第2引数にTPM_RIGHTBUTTONを
セットしてやってみたら、ポップアップは望んだ通りの動きを
してくれました。しかもこれを設定すると右クリックを何度
押してもスレッドカウンタは0のまま変わらず、安心して呼び出せる
ようにもなりました。(何故かはまだ分かってないのですが…)
 ありがとうございました。またよろしくお願いします。
イグトランス
記事: 899
登録日時: 2005年5月31日(火) 17:59
お住まい: 東京都
連絡する:

#6 投稿記事 by イグトランス »

GetAsyncKeyStateではどうしても「取りこぼし」が発生する可能性を無くすことができません。
Windowsはイベント駆動(ドリブン)型ですから,それに沿ったプログラムの作りを念頭に置かれています。
つまりマウスボタンのクリックはOnLButtonUpやOnRButtonUpなどのイベントで検出するようにする方が確実です。

その場合RButtonUpイベントではTrackPopupMenuを(TPM_RETURNCMDを指定して)呼ぶと共に,
その戻り値でSelectCasePopUpMenu相当のことを行えるはずです。

左ボタンクリックはLButtonUpイベントで検出しますが,それをどうにかして「メインスレッド」へ伝えればよいのです。
それにはやっぱりメッセージが楽です。スレッドを作ったときに得られるIDを元にPostThreadMessageでメッセージを投げることができます。

コード: 全て選択

Sub MainWnd_LButtonUp(flags As Long, x As Integer, y As Integer)
	PostThreadMessage(threadID, WM_LBUTTONUP, flags, MAKELONG(x, y)) 'threadIDはグローバル変数にしておく
End Sub
そして「メインスレッド」では,たとえばこういう風にしてそのメッセージを受け取ったら動作するようにします。

コード: 全て選択

Function MainOperation(dummy As DWord) As DWord
	While 1
		Dim msg As MSG
		Dim ret As Long
		ret = GetMessage(msg, 0, 0, 0)
		If ret = 0 Or ret = -1 Then
			MainOperation = msg.wParam
			Exit Function
		End If

		If msg.message = WM_LBUTTONUP
			Dim x As Long, y As Long
			x = LOWORD(msg.lParam)
			y = HIWORD(msg.lParam)
			Dim I As Long
			For I = 0 to 19
				DrawCircle(hMemDC, x, y, 4 + I * 2)
				InvalidateRect(hMainWnd, ByVal NULL, FALSE)
				Sleep(100)
			Next I
		End If
	Wend
End Function
なお,排他制御は話を簡単にするため行っていません。

ところで今まで「メインスレッド」と書きましたが,
普通はメインスレッドと言うとCreateThreadなどで後から作ったスレッドではなく,
アプリケーションの実行を開始するときから実行を始めるスレッドの事を指します。
ウィンドウプログラムではウィンドウを作ってメッセージループに入る流れがメインスレッドの主な動作です。(ABでRADを使ったときもそうなっています)
jacoby
記事: 106
登録日時: 2006年6月02日(金) 18:20

レクありがとうございます。

#7 投稿記事 by jacoby »

 レクありがとうございます。
返信遅れてすみません。

「PostThreadMessage」,「GetMessage」
スレッドに対するメッセージの受け渡し方法
勉強になりました。(「メインスレッド」も)
 それから、
つまりマウスボタンのクリックはOnLButtonUpやOnRButtonUpなどの
イベントで検出するようにする方が確実です。
「Down」でなくて「Up」なんですね。
 初め左クリックも「Down」イベントで取っていて、
そのときはクリック反応がうまく取れたり取れなかったりで、
何故なんかなと思いながら、GetAsyncKeyStateに変えました。
言われてみればクリックが押されてなければ「Up」は
あり得ないとも思えます。
 下のような簡単なコードで実験したところ、

コード: 全て選択


Sub MainWnd_LButtonDown(flags As Long, x As Integer, y As Integer)
 Print "Left Down"
End Sub

Sub MainWnd_LButtonUp(flags As Long, x As Integer, y As Integer)
 Print "Left Up"
End Sub
 カチカチとクリックする度に
常に、"Left Down"と"Left Up"がセットで表示される
筈だと思ったのですが、必ずしもそうではなく、
"Left Down"はしばしば表示されないことも
ありました。
 ところが"Left Up"はクリック毎に確実に表示される。
そう思って改めて見直すと、ポップアップメニューの選択時
などもボタンが「放された」時に決定されているようで、
ここらへんの理由はハッキリ自分にはわかりませんが
(Upの信号がDownの信号に被さってしまうのかなとか、
勝手にありこれ考えますが…)
いずれにしても以降、「Up」にコードを書くようにしたいと思います。
 それで上の様にソースを変えたのですが
困った点が二つ出てきました。

 一つは、もし設計として「同心円を描画中の左クリックは無視する」と
するなら、どうするべきか。
 このままだと同心円を描画中も左クリックを拾ってしまい、カチカチと
クリックを繰り返すと後から後からその位置に円が描画されてしまう。
 左クリックのメッセージが「メッセージキュー」に溜まるためか、
と思ったのですが、そこからの解決が中々分からず。

 もう一つは、
 ポップアップメニューを開いて、でも"やっぱりやめた"と何も選択しない場合
メニュー領域以外を左クリックしてクリアする時があります。
 自分のソースではそのときの左クリックを「同心円の描画ポイント」として
取ってしまいそこで描画を開始してしまう。
 これも設計として「ポップアップメニューのクリア時の左クリックは同心円の
描画ポイントとして扱わない」とするなら、どうするべきか。
 メニューで何か選択したときは例によってクリックが「Up」になるまで
TrackPopUpMenuの制御の下のようで大丈夫なのですが、
クリアするときの左クリックはDownされたときに処理が戻ってくるようで
そこから改めてUpになると、「新たな左クリック」と認識されてしまいます。
 これがどうにも、その違いを区別させることが難しくて。
 また初めのコードのMainWnd_RButtonDown()内でやってたように

コード: 全て選択

   
 '↓左クリックがUpになるまで待つ
  'ただし、時々左クリックがUpになっていないのにこの判定ループを
  'すり抜ける時がある
  While GetAsyncKeyState(1) And &H8000
  Wend
とするのは、あんまり良くないような。

 ズルズル聞いてしまってすみません。
もし解決の方法、ヒントがあれば教えてください。
イグトランス
記事: 899
登録日時: 2005年5月31日(火) 17:59
お住まい: 東京都
連絡する:

#8 投稿記事 by イグトランス »

UpかDownかは,私も根拠があってのことではなく,単にUpで反応するものが多かったからと言うだけでUpを選んでいます。

描画中に左クリックされた場合の対処は,描画中は左クリックされてもメッセージを投げないと言う風にすればよいです。

たとえばこんなグローバル変数を用意し,

コード: 全て選択

Dim IsDrawing As Long
描画中であることを記録させます。

コード: 全て選択

If msg.message = WM_LBUTTONUP Then
    IsDrawing = TRUE
    ' 中略
    IsDrawing = FALSE
End If
あとは大体想像が付くとは思いますが,こんな風に描画中でないときだけ投げると言う事が実現できると思います。

コード: 全て選択

Sub MainWnd_LButtonUp(flags As Long, x As Integer, y As Integer) 
    If IsDrawing = FALSE Then
        PostThreadMessage(threadID, WM_LBUTTONUP, flags, MAKELONG(x, y))
    End If 
End Sub
もう1つのメニューキャンセル時は,今まで考えたことがありませんでした。
メニューが表示されていようとお構いなく左クリックを受け付けるという挙動が一般的ですから。
jacoby
記事: 106
登録日時: 2006年6月02日(金) 18:20

レクありがとうございます。

#9 投稿記事 by jacoby »

素早いレスありがとうございます。

今度はMainWnd_LButtonUp()に向けて描画中のフラグ
を立ててやるんですね。
 
 それから、メニューキャンセルですが、
このプログラムではやっぱりそれが出来ないとなると
かなり不便になってしまうので、もうちょっと考えて
みようと思います。
 また何かいい方法があったらその時は是非教えてください。

 長々と聞いてしまいましたが、ありがとうございました。
プログラムも丁寧に書いて貰って。参考にさせてもらいます。
またよろしくお願いします。
Tomorrow
記事: 72
登録日時: 2005年6月04日(土) 10:09

Re: レクありがとうございます。

#10 投稿記事 by Tomorrow »

 それから、メニューキャンセルですが、
このプログラムではやっぱりそれが出来ないとなると
かなり不便になってしまうので、もうちょっと考えて
みようと思います。
 また何かいい方法があったらその時は是非教えてください。
メニュー自体にキャンセルする項目をつけるのはどうですか?
こんな風に↓

コード: 全て選択

――――――――
| 赤に変更  |
| 緑に変更  |
| 青に変更  |
|―――――――|
| キャンセル |
――――――――
「キャンセル」項目をクリックした時点でメニューは消えてくれますし。
jacoby
記事: 106
登録日時: 2006年6月02日(金) 18:20

レスありがとうございます。

#11 投稿記事 by jacoby »

レスありがとうございます。

僕もそれを考えたんですが、ただ
ポップアップのキャンセル項目をクリックしてクリアした場合と
領域外クリックでクリアした場合で
動作が異なってくるので、迷っていました。

それで結局、まどろっこしい方法なんですが
別のやり方で試してみました。

1,まずポップアップメニューの戻り値である
グローバル変数「popUpMenuRet」をあらかじめ「-1」にセット

2,ポップアップを領域外クリックでクリアした場合
popUpMenuRetに「0」がセットされて戻ってくるので
左クリックイベントのMainWnd_LButtonUp()の中では
「もしpopUpMenuRetが0なら
    ・左クリックUPのメッセージをスレッド1にポストしない
    ・popUpMenuRet=-1」
と書く。

 この方法で何とか実行させました。
あまりスマートな方法じゃないなーと思ってはいるんですが。

Tomorrowさん、レスありがとうございます。
また何かいいアイデアがあったら教えてください。
返信する