Windows API - CUI プログラミング編 第4回 〜C++ ストリームクラス〜

エディットコントロール出力ストリームクラス

#include <iostream>
std::ostream;

C++ における、出力ストリームクラスの基底クラスが、std::ostream であるため、 これを継承したクラスのオブジェクトは、std::ostream& 引数に渡すことが出来ます。 これを利用して、エディットコントロールに出力するクラスを設計してみます。

ただし、第1回で書いたように、Windows においては、Unicode が使われているため、 この変換作業を行うバッファクラスを別に設計する必要が出てきます。

Unicode 変換バッファクラスの実装

バッファクラスの基底クラスは、

std::stringbuf;

であり、実は、std::stringstream のバッファクラスでもあります。これを継承して、

class CSeditOutputBuffer : public std::stringbuf{
protected:
 HWND& hEditWnd;
 virtual int sync(void);
public:
 CSeditOutputBuffer(HWND& rEditWnd);
};

というクラスを宣言します。hEditWnd はもちろん、前回のエディットコントロールの ウィンドウハンドルになり、これを、コンストラクタで初期化します。

CSeditOutputBuffer::CSeditOutput(HWND& rEditWnd):
 hEditWnd(rEditWnd){
}

そして、肝心の Unicode への変換作業を行うのが、sync() 仮想メソッドとなります。 protected: virtual となっているように、基底クラスである std::stringbuf において、 オーバーライドされることが想定されたもので、このメソッドは、 例えば、std::endl や std::flush などのマニピュレータが流されると呼び出されます。

とりあえず、中身を最初に全部書いておくと、

int CSeditOutputBuffer::sync(void){
 int n=std::stringbuf::sync();
 int wstrLength=MultiByteToWideChar(
  CP_ACP, 0, str().c_str(), -1, NULL, 0
 );
 LPWSTR wstrBuf=new WCHAR[wstrLength+1];
 wstrLength=MultiByteToWideChar(
  CP_ACP, 0, str().c_str(), str().length(),
  wstrBuf, wstrLength+1
 );
 int length=GetWindowTextLength(hEditWnd);
 SendMessage(hEditWnd,EM_SETSEL,length,length);
 SendMessage(hEditWnd,EM_REPLACESEL,1,(LPARAM)wstrBuf);
 delete [] wstrBuf;
 str("");		
 return n;
}

となります。流れを簡単に言えば、まず、Unicode に変換するためのバッファを 作ります。MultiByteToWideChar() API 関数の第6引数を 0 とすることで、 必要なバッファの長さが取得できるのを利用します。

この後は、実際に変換を行います。ここで、std::stringbuf::str() メソッドを 何度か使っていますが、機能は、std::stringstream::str() と全く同じで、 引数がない場合はバッファを参照し、引数を指定した場合はバッファを指定文字列で 初期化します。

変換が終われば、後は、EM_SETSEL でカーソルを末尾に動かした後に、 EM_REPLACESEL でこの位置を Unicode 文字列 buf で置き換えます。

最後にバッファを解放して、str(""); とすることで新たな文字列が ストリームに流せるよう準備しておきます。

出力ストリームクラスの実装

ここまで準備が終われば、あとは、実際の出力ストリームクラスを実装するだけとなります。

class CSeditOutputStream : public std::ostream{
 CSeditOutputBuffer pBuf;
 HWND hWnd;
public:
 CSeditOutputStream(void);
 BOOL mInitWnd(
  HWND hParentWnd, int X, int Y, int nWidth, int nHeight
 ); 
 operator HWND&(void);
};

コンストラクタは、std::ostream と pBuf の初期化を行います。

CSeditOutputStream::CSeditOutputStream(void):
 pBuf(hWnd), std::ostream(&pBuf){
}

std::ostream のコンストラクタは、引数としてこのように、std::stringbuf* を 受けることで、バッファを指定することが出来ます。

あとは、CreateWindow() に相当するものを、mInitWnd() メソッドとして実装します。

BOOL CSeditOutputStream::mInitWnd(
 HWND hParentWnd, int X, int Y, int nWidth, int nHeight){
 hWnd=CreateWindow(
  TEXT("EDIT"), TEXT(""),
  WS_CHILD | WS_HSCROLL | WS_VSCROLL | ES_READONLY |
  ES_LEFT | ES_MULTILINE | WS_VISIBLE | WS_BORDER,
  X, Y, nWidth, nHeight, hParentWnd, NULL,
  (HINSTANCE)GetWindowLong(hParentWnd,GWL_HINSTANCE),NULL
 );
 if(!hWnd) return FALSE;
 else return TRUE;
);

ちなみに、operator HWND&(void); はそのまま return hWnd; としておくことで、 キャスト可能にしておきます。

CSeditOutputStream オブジェクト

長い道のりでしたが、これで、

