MASATOの開発日記


前の開発日記 次の開発日記 一覧

2002/02/24

ソフトウェアは、オプション設定という名前のコマンドがあり、 そこには様々なオプションがあって、それを色々と変更することができる。 というのは良くある話です。このオプション設定が、一番最初にどのような設定がなされているか、 すなわちデフォルト設定というのは非常に重要だと思います。
私は、起動した段階でほとんどの人がもっとも使いやすいようにオプションが設定されていて、 それでもまだ自分好みに設定を変えたい人のためにオプション設定コマンドを 用意しておく、という形を目指しています。(難しいですけれど)

ある2つのUI(ユーザーインターフェース)があり、 ユーザの好みが半々に分かれるようならば、UIをもっと改良し、 好みが8:2に分かれるくらいまでのものを作った上、 8割の人が好むUIをデフォルト設定にし、2割の人の為に設定を変更でいるようにしたいですね。
そんなわけで、最近はオプションで設定できるからいいや、という考えは避けるようにしています。

モデルビューコントローラとドキュメントビュー

MVC(モデルビューコントローラ)というアーキテクチャパターンがあります。 これと、MFCのドキュメント-ビュー アーキテクチャとどのような関連があるのか考えてみました。

MVCは、モデルとビューとコントローラーの3つから成り立っているのに対し、 MFCは、ドキュメントとビューとアプリケーションの3つから成り立っています。
アプリケーションクラスは、実際には全体の管理を行うだけですので、 MVCのモデルとビューとコントローラーをMFCではドキュメントとビューが担当しているわけです。

MVCのモデルはMFCのドキュメントが担当しています。 MVCのビューはそのままMFCのビューが担当しています。 そして、MVCのコントロールは、一般的にMFCのビューが主に担当しているようです。 つまり、MFCのビューは、データの表示と共に、ユーザ入力の処理まで担当しているわけです。 設計によってはMFCのドキュメントをMVCのコントロールとすることもできるでしょう。 MFCのドキュメントとビュー、どちらにコントロールの機能を持たせるにしろ、 どちらかに集中して持たせるべきでしょう。コントロールの機能を分散させてしまうと 色々とメンテナンス上の問題がありそうです。

また、こうしたMVCパターンを使う上で注意することは、ビューはデータを 持たないようにすることだと思います。必要なデータは毎回モデルから入手する ようにします。以前ビューにデータを保持させた上、モデルと同期を取ろうとし て酷く複雑なコードを書いてしまいました。
なお、このように表示のたびにビューがモデルにアクセスするようにすると、 ビューはモデルに深く依存するようになります。 そうなると、ビューだけ他のアプリケーションで再利用しようとしてもまず無理です。 (その代わりモデルの再利用は容易になります。) しかしビューだけ再利用ということはあまり無いような気がしますので、 許容できるリスクなのではないかなと思います。

クラスにメッセージを送る方法(1)

環境Visual C++ 6.0

今回は、クラスのインスタンスにメッセージを送る方法について説明します。 このメッセージは、いわゆるウィンドウメッセージのことです。 SendMessageは単純にメンバ関数を呼ぶのと同様ですが、 PostMessageはメンバ関数を呼ぶだけでは簡単には実現できません。 そして、異なるスレッドからPostMessageするのと同様のことは、メンバ関数では実現不能です。

しかし、PostMessageというのは、ウィンドウ、あるいはスレッドにしか送ることができません。 ただのクラスにPostMessageする手段が無いのです。

特に、スレッド間のやり取りにおいては、メッセージのPostと関数呼び出しはまったく異なります。 メッセージのPost時の処理は、そのメッセージを受け取ったスレッドで行われ、 関数呼び出し時の処理は、呼び出したスレッドで行われます。 メッセージをPostして処理することにより、危険領域(クリティカルセクション)を減らし、 ソフトウェアの動作を単純にして安定させることができます。
もちろん処理をPost先のスレッドで行うことによる欠点もありますので、 どんな場合もPostの方が良いというわけではありません。 しかし、Postの方が良い場合というのは確実にあります。

というわけで、ただのクラスにPostMessageをするにはどうしたら良いのか、と考えてみました。 基本的な考えはそれほど難しいものでもなく、 見えないウィンドウを保持しておき、そこにメッセージを送るという方法です。

CAsyncSocketはこの手法を利用しているようです。 CAsyncSocketのコードを参考に、次のようなクラスを考えてみました。

クラスは2つ、CWndProcDeliverとCWndProcReceiverからなります。 CWndProcDeliverクラスはCWndクラスから派生し、実際にウィンドウメッセージを 受け取るクラスで、CWndProcReceiverは、CWndProcDeliverが受け取ったメッセージを 流してもらうためのインターフェースです。
クラスAにウィンドウメッセージを受け取る機能を持たせようと思った場合は、 CWndProcReceiverを継承して(必要に応じて多重継承して)クラスAを定義し、 クラスAのメンバ変数としてCWndProcDeliverを保持する必要があります。 CWndProcReceiverは、javaでいうInterfaceのように扱います。

CWndProcDeliverのヘッダファイルは大体このようになります。 クラスウィザードからCWndの派生クラスを作成し、それにいくつか関数を 追加する形になります。

class CWndProcReceiver;
class CWndProcDeliver : public CWnd
{
public:
  CWndProcDeliver(CWndProcReceiver* receiver);
  virtual ~CWndProcDeliver();
  BOOL Create();
protected:
  virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);
  CWndProcReceiver* m_lpReceiver;
};

WindowProcはクラスウィザードから追加すると楽です。

CWndProcReceiverのソースファイルは次のようになります。

CWndProcDeliver::CWndProcDeliver(CWndProcReceiver* receiver)
: m_lpReceiver(receiver)
{
  Create();
}

CWndProcDeliver::~CWndProcDeliver()
{
  DestroyWindow();
}

BOOL CWndProcDeliver::Create()
{
  return CreateEx(0, AfxRegisterWndClass(0), _T(""),
    WS_OVERLAPPED, 0, 0, 0, 0, NULL, NULL);
}

LRESULT CWndProcDeliver::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
  LRESULT result;
  if (m_lpReceiver){
    if (m_lpReceiver->OnWndMsg(this, message, wParam, lParam, &result)){
      return result;
    }
  }
  return CWnd::WindowProc(message, wParam, lParam);
}

OnWndMsgは、CWndのメンバ関数と同じ形にしました。 独自の形にしても良かったのですが、他のライブラリと見た目同じにしておくと 分かりやすいと思ったからです。

CWndProcReceiverクラスの説明はまた次回。

前の開発日記 次の開発日記 一覧