メンバー関数をスレッド関数に

スレッドを起こす古いプログラムをC++化しようとして、ちょっと躓いてしまいました。備忘録のメモです。

Windowsで、スレッドを起こすAPIは CreateThread、もしくは通常はCRTライブラリの_beginthreadexを利用しますが・・・、これらのAPIに渡す、スレッド関数(スレッドの開始アドレス)の呼び出し規約は、__cdeclもしくは__stdcallです。要はグローバル関数(自由関数)なわけですが、C++化する上で、できるだけグローバル関数を減らしたいと思いました。

プログラム自体をC++化する上で、単純にクラス内に静的な(staticな)メンバを追加してこれをスレッド関数にして、新しいスレッドに渡すパラメータにオブジェクトのインスタンスポインタを渡せば、最小の変更でうまく行きます。

が、もっと簡単に、、オブジェクト・インスタンスのメンバ関数をスレッド関数にしたい場合は、ちょっと工夫がいるようです。
というわけで、上記のようなプロキシ的な静的メンバ関数を設置して、インスタンスメンバー関数をスレッドに渡すようにしたコードを書いてみました。

/*
  SimpleThreading.hpp
  簡単なスレッドクラスです。継承させて使用します。
 
  追記(2010/4/10):致命的な欠陥があるので実際には使わないように。参考程度に見てください。
       メンバー関数ポインタとパラメータを CSimpleThreadingのメンバ変数にいったん保存して
       スレッド関数(ThreadStartProxyから参照している部分がスレッドアウトになってます(笑)
	
  class CMyObject : public CSimpleThreading<CMyObject>
   {
	   ...
   };
*/
#include <windows.h>
#include <process.h>

template<typename T>
class CSimpleThreading
{
  typedef UINT_PTR (T::*MEM_FUN)(LPVOID);

  LPVOID m_pvParameter;
  MEM_FUN m_ThreadStart;

  static UINT_PTR __stdcall ThreadStartProxy(LPVOID pvSimpleThreading)
    {
      CSimpleThreading *pSimpleThreading = (CSimpleThreading*)pvSimpleThreading;

     //修正しました。(10/4/16)
      return ((T*)pSimpleThreading->*(pSimpleThreading->m_ThreadStart))(pSimpleThreading->m_pvParameter);
    }

protected:
  //引数:メンバ関数ポインタ、スレッド処理に渡すパラメータ、サスペンド
  HANDLE BeginThreadEx(MEM_FUN ThreadStart,LPVOID pvParam,BOOL bSuspended = FALSE)
    {
      if(!ThreadStart)
        return NULL;

      m_pvParameter = pvParam;
      m_ThreadStart = ThreadStart;

      return (HANDLE)_beginthreadex(NULL,
                                    0,
                                    CSimpleThreading::ThreadStartProxy,
                                    (LPVOID)this,
                                    bSuspended ? CREATE_SUSPENDED : 0,
                                    NULL);
    }
};

このコードの要旨は、静的メンバ関数を_beginthreadex()APIに渡し、その中で実際にスレッドに処理させたいメンバー関数をコールする、というごく一般的なものです。
CSimpleThread::BeginThreadExで、メンバ関数ポインタとパラメータをプロパティとして保存しています。あとは、静的メンバー関数内で、そのメンバー関数ポインタをパラメータを引数としてコールしています。
メンバー関数ポインタをコールするには、どのインスタンスのメンバー関数をコールするのか、といった情報が必要なので、必ずpObject->*や、Object.*といったものが必要です。メンバー関数内でコールする場合も、this->*は省略できないようです。

実際には、上記クラスを継承させて使用します。

/*
  ThreadTest.cpp
  使用例
*/
#include "SimpleThreading.hpp"

class CMain : public CSimpleThreading<CMain>
{
public:
  
  UINT_PTR ThreadStart(LPVOID pvParam)
    {
      //スレッド処理...
      return 0;
    }
  