LRESULT CALLBACK WndProc(
 HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
 static CSeditOutputStream editOut;
 switch(uMsg){
 case WM_CREATE:
  editOut.mInitWnd(hWnd,X,Y,nWidth,nHeight); 
 ...
 }
 return 0;
}

とすることで、editOut という CSeditOutputStream オブジェクトが使えるように なります。

これまで書いてきたように、std::ostream の継承となるので、

editOut<<"String\r\n"<<std::flush;

などと流すことで、エディットコントロールに追記されます。もちろん、

friend std::ostream& operator<<(
 std::ostream& output, const C& tObj
);

が定義される全てのクラス C のオブジェクトを流すことが出来ます。

また、operator HWND&(); によって、同時にウィンドウハンドルとしても振る舞います。

エディットコントロール入力ストリームクラス

要領は、出力ストリームクラスと同様に、std::istream の継承クラスを 作ればいいだけなのですが、今回は、std::istringstream というクラスを 継承することで、バッファも標準のものをそのまま使います。

入力ストリームクラスの実装

class CSeditInputStream : public std::istringstream{
 HWND hWnd;
public:
 BOOL mInitWnd(
  HWND hParentWnd, int X, int Y, int nWidth, int nHeight
 ); 
 int mSetBuf(void);
 operator HWND&(void);
};

ほとんど CSeditOutputStream と同じですが、コンストラクタもバッファもなく、 代わりに mSetBuf(void); というメソッドが追加されます。

まず、この mSetBuf(); ですが、CSeditOutputBuffer::sync() と同じような 役割を持ちます。ただし、こちらは sync() のように自動的に呼び出されることはなく、 入力ストリームオペレータ >> を用いる前に手動で呼び出す必要があります。

int CSeditInputStream::mSetBuf(void){
 clear();
 int wstrLength=GetWindowTextLength(hWnd);
 if(wstrLength>0){
  LPWSTR wstrBuf=new WCHAR[wstrLength+1];
  wstrLength=GetWindowText(hWnd,wstrBuf,wstrLength+1);
  int strLength=WideCharToMultiByte(
   CP_ACP,0,wstrBuf,wstrLength,NULL,0,NULL,NULL
  );
  LPSTR strBuf=new char[strLength];
  WideCharToMultiByte(
   CP_ACP,0,wstrBuf,wstrLength,strBuf,strLength,NULL,NULL
  );
  std::string tempStr(strBuf,strLength);
  int retAt=std::string::npos;
  while((retAt=tempStr.find_first_of("\r\n"))!=std::string::npos)
  tempStr.at(retAt)=' ';
  str(tempStr);
  delete [] strBuf;
  delete [] wstrBuf;
  SetWindowText(hWnd, TEXT(""));
 }
 return wstrLength;
} 

最初の、std::istringstream::clear() メソッドですが、ストリームのバッファの中身が 空になるなどの理由でストリームのエラーフラグが立っていると、 以降の操作が出来ないので、必ずここでバッファを全てクリアします。

GetWindowText() API 関数を用いてエディットコントロールから Unicode 文字列を 取得した後は、WideCharToMultiByte() API 関数を用いてこれを C 文字列に変換します。

この後のごちゃごちゃした作業ですが、一言で言えば、改行文字を空白文字に置き換えます。 ES_MULTILINE を指定しなければ、別に必要のない作業です。

最後に、std::istringstream::str(); メソッドを使ってストリームのバッファに 文字列をセットしておきます。

mInitWnd() などの残りのメソッドは出力の場合と同じです。 ただし、少なくとも ES_READONLY は外さないと エディットコントロールに書き込めません。

CSeditInputStream オブジェクト

CSeditOutputStream オブジェクトと使い方は同じです。

mInitWnd() メソッドで初期化してしまえば、後は、

std::string str;
editIn.mSetBuf();
editIn>>str;

などとすることが出来ます。当然、std::string ではなく、

friend std::istream& operator>>(
 std::istream& input, C& tObj
);

が定義される全てのクラス C のオブジェクトに流し込むことが出来ます。

CstdWnd 基底クラス

これまで実装してきたクラスを見ると分かるように、

class CstdWnd{
protected:
 HWND hWnd;
public:
 BOOL mInitWnd(
  HWND hParentWnd, int X, int Y, int nWidth, int nHeight
 );
 operator HWND&(void);
};

というクラスの中で宣言されるようなプロパティやメソッドが現れます。

そこで、改めてこのような基底クラスを定義しておき、先程の2つのクラスを、 この CstdWnd クラスとの多重継承で定義しておくと、 多少、記述が楽になります。

このクラスを継承すると、例えば、第1回で書いたような親ウィンドウの生成、 ウィンドウプロシージャの処理を行うようなクラスも作ることが出来ます。

これまで定義してきた、CSeditOutputStream や CSeditInputStream をはじめとした、 全ての子ウィンドウを、このクラスのプロパティとすることで、 各オブジェクトを包括的に管理するオブジェクトとして利用します。


文章作成 : yukki-ts (-+-twilight serenade-+- [stage])