スレッド上からボタンをDisableにしたい

[戻る]
トビア 2017/08/24(木) 14:17:22
サブスレッドからUIのボタンをDisableにしたいと思っています。

下記のようにすると、ボタンの見た目はDisableにできるのですが
スペースキーで何故か押下できてしまい、ボタンクリックイベントも発生してしまい
ます。

スレッド上から、正しくボタンをDisableにさせるには
どうしたらよいでしょうか?

BOOL CTestDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    m_hThread = (HANDLE)_beginthreadex(NULL, 0, TestThread, 
m_btn.m_hWnd, 0, NULL);

    return FALSE;
}

unsigned __stdcall CTestDlg::TestThread(LPVOID pParam)
{
    CButton *btn = (CButton*)FromHandle((HWND)pParam);
    btn->EnableWindow(FALSE);

    //〜 ボタンをDisableにしてから行いたい処理 〜

    return 0;
}

トビア 2017/08/24(木) 14:21:47
なお、自作のメッセージを作ってのPostMessageだと正しくDisbleにできたのです
が、
ボタンを完全にDisableにしてから行いたい処理があるので、
同期が取れないPostMessageは使えない状況です。
SendMessageはデッドロックする可能性がある、とのことなので使いたくないです。

https://msdn.microsoft.com/ja-jp/library/h14y172e.aspx

ここを見ると、スレッドへハンドル渡しならできる、みたいに記載されてますが
上記のハンドル渡しの方法は駄目なのでしょうか?


↓この方法は、Disableにだけはできました

BOOL CTestDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    m_hThread = (HANDLE)_beginthreadex(NULL, 0, TestThread, this, 0, 
NULL);

    return FALSE;
}

unsigned __stdcall CTestDlg::TestThread(LPVOID pParam)
{
    CTestDlg *pDlg = (CTestDlg*)pParam;

    pDlg->PostMessage( WM_DISABLEBUTTON );

    //〜 ボタンをDisableにしてから行いたい処理 〜

    return 0;
}

// 自作メッセージの処理
afx_msg LRESULT CDlgDriverInstall::OnDisableButton(WPARAM wParam, LPARAM 
lParam)
{
    m_btnCancel.EnableWindow( FALSE );
    return 0;
}

仲澤@失業者 2017/08/28(月) 16:13:51
2点気になるところがあります。
両者の競合脱線かもしれません。

1.CWndの派生クラス(CObjの派生クラスも)は、スレッド間を跨いで受け渡しすることがで
きません。
 Win32SDK関数の EnableWindow( HWND hWnd, BOOL bEnable); を使用してみてはどうで
しょう。

2.スレッドの起動関数に依存して、スレッド関数内での制限があります。注意しましょう
。
  CreateThread     :  スレッド関数内では、C言語ラインタイムが使えない
  _beginthread(ex) :  スレッド関数内では、(多くの)MFCクラスが使えない
  AfxBeginThread   :  スレッド関数内で、C言語ランタイム、MFCとも使える

また、SendMessage()がデッドロックを引き起こすというのは誤解です。
SendMessage()がスレッドA内から発行されるときは、一旦スレッドAを止めてから
SendMessage()を行い、対象HWNDのコールバックから戻ったのち、スレッドAを再開します
。
このときのメッセージ処理中に、さらにメッセージを送付するなどの危うい処理がなけれ
ば、
一般的にデッドロックすることはあり得ません。

トビア 2017/08/28(月) 17:50:18
ご回答&ご指摘ありがとうございます。


>_beginthread(ex) :  スレッド関数内では、(多くの)MFCクラスが使えない
これ、知らなかったです。
しかし、私が使っている限りでは、_beginthreadex でMFCが使えなかった事がありませ
ん。
CStringなどの基本的なMFCクラスは問題なくて、あまり使わないようなクラスがNGなんで
しょうか?

ちなみに、今回の問題を _beginthreadex をやめて AfxBeginThread を使ってみたのです
が
結果は変わらず、関係ないようでした。


>Win32SDK関数の EnableWindow( HWND hWnd, BOOL bEnable); を使用してみてはどうで
しょう。
こちらも、試してみました。

最初の投稿のソースで、
 ::EnableWindow( (HWND)pParam, FALSE);
としてみましたが、結果は全く同じで、
見た目はDisable状態でキー操作だけ受け付けてしまいます。


>また、SendMessage()がデッドロックを引き起こすというのは誤解です。
自身のプログラム上の処理ではデッドロックする可能性はほぼゼロなのですが、
メインのUIスレッドにOSやほかのアプリなんかからメッセージが送られて
止まっている状態などでは、デッドロックする、という話を聞いた事があります。

「スレッド上からSendMessageすると、何か特殊な事があった場合に
 デッドロックする可能性があるから、出来る限り使わないほうが良い」
と教えられていました。

確かに、一般的にデッドロックする事はないと思うのですが、気にしすぎでしょうか?

ITO 2017/08/29(火) 09:59:01
うーーん、

これを
 >CButton *btn = (CButton*)FromHandle((HWND)pParam);
 >btn->EnableWindow(FALSE);
このスレッドのルーチンないで行うと、他のウインドウが重なったりすると
HWND == NULL等の例外が発生してソフトが異常終了する可能性があります。
>unsigned __stdcall CTestDlg::TestThread(LPVOID pParam)
ウインドウ操作の関数は、必ず表示スレッドで行うようにした方がいいです。

メインスレッドとの間で共有のフラグを作ってメッセージ処理と同じこととをするのは
だめですか?

仲澤@失業者 2017/08/29(火) 13:23:59
EnableWindow()が実行されると、WM_CANCELMODE、WM_ENABLE、等が
当該ウインドウに送付されて設定結果が反映されます。
従って、有効設定の対象ボタンを持つGUI側のスレッドのメッセージキューが
回らないと有効設定が反映されないと考えられます。

