ドナドナされるプログラマのメモ

Windows用アプリのプログラミングメモ

重い処理中にステータス(状態)を表示するダイアログの実装(ユーザーインターフェーススレッド)

解決したい課題

MFCを使ったソフトにおいて重い処理をしている最中に、進捗等を表示するダイアログを表示したくなることは多々ある。しかし、単純にダイアログを作って表示しようとしても、以下のようにうまく行かないことが多い。

  • 重い処理が終わるまでダイアログが表示されない
  • ダイアログは表示されるが、表示内容が更新されない

何が原因か

これらが起きるのは、今の処理が終わらないとダイアログを表示するための処理が実行されないためである。ダイアログの表示やコントロールの変更を処理するメッセージはダイアログのメッセージプロシージャで処理されるのだが、困ったことにこのメッセージプロシージャは、デフォルトではメインウィンドウのメッセージプロシージャに接続されてしまうようなのだ。そのため、メインウィンドウのメッセージプロシージャが重い処理によって応答しなくなった場合、ダイアログの方も応答が止まり上記の症状が発生する。

一般的な解決方法

一般的な解決方法は、重い処理を別スレッド化してしまい、メインのメッセージプロシージャの応答性を確保することである。すなわち、AfxBeginThread()を使ってワーカースレッドを生成し、そのスレッド上で重い処理を実行する。AfxBeginThreadの第1引数はワーカースレッドの関数、第2引数はその関数への引数である。多くの場合、第2引数はワーカースレッドに渡したいオブジェクト、すなわちその関数が属するオブジェクトへのポインタである。そしてメインのウィンドウまたはダイアログでは、適宜ワーカースレッドの状態などを覗いてGUI上の表示を更新する。

Example of creating worker thread

この方法にて注意すべき点は、通常はワーカースレッドからはメイン側のMFC関数を直接呼び出せないことである。例えば、SetWindowText()を直接呼び出すことはできない。これは、ワーカースレッドにはMFCオブジェクト生成時に自動保存されるHWND情報が無いためである。詳しくは以下を参照。

マルチスレッド: MFC プログラミングのヒント | Microsoft Docs

そのため、前述のようにダイアログ側から適宜覗いて表示を更新するか、上記リンクにあるようにPostMessage()等を使ってメイン側ウィンドウに表示の更新を促すメッセージを送る必要がある。なお、上記リンクには FromHandle()を使ってメイン側オブジェクトへのポインタを取得してアクセスすることも書かれているが、これによりアクセスできるのは各MFCオブジェクトであり、MFCから派生したユーザー定義のクラスではないため実用的ではない。

ワーカースレッドに処理を移せない場合の解決方法

「重い処理」の内容によっては、処理をワーカースレッドに移すことができない。たとえば、CTreeCtrlに数万、数十万のノードを追加する処理は非常に重いのでワーカースレッドに移したくなるが、前述の通りワーカースレッドからはメイン側のMFC関数を呼び出せないため移せない。

このような場合は、例えばメインウィンドウとは別のスレッドを生成し、ダイアログのメッセージプロシージャをそちらのスレッドに接続すればよい。このようにすれば、メインウィンドウは重い処理のために応答しなくなるが、状態を表示するダイアログは応答を継続できる。MFCにおいて、メッセージプロシージャを有するスレッドはユーザーインターフェーススレッドと呼ばれ、単純なスレッドであるワーカースレッドとは区別される。ユーザーインターフェーススレッドもAfxBeginThreadで生成できるが、ワーカースレッド生成時とは引数が異なり以下の引数の方を使う。

CWinThread* AfxBeginThread(
  CRuntimeClass* pThreadClass,
  int nPriority = THREAD_PRIORITY_NORMAL,
  UINT nStackSize = 0,
  DWORD dwCreateFlags = 0,
  LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);

使用例は以下の通り。

CStatusDlgThread* pThread = DYNAMIC_DOWNCAST(CStatusDlgThread, AfxBeginThread(RUNTIME_CLASS(CStatusDlgThread)));

ここで、CStatusDlgThreadはCWndThreadクラスの派生であり、ユーザーインターフェーススレッドの実体である。また、DYNAMIC_DOWNCASTは単なるdynamic_castでもよいはずだが、ここではMFCの流儀に則っている。

作成したユーザーインターフェーススレッドにダイアログのメッセージプロシージャを接続するには、CWndThread::m_pMainWndにダイアログへのポインタをセットすればよい。

ダイアログに表示する内容を変更する場合は、メインウィンドウからダイアログに対しユーザー定義のメッセージをSend / PostMessageする。

状態を表示するためのエディットボックスと処理を中止するキャンセルボタンを有するダイアログの実装例を以下に示す。この例では、以下のような構成となっている。

  • CFactorioFactoryCalculatorDlg:メインウィンドウのダイアログ。OnCbnSelchangeComboTarget()が呼び出された際に、AddNode()を呼び出してCTreeCtrl m_CtrlTreeに大量のアイテムを追加する。追加処理中に状態表示ダイアログを表示し、ユーザーに進捗を示す。
  • CStatusDlg:状態表示ダイアログ。ユーザー定義のメッセージ:ID_UPDATE_STATUSを受け取ると、メンバ変数:m_Statusにセットされた文字列をエディットボックスに表示する。
  • CStatusDlgThread:状態表示ダイアログのユーザーインターフェーススレッド。状態表示ダイアログを所有し、InitInstance時にDoModal()を呼び出す。ダイアログが閉じられるとm_Validがfalseとなり、メインウィンドウ側は処理を中止すべき状態になったことを知ることができる。UpdateStatusを呼び出すことでダイアログに文字列を表示できる。

f:id:donadonasan:20200505102718p:plain

実装したダイアログの外観

A dialog with independent User Interface thread (C ...