	DWORD Run()
    {
      DWORD dwWait = 0;
      HANDLE hThread = BeginThreadEx(&CMain::ThreadStart,NULL);

      if(hThread == NULL)
        return 0;

      return WaitForSingleObject(hThread,INFINITE);
    }
};

実際にはプロキシ的に静的メンバ関数をかましているので、メンバー関数をスレッド関数にしているわけではありませんが、ちょこっとしたプログラムにはこれで十分です(^^

エアロのガラス効果

Windows Vistaに全く興味がなかったわけでもないですが、メインのPCをWindows7にしたことだし・・・ということで、ほとんど何の知識もないWindows Aeroですが、ちょっと勉強がてらいじってみました。

Aeroの最大の特徴は、やはり、ウィンドウフレームにも適用されているガラス効果でしょう。各ウィンドウとガラス効果などのAero効果をデスクトップ上のウィンドウとして表示できるように合成・管理しているのが、DWM(Desktop Window Manager)ということだそうです。

クライアント向けのWindowsで、NT4からXPまでのウィンドウマネージャなどの大部分はカーネルモードで動作していたと思います。これは純粋にパフォーマンス向上のためと思いますが、Vista以降ではドライバモデルが変更され、iその一部がユーザーモードで動作するようになりました。本来のNTの思想に戻ったといえます。ビデオカードをはじめとするハードウェア自体の性能向上のおかげでしょうね・・・。

このDWM、タスクマネージャで確認するとVistaと7では、明らかにメモリ使用量が違っています。詳しくは知りませんが、なんらかの改良が施されているんだと思います。

で、DWMに関連するAPIでネットを検索しても、SDKプログラマの興味がないのか、関心がないのか、C/C++ WindowsSDKで利用できるまとまった情報(コード)が少ないです。使ってみました、という記事はいっぱい出るんですが、詳細な記事が見つからず。MSDNのAPI説明見てもいまいち分からないし・・・。

まぁ、だけど利用できるAPIは限られていて、DWMに関連するAPIで最も簡単に利用できるのが、DwmExtendFrameIntoClientAreaというAPIです。
これは、ウィンドウフレームに適用されているガラス効果をクライアントエリアに拡張するAPIなんですが、使い方は至って簡単です。ガラス効果をどれだけクライアント側に食い込ませるかをMARGINS構造体にセットして、それをウィンドウハンドルとともに渡せばOKです。

//{左,右,上,下}という風になります。
//下記例だと、上部20ピクセルが拡張されます。
MARGINS margins = {0,0,20,0};
DwmExtendFrameIntoClientArea(hWnd,&margins);

//ちなみに、以下のように-1をセットすると、クライアント領域すべてがガラス効果の影響受けます。
MARGINS margins = {-1};

※dwmapi.hのインクルードと、dwmapi.libのリンクが必要です。

MARGINS margins={-1};としたときのガラス効果

このようにAPI自体はカンタンに利用できる反面、C/C++とWindowsSDKのみで組んで行くには相当めんどくさい処理が必要になります。

  1. 単純にボタンやエディットなどのコントロールを配置すると、悲惨なことになる。
  2. ガラス効果上に黒い文字を描画すると、黒い文字が透明になっちゃう(^^;;;
  3. ガラス効果にしたクライアント領域部分は、別途常に黒く塗りつぶさないといけない。
    (要するに、拡張した部分の背景色の再描画処理を別途記述しなければならないかと・・・)
  4. DWMのデスクトップコンポジションが有効か無効かを判別してそれぞれに対する処理を行う必要がある。

などなど・・・

ガラス効果上に配置した黒く塗りつぶしたGDIアイテムは、全部透明になってしまいます(テキストとかメニューの文字とか)。要するに、色値が0x00000000(要するに黒色)の部分がガラス効果として透明なるっちゅーわけだから、当たり前と言えば当たり前ですが・・・。COLORREF型の先頭1バイトを255にすればいいのか?よくわかんないです。
これの解決方法としては、コントロールをオーナードローで描画するか、GDI+ライブラリを使用する、という選択肢がMSDNで紹介されていました。が、子コントロールの色を全部修正するなんて、メンドーだし、他の解決方法があるのかもしれません。
そのうちガラス効果関連処理をラップしたC/C++のクラスライブラリがそのうち出るかもしれないし、でないかもしれないし、すでに出ているのかも・・・。C#の情報は結構あるんですけどね・・・。

ダイアログをリソースファイルで定義してDialogBox() APIで表示させているようなプログラムの場合、最低限以下のようなウィンドウメッセージを捕まえて背景を別途処理しないといけない。

・WM_INITDIALOG
デスクトップコンポジションが有効なら、DwmExtendFrameIntoClientAreaを実行。

・WM_ERASEBKGND
ガラス効果を適用しても、ダイアログの背景用描画ブラシで上書きされてしまうので、ガラス効果を拡張した部分は黒色で、それ以外はダイアログの背景色で背景を塗りつぶす。

・WM_DWMCOMPOSITIONCHANGED
デスクトップコンポジションが有効になったり、無効になったりすると、飛んでくるので、これを捕まえて適切な処理を行う。
注意しなければならないのは、デスクトップコンポジションが有効になったらその都度、DwmExtendFrameIntoClientAreaをコールする必要があること。

INT_PTR CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
  //上部20ピクセルを拡張
  MARGINS margins = {0,0,20,0};
  RECT rect = {0};
  BOOL fDwmEnabled = FALSE;
  HBRUSH hBrush = NULL;
  LONG oldBottom = 0;

  switch(message)
  {
  case WM_INITDIALOG:
    DwmIsCompositionEnabled(&fDwmEnabled);
    if(fDwmEnabled)
      DwmExtendFrameIntoClientArea(hDlg,&margins);
    return (INT_PTR)TRUE;

  case WM_ERASEBKGND:
    DwmIsCompositionEnabled(&fDwmEnabled);
    GetClipBox((HDC)wParam,&rect);

    //デスクトップコンポジションの有効・無効によって背景に使用するブラシを決定
    hBrush = fDwmEnabled ? (HBRUSH)GetStockObject(BLACK_BRUSH) : GetSysColorBrush(COLOR_BTNFACE);

    //クライアント領域の上部20ピクセル(ガラス効果)の背景を描画
    oldBottom = rect.bottom;
    rect.bottom = rect.top + 20;
    FillRect((HDC)wParam,&rect,hBrush);
    
    //クライアント領域の残りの下部の背景を描画
    hBrush = GetSysColorBrush(COLOR_BTNFACE);
    rect.top += 20;
    rect.bottom = oldBottom;
    FillRect((HDC)wParam,&rect,hBrush);

    return (INT_PTR)TRUE;

  case WM_DWMCOMPOSITIONCHANGED:
    DwmIsCompositionEnabled(&fDwmEnabled);
    if(fDwmEnabled)
      DwmExtendFrameIntoClientArea(hDlg,&margins);
    return (INT_PTR)0;

  case WM_COMMAND:
      //ダイアログ上のコントロールへの応答処理など
      if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
        {
          EndDialog(hDlg, LOWORD(wParam));
          return (INT_PTR)TRUE;
        }
      break;
  }
  return (INT_PTR)FALSE;
}

ということで、なんとかかんとか、思ったように表示できました・・・。

ガラス効果2

なんか、久しぶりにGUIなAPI触ると、何がどうだったか・・・忘却の彼方へ・・・(ーー;

使用中止にすべきか否か

今、家の前にあった某企業の社宅、いわゆる団地がどんどん解体されて更地になりつつあります。なんでも高層階の独身寮を新たに建設するんだそうです。見慣れた団地風景がどんどん壊されて家から見える景色がどんどん変わっていきます。親が某造船所の工員だった関係で、僕は生まれたときから団地育ち。団地と言っても今風のマンション風なものじゃなく、昭和40年代に大量に建てられた、4階建ての、いかにも「団地」というもの。どんどん街並みが変わっていくのもなんか寂しいような気もします。

そんなことはさておき・・・

OSを64ビット化したことのメリットもありますが、やはりデメリットも大きいものがあります。

xkeymacsというキーボード入力をフックしてキーバインドを強制的に変更するソフトを使っていたのですが、これがもう使い物になりません。具体的には、エクスプローラなどの64ビットアプリ上でのIME動作がかなりメチャクチャです。

しかし、32bitアプリだと正常動作するので、ブラウザなどの主要なネットツールなどは問題ないのでいいんですが・・・。

xkeymacsが64bit/32bitでも正常に動作してくれるようにバージョンアップしてくれれば一番いいのですが、ソフトウェアの性質上、難しいようです。ほかにもキーバインドを変更するソフトがあるにはあるんですが、設定が難しかったり、xkeymacsのようなお手軽さはないです。

OSそのものに、ユーザーインターフェイスの細かいところを簡単に変更できるような仕組みにしておいて欲しいなぁ~、とも思います。ま、やれペン入力だ、マルチタッチだ、とそれは結構なことなんですが・・・、やはり文字入力の簡便さにおいて、キーボードの右に出るものは現状、無いと思います。

Notepadは相変わらず使えない

Windows 7になって、標準でついてくるアクセサリがちょっと変わりましたね。とくにペイント(mspaint.exe)はマイクロソフトが流行らそうと躍起になっている(笑)リボンUI(Office 2007とおなじUI)を搭載してちょっと趣が違ってます。電卓(calc.exe)もなかなか使えそうです。

変わったアクセサリもあるけど、全く何も変わってないアクセサリもあります。ノートパッド(notepad.exe)です。ノートパッドは、エディタとしても使えず、かといってテキストビューア的に使おうとしても、認識できる文字コードが少なすぎて、ビューアにもできず・・・テキストコントロールとメニューをウィンドウに貼り付けただけの、まるでサンプルプログラムにしか思えません。

で、このノートパッド、前々から文字コードの認識が悪くて、たまに・・・

こんな状態になったりします。。。

別にこの内容は特殊な文字コードを使っている訳じゃなく、単に半角の数字と空白が交互に並ぶだけのもの・・・。

普段使用しているエディタで表示すると・・・

プログラムで使用する乱数を発生させるために書いた、単なるテストコードの出力(下、参照)なんですが・・・改行を入れなかったのが悪いのかなと思って改行を入れてみたりしたんですが、変わらず文字化け・・・。

#include <iostream>
#include <ctime>
#include <boost/random.hpp>
#include <boost/lexical_cast.hpp>

int main(int argc,char **argv)
{
  int count = 0;
  boost::mt19937 rand(std::time(0));

  if(argc < 2)
    return 0;

  try
    {
      count = boost::lexical_cast<int>(argv[1]);
    }
  catch(std::bad_cast e)
    {
      std::cout << "please input number..." << std::endl;
      return 0;
    }

  for(int i=0;i<count;i++)
    std::cout << rand() % 6 + 1 << ' ';

  return 0;
}

単にboostライブラリで1~6(サイコロ)の乱数を発生させているだけ。(しかし・・・STLやBoostを使うと異様にファイルサイズが大きくなるな・・・)

ん~・・・・まぁ、どうせノートパッドなんて使わないから別にいいや(結論は結局それ(^^;;;

美人時計

最近知りました、bijin-tokei ~ http://www.bijint.com/~
一分ごとにかわるがわる美人が時間を教えてくれるなんて、いいアイデアですねぇ~。

iPod Touch版買おうかな。。。これのWindows用 デスクトップガジェット、出して欲しいす(^^;
美男子が好みの方は、美男時計もありますが、僕は要りません(笑)

それはさておき、 続きを読む