従って
(1)EnableWindow(HWND,FALSE);
(2)while( FALSE == IsWindowEnabled( HWND))  sleep(1);
(3)非活性化後の処理
の様な、処理待ちする必要があるかもしれません。

又は、スレッド内からのボタンの非活性化をあきらめ、
別の場所で行うべきかもしれません。
個人的にはこっちをお勧めします。

gak 2017/08/29(火) 22:05:30
基本的にHWNDを介する操作は一部例外(SendMessage/PostMessage/AttachThreadInput後の
Focus設定系等)を除き他スレッド(HWND作成スレッド以外)からすべきでは無い。
※特にFocus設定系の処理は明確に失敗する(MSDNにも記述有り)

今回の場合、EnableWindowを他スレッドからCallしているので、無効化処理過程で
Keyboardフォーカスの移動に失敗し、フォーカスが無効ボタンに残ったままになっている
のでは無いだろうか。

>  ::EnableWindow( (HWND)pParam, FALSE);
Window無効時のマウス/キーボード等の入力無効化は結構浅い階層で行われていたと思う。

 ::EnableWindow(hwnd, FALSE); // hwnd == ボタンのHWND
 ::PostMessage(hwnd, WM_LBUTTONDOWN, ...);
 ::PostMessage(hwnd, WM_LBUTTONUP, ...);

という感じのコードを昔試した時は、無効状態にも関わらずClickイベントが起きた。
なので、(先述した様に)Keyboardフォーカスが残っていた場合、ボタンに対して
WM_KEYDOWN等が発生して押下処理が起きているのかも。

# 開発環境無いので験していません。以下感じのコードでClickイベントが起きるならば
# 私見が合っているのかもしれない...

 ::EnableWindow(hwnd, FALSE);
 ::PostMessage(hwnd, WM_KEYDOWN, VK_SPACE);
 ::PostMessage(hwnd, WM_KEYUP, VK_SPACE);

トビア 2017/08/30(水) 11:42:29
>ITOさん
> メインスレッドとの間で共有のフラグを作って
> メッセージ処理と同じこととをするのはだめですか?
これは、メインスレッドでフラグの監視をずっとループか何かで見る、
ということでしょうか?


>仲澤@失業者さん
IsWindowEnabled()はボタンのスタイルだけを見ている関数のようで、
ボタンが実際にDisableになっているかどうかは見ていないようです。
EnableWindow()でスタイルの設定はすでにできているようで、
表示DisableだがClickできる状態 だった場合、
IsWindowEnabled()はFALSEでかえってきちゃいます。

別の場所で行う手段でよい案が浮かばないのですが、
何か良い方法がありますでしょうか?


>gakさん
 ::EnableWindow(hwnd, FALSE);
でボタン見た目Disable + Clickは受け付ける状態で、

 ・スレッド内でMessageboxを表示
 ・多アプリを操作して戻ってくる

のどちらかをすると、ボタンはClickも受け付けなくなりました。
(正しい動きになった)

おそらく、スレッド上でDisableにすると
メインスレッドにメッセージは投げられているのですが
メッセージキューの処理が止まっている?ようで、
何かキューの処理が進むような事をすれば正常にメッセージが処理されて
Disableも正常に行われてClickも無効となるようです。

でも見た目だけDisableになって、Clickだけ効かないってのも
推奨されていない方法とはいえ、変な動作ですね。
(MFCのバグでしょうか?)

gak 2017/08/30(水) 22:11:11
> メッセージキューの処理が止まっている?ようで、
メッセージキュー処理止まってたらスペースキー押下でクリック発生なんて起き得ないの
で違うんじゃないかな。

> ・スレッド内でMessageboxを表示
> ・多アプリを操作して戻ってくる
どちらもフォーカスの更新が発生する操作だね。

@他スレッドからEnableWindow()
 ⇒無効化されたボタンからフォーカス退避するのに失敗
Aスレッド内でMessageboxを表示+閉じる
 ⇒無効ボタン→Messagebox→ダイアログ、とフォーカス遷移
Bスペースキー押下
 ⇒Aで無効ボタンからKeyboardフォーカスが外れてるので、無効ボタンのクリック無効

とでもなってるんじゃないかと。

> 基本的にHWNDを介する操作は一部例外を除き他スレッドからすべきでは無い。
ちなみに(WIN32互換の).Netフォームアプリ(非互換のWPFでもそうだけど)では、他スレッ
ドからWindow操作しようとするとFrameworkが例外返してくる。
問題なけりゃこんな制限は設けないと思うので、今回のケースでも他スレッドからの直接
無効化は諦め、Send/PostMessageで自スレッドにswitchして無効化するのが無難に思う。

ITO 2017/08/31(木) 09:17:46
>これは、メインスレッドでフラグの監視をずっとループか何かで見る、
>ということでしょうか?
そうですね、僕は、表示スレッドのWM_TIMERでまとめて処理していますね。

トビア 2017/09/01(金) 13:30:07
>gakさん
なるほど、フォーカス遷移の仕組みが良くわかりました。
私のキュー云々の想定ではなさそうですね。

確かに、一度別の場所にフォーカス移動しているので
更新されて正常な動作になっているのかもしれませんね。

MFC/C++も出来ないなら出来ないでエラー返してくれれば良かったんですけどね。
中途半端にフォーカスだけ残るとかバグっぽい動作はやめて欲しい。


>ITOさん
なるほど、タイマーですか。
タイマーであれば、スレッドから更新したい時にだけ
Settimerして、普段はタイマーOFFにしておけば無駄にループ処理も
行われていなくて良さそうですね。

古いスレッドには追加発言できません。