ぷるぷるの雑記

Unityやシェーダーに関するメモ置き場、たまにCとLinux

C++でハンドラをシンプルに書きたい

C++でデスクトップアプリケーションを書いているとイベントハンドラ(メッセージハンドラ)を書くことになると思います. ボタンをクリックしたら何かを実行するくらいであれば問題ないですが、右クリック中にマウスを動かしたときはAという動き、マウスホイールをクリック中にマウスを動かしたときはBという処理をしたいときにどうすればシンプルに書けるかなーと少し思案していました. 以下フレームワークMFC(という古の技術)を使用していますが、フレームワークに依らない話になります.


最初は次のように実装しました.

/********************** Hoge.h **********************/
class Hoge
{
// Field
private:
    BOOL isRButtonDown=false;
    BOOL isMButtonDown=false;

// Method
private:
    void ShoriA(CPoint pt);
    void ShoriB(CPoint pt);
    DECLARE_MESSAGE_MAP()
    afx_msg void OnMouseMove(UINT nFlags, CPoint pt);
    afx_msg void OnRButtonDown(UINT nFlags, CPoint point);
    afx_msg void OnRButtonUp(UINT nFlags, CPoint point);
    afx_msg void OnMButtonDown( UINT nFlags, CPoint point);
    afx_msg void OnMButtonUp(UINT nFlags, CPoint point);
}

/********************** Hoge.cpp **********************/

BEGIN_MESSAGE_MAP(Hoge, CEdit)
    ON_WM_PAINT()
    ON_WM_MOUSEMOVE()
    ON_WM_RBUTTONDOWN()
    ON_WM_RBUTTONUP()
    ON_WM_MBUTTONDOWN()
    ON_WM_MBUTTONUP()
END_MESSAGE_MAP()


void Hoge::OnPaint() {
// 描画処理
}

void Hoge::OnMouseMove(UINT nFlags, CPoint pt)
{
    if(isRButtonDown){
         ShoriA(pt);
    }else if(isMButtonDown){
         ShoriB(pt);
    }
    
    // 再描画
    OnPaint();
}

void Hoge::ShoriA(CPoint pt)
{
// 右クリック中の処理
}

void Hoge::ShoriB(CPoint pt)
{
// マウスホイールクリック中の処理
}

void Hoge::OnRButtonDown(UINT nFlags, CPoint point)
{
    isRButtonDown=TRUE;
}

void Hoge::OnRButtonUp(UINT nFlags, CPoint point)
{
    isRButtonDown=FALSE;
}

void Hoge::OnMButtonDown(UINT nFlags, CPoint point)
{
    isMButtonDown=TRUE;
}

void Hoge::OnMButtonUp(UINT nFlags, CPoint point)
{
    isMButtonDown=FALSE;
}

これでも動くには動くんですが、OnMouseMove()内にif文が複数あり読みにくいです. さらに条件分岐が増えると、コードが読みにくくなったり条件の追加忘れが生じる恐れがあります.


処理をシンプルにするために、関数ポインタを使った実装方法に変更しました。

/********************** Hoge.h **********************/
class Hoge
{
// Field
private:
void (Hoge::*pFuncOnMouseMove)(CPoint pt) = NULL;

// Method
private:
    void ShoriA(CPoint pt);
    void ShoriB(CPoint pt);
    DECLARE_MESSAGE_MAP()
    afx_msg void OnMouseMove(UINT nFlags, CPoint pt);
    afx_msg void OnRButtonDown(UINT nFlags, CPoint point);
    afx_msg void OnRButtonUp(UINT nFlags, CPoint point);
    afx_msg void OnMButtonDown( UINT nFlags, CPoint point);
    afx_msg void OnMButtonUp(UINT nFlags, CPoint point);
}

/********************** Hoge.cpp **********************/

BEGIN_MESSAGE_MAP(Hoge, CEdit)
    ON_WM_PAINT()
    ON_WM_MOUSEMOVE()
    ON_WM_RBUTTONDOWN()
    ON_WM_RBUTTONUP()
    ON_WM_MBUTTONDOWN()
    ON_WM_MBUTTONUP()
END_MESSAGE_MAP()


void Hoge::OnPaint() {
// 描画処理
}


void Hoge::OnMouseMove(UINT nFlags, CPoint pt)
{
    // NULLチェック
    if (pFuncOnMouseMove) {
                // メンバの関数ポインタの呼び出しにはthisポインタを使う必要がある
        (this->*pFuncOnMouseMove)(pt);
    }

}

void Hoge::ShoriA(CPoint pt)
{
// 右クリック中の処理
}

void Hoge::ShoriB(CPoint pt)
{
// マウスホイールクリック中の処理
}

void Hoge::OnRButtonDown(UINT nFlags, CPoint point)
{
    pFuncOnMouseMove = &Hoge::ShoriA;
}

void Hoge::OnRButtonUp(UINT nFlags, CPoint point)
{
    pFuncOnMouseMove = NULL;
}

void Hoge::OnMButtonDown(UINT nFlags, CPoint point)
{
    pFuncOnMouseMove = &Hoge::ShoriB;
}

void Hoge::OnMButtonUp(UINT nFlags, CPoint point)
{
    pFuncOnMouseMove = NULL;
}

かなりシンプルな実装になったと思います. 関数ポインタのNULLチェックが必要になることとインターフェースを統一する必要があることを考慮してもこちらの実装の方が良い気がします.

MFCで他プロセスのウィンドウにメッセージを送る方法

はじめに

MFCアプリケーションで他プロセスのメインウィンドウにメッセージを送ってみましょう. 


送信用のプログラムではSendMessage()またはPostMessage()を使います.


受信用のプログラムではPreTranslateMessage()を使います. MFCでもWndProcをオーバーライドできるようですが、手続きが大変そうなので今回は扱いません.


最後に、共有メモリを使って詳細なデータをやり取りできるようにします.

環境

項目 バージョン
OS Windows11
Visual Studio 2017 Community Edition (ツールセットv141)
Windows SDK 10.0.17763.0

送信部

  1. SendMsgという名前でMFCアプリケーションプロジェクトを作成
  2. ダイアログのメンバにウィンドウハンドラhWndを定義
  3. ダイアログの初期化時に受信用プロセスのメインウィンドウハンドラを取得
  4. メインダイアログ上にメッセージ送信用のボタンを配置、ID_BUTTONSENDとする.
  5. メインダイアログ内にID_BUTTONSENDのBN_CLICKEDイベントに対するハンドラを定義
  6. ハンドラ内でhWndにSendMessage()する. メッセージはビルトインのものでも、適当な数値でも良い.

変更した箇所は次のようになります.

// SendMsgDlg.h
class CSendMsgDlg : public CDialogEx
{
    // 省略
protected:
    // 追加
    HANDLE hRecv;
    HWND hWnd;
    afx_msg void OnBnClickedButton1();
}

// SendMsgDlg.cpp

BEGIN_MESSAGE_MAP(CSendMsgDlg, CDialogEx)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_BN_CLICKED(IDC_BUTTONSEND, &CSendMsgDlg::OnBnClickedButton1)
END_MESSAGE_MAP()

BOOL CSendMsgDlg::OnInitDialog()
{
        // 省略
    // TODO: 初期化をここに追加します。

        // RecvMsgを起動して起きそのPIDを設定する
    DWORD dwProcId= 11111;

        // dwProcIdをPIDにもつプロセスのプロセスハンドルを取得
        // 取得できなかった場合はプログラム即終了
    hRecv = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcId);
    if (hRecv == NULL) {
        AfxMessageBox(_T("Recv Proc Not Found\n"));
        exit(1);
    }

    // 現在開いているプロセスのメインウインドウを取得
        // そのウインドウハンドラががdwProcIdのPIDのプロセスにアタッチされたものであればそれが求めるべきウィンドウハンドラ
    hWnd = ::GetTopWindow(NULL);
    do {
        if (GetWindowLong(hWnd, GWL_HWNDPARENT) != 0 || !::IsWindowVisible(hWnd))
            continue;
        DWORD ProcessID;
        GetWindowThreadProcessId(hWnd, &ProcessID);
        if (dwProcId == ProcessID) {
            TRACE("--------------------------- PID %d  Found --------------------------- \n",(int)ProcessID);
            break;
        }
    } while ((hWnd = ::GetNextWindow(hWnd, GW_HWNDNEXT)) != NULL);

        // 共有メモリの処理(後ほど)

    return TRUE;  // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}

void CSendMsgDlg::OnBnClickedButton1()
{
    TRACE(" --------------------------- hWnd %p --------------------------- \n",hWnd);
    // TODO: ここにコントロール通知ハンドラー コードを追加します。
    ::SendMessage(hWnd, WM_CLOSE, 0, 0);
 // ::SendMessage(hWnd, WM_USER + 1, 0, 0);
}

::SendMessage()ですが、::(スコープ解決演算子)をつけないとCWnd::SendMessage()が呼び出されてしまいます. CWnd::SendMessage()は引数にウィンドウハンドルをとらず自身にメッセージを送信する関数なので、今回のようにウィンドウハンドルを指定してメッセージを送りたいときには::SendMessage()を使いましょう.

受信部

  1. RecvMsgという名前でMFCアプリケーションプロジェクトを作成
  2. ダイアログでPreTranslateMessage()をオーバーライドする
  3. 独自の応答をさせたいメッセージごとに処理を実装
  4. それ以外のメッセージに対しては基底クラスのPreTranslateMessage()を呼び出す.

変更した箇所は次のようになります.

// RecvMsgDlg.h
class CRecvDlg : public CDialogEx
{
    // 省略
protected:
    virtual BOOL PreTranslateMessage(MSG* pMsg) override;
};

// RecvMsgDlg.cpp
BOOL CRecvMsgDlg::PreTranslateMessage(MSG* pMsg)
{
    if (pMsg->message == WM_CLOSE) {
        AfxMessageBox(_T("WM_CLOSE"));
    }
    else if (pMsg->message == WM_USER + 1) {
        AfxMessageBox(_T("USER DEFINE"));
    }else {
        return CDialogEx::PreTranslateMessage(pMsg);
    }

    return TRUE;
}

以上で、SendMsg側のボタンを押すとRecvMsg側のダイアログが表示されます. なお、親クラスのPreTranslateMessage()を呼び忘れるとダイアログが一切のメッセージを受け取らず、ボタンクリックどころかウインドウを移動させることもできなくなるので注意です.

共有メモリを利用できるようにする

別プロセスのウィンドウにメッセージを送るのは簡単にできますが、このままではデータのやり取りが出来ずかゆいところに手が届きません. 大量のデータを複数のウィンドウで共有するためには以下の方法がまず考えられます.

  1. 同一プロセスで複数のウィンドウを扱う
  2. 共有メモリを利用し異なるプロセスから同じデータにアクセスできるようにする.


今回は後者の共有メモリを利用しましょう. 共有メモリを使う手順は次のようになります.

  1. ファイルマッピングオブジェクトを共有メモリ用に作成 (CreateFileMapping() )
  2. ファイルマッピングオブジェクトへのハンドルを取得(CreateFileMapping() )
  3. プロセスのメモリにマッピング(MapViewOfFile() )

1.と2. を同時に行ってくれるAPI CreateFileMapping() はパラメタが多すぎるのでこちらを参考にさせていただきました.

変更した箇所は次のようになります.

// SendMsgDlg.h
class CSendMsgDlg : public CDialogEx
{
    // 省略
protected:
    // 共有メモリ用に追加
    HANDLE hSharedMem;
        int* pSharedMem;
};


// SendMsgDlg.cpp
BOOL CSendMsgDlg::OnInitDialog()
{
        // 省略
        // 共有メモリの処理
    
        // 共有メモリサイズ: 1024Byte
        DWORD dwSharedMemorySize = 1024;

        // ファイルマッピングオブジェクトを共有メモリ用に作成
        hSharedMem =::CreateFileMapping(
              INVALID_HANDLE_VALUE  // ファイルハンドル( 共有メモリの場合は、0xffffffff(INVALID_HANDLE_VALUE)を指定 )
            , NULL                  // SECURITY_ATTRIBUTES構造体
            , PAGE_READWRITE        // 保護属性( PAGE_READONLY / PAGE_READWRITE / PAGE_WRITECOPY, SEC_COMMIT / SEC_IMAGE / SEC_NOCACHE / SEC_RESERVE )
            , 0                     // ファイルマッピング最大サイズ(HIGH)
            , dwSharedMemorySize    // ファイルマッピング最大サイズ(LOW)
            , L"TR_SHARED_MEM" // 共有メモリ名称
        );

        // ファイルマッピングオブジェクトをプロセスのメモリ空間にマッピング
        // 返り値の型がLPVOIDなので、int型にキャストする
        pSharedMem = (int*)::MapViewOfFile(
        hSharedMem         // ファイルマッピングオブジェクトのハンドル
        , FILE_MAP_WRITE        // アクセスモード( FILE_MAP_WRITE/ FILE_MAP_READ / FILE_MAP_ALL_ACCESS / FILE_MAP_COPY )
        , 0                     // マッピング開始オフセット(LOW)
        , 0                     // マッピング開始オフセット(HIGH)
        , dwSharedMemorySize    // マップ対象のファイルのバイト数
    );

        if ( hSharedMem == NULL ) {
        }
    return TRUE;  // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}

void CSendMsgDlg::OnBnClickedButton1()
{
    TRACE("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& %p\n",hWnd);
    // TODO: ここにコントロール通知ハンドラー コードを追加します。
        pSharedMem[0] = 1;
        pSharedMem[1] = 2;
        pSharedMem[2] = 3;
    //::SendMessage(hWnd, WM_CLOSE, 0, 0);
        ::SendMessage(hWnd, WM_USER + 1, 0, 0);

}

// RecvMsgDlg.h
class CRecvDlg : public CDialogEx
{
    // 省略
protected:
    // 共有メモリ用に追加
    HANDLE hSharedMem;
    int* pSharedMem;
};

// RecvMsgDlg.cpp
BOOL CRecvDlg::OnInitDialog()
{
    // 省略
    // TODO: 初期化をここに追加します。
    // 共有メモリサイズ: 1024Byte
    DWORD dwSharedMemorySize = 1024;

    hSharedMem = ::CreateFileMapping(
        INVALID_HANDLE_VALUE  // ファイルハンドル( 共有メモリの場合は、0xffffffff(INVALID_HANDLE_VALUE)を指定 )
        , NULL                  // SECURITY_ATTRIBUTES構造体
        , PAGE_READWRITE        // 保護属性( PAGE_READONLY / PAGE_READWRITE / PAGE_WRITECOPY, SEC_COMMIT / SEC_IMAGE / SEC_NOCACHE / SEC_RESERVE )
        , 0                     // ファイルマッピング最大サイズ(HIGH)
        , dwSharedMemorySize    // ファイルマッピング最大サイズ(LOW)
        , L"TR_SHARED_MEM" // 共有メモリ名称
    );

    pSharedMem = (int*)::MapViewOfFile(
        hSharedMem         // ファイルマッピングオブジェクトのハンドル
        , FILE_MAP_READ        // アクセスモード( FILE_MAP_WRITE/ FILE_MAP_READ / FILE_MAP_ALL_ACCESS / FILE_MAP_COPY )
        , 0                     // マッピング開始オフセット(LOW)
        , 0                     // マッピング開始オフセット(HIGH)
        , dwSharedMemorySize    // マップ対象のファイルのバイト数
    );

    if (hSharedMem == NULL) {
        AfxMessageBox(_T("Error on Creating Shared Memory"));
        exit(1);
    }

    return TRUE;  // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}

BOOL CRecvDlg::PreTranslateMessage(MSG* pMsg)
{
    if (pMsg->message == WM_USER + 1) {
        for (int i = 0; i < pSharedMem[2]; i++) {
            AfxMessageBox(_T("aaa"));
        }
    }else {
        return CDialogEx::PreTranslateMessage(pMsg);
    }

    return TRUE;
}

SendMsg上のボタンを押すと3回ダイアログが表示されます. これで2つのプロセスで共有メモリを使うことが出来ました. 本来であればWaitForSingleObject()を使った排他操作をすべきですが、今回は情報で流れが一方こうなので割愛しました(という言い訳). また、ダイアログのデストラクタ( あるいはOnDestroy() )では、::UnmapViewOfFile()と::CloseHandle()を忘れずに呼び出しましょう.

参考

chokuto.ifdef.jp

yu-hr.hatenadiary.org

country-programmer.dfkp.info

www.wabiapp.com

プログラミングでは一般的すぎる名前を使わない方が良い

タイトルの通り、プログラミングにおいて一般的すぎる名前をファイル名やクラス名にすると痛い目に合うかもしれません. 自分が経験した例を2つ紹介します.

PHPの例

Windows(xampp)において、index.phpから同ディレクトリ中のtable.phpというファイルをrequireしました.

// フォルダ構成
Project/-------- index.php
            ¦
             --- table.php

// index.php
<?php
   require("table.php");


Apache起動後、このサイトにブラウザでアクセスすると以下のようなエラーメッセージが.

Fatal error: Uncaught Error: Failed opening required 'HTML/Common.php' (include_path='C:\xampp\php\PEAR') in C:\xampp\php\pear\Table.php:68 Stack trace: #0 C:\xampp\htdocs\table\index.php(2): require() #1 {main} thrown in C:\xampp\php\pear\Table.php on line 68


エラーメッセージから察するに、pearフォルダ内にあるTable.phpを参照してしまってますね. どうやらphpのrequireやincludeはライブラリ内のファイルを参照し、そのあとで作業ディレクトリ内を参照するようです.


解決策はいろいろあると思いますが、次の2つが真っ先に思い浮かびました.

  1. 取り込むファイル名を意味が通る範疇で変更する(tbl.phpなど)
  2. requireのパスを./table.php のようにする


普段からパスを意識しろってことですね. ちなみに、phpのインクルードパスはphp.iniに定義されています.

// php.ini一部抜粋

;;;;;;;;;;;;;;;;;;;;;;;;;
; Paths and Directories ;
;;;;;;;;;;;;;;;;;;;;;;;;;

; UNIX: "/path1:/path2"
include_path=C:\xampp\php\PEAR
;
; Windows: "\path1;\path2"
;include_path = ".;c:\php\includes"
;
; PHP's default setting for include_path is ".;/path/to/php/pear"
; https://php.net/include-path

; The root of the PHP pages, used only if nonempty.
; if PHP was not compiled with FORCE_REDIRECT, you SHOULD set doc_root
; if you are running php as a CGI under any web server (other than IIS)
; see documentation for security issues.  The alternate is to use the
; cgi.force_redirect configuration below
; https://php.net/doc-root
doc_root=

; The directory under which PHP opens the script using /~username used only
; if nonempty.
; https://php.net/user-dir
user_dir=

; Directory in which the loadable extensions (modules) reside.
; https://php.net/extension-dir
;extension_dir = "./"
; On windows:
extension_dir="C:\xampp\php\ext"

; Directory where the temporary files should be placed.
; Defaults to the system default (see sys_get_temp_dir)
;sys_temp_dir = "/tmp"

; Whether or not to enable the dl() function.  The dl() function does NOT work
; properly in multithreaded servers, such as IIS or Zeus, and is automatically
; disabled on them.
; https://php.net/enable-dl
enable_dl=Off

; cgi.force_redirect is necessary to provide security running PHP as a CGI under
; most web servers.  Left undefined, PHP turns this on by default.  You can
; turn it off here AT YOUR OWN RISK
; **You CAN safely turn this off for IIS, in fact, you MUST.**
; https://php.net/cgi.force-redirect
;cgi.force_redirect = 1

; if cgi.nph is enabled it will force cgi to always sent Status: 200 with
; every request. PHP's default behavior is to disable this feature.
;cgi.nph = 1

; if cgi.force_redirect is turned on, and you are not running under Apache or Netscape
; (iPlanet) web servers, you MAY need to set an environment variable name that PHP
; will look for to know it is OK to continue execution.  Setting this variable MAY
; cause security issues, KNOW WHAT YOU ARE DOING FIRST.
; https://php.net/cgi.redirect-status-env
;cgi.redirect_status_env =

; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI.  PHP's
; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok
; what PATH_INFO is.  For more information on PATH_INFO, see the cgi specs.  Setting
; this to 1 will cause PHP CGI to fix its paths to conform to the spec.  A setting
; of zero causes PHP to behave as before.  Default is 1.  You should fix your scripts
; to use SCRIPT_FILENAME rather than PATH_TRANSLATED.
; https://php.net/cgi.fix-pathinfo
;cgi.fix_pathinfo=1

; if cgi.discard_path is enabled, the PHP CGI binary can safely be placed outside
; of the web tree and people will not be able to circumvent .htaccess security.
;cgi.discard_path=1

; FastCGI under IIS supports the ability to impersonate
; security tokens of the calling client.  This allows IIS to define the
; security context that the request runs under.  mod_fastcgi under Apache
; does not currently support this feature (03/17/2002)
; Set to 1 if running under IIS.  Default is zero.
; https://php.net/fastcgi.impersonate
;fastcgi.impersonate = 1

; Disable logging through FastCGI connection. PHP's default behavior is to enable
; this feature.
;fastcgi.logging = 0

; cgi.rfc2616_headers configuration option tells PHP what type of headers to
; use when sending HTTP response code. If set to 0, PHP sends Status: header that
; is supported by Apache. When this option is set to 1, PHP will send
; RFC2616 compliant header.
; Default is zero.
; https://php.net/cgi.rfc2616-headers
;cgi.rfc2616_headers = 0

; cgi.check_shebang_line controls whether CGI PHP checks for line starting with #!
; (shebang) at the top of the running script. This line might be needed if the
; script support running both as stand-alone script and via PHP CGI<. PHP in CGI
; mode skips this line and ignores its content if this directive is turned on.
; https://php.net/cgi.check-shebang-line
;cgi.check_shebang_line=1

VC++の例

Visual Studio 2017においてPictureというクラスを自分で定義すると、Pictureというstruct型が既にありビルドが通りませんでした. 「すべての Visual C++ ディレクトリ」において「struct Picture」で検索すると一件だけ引っ掛かりました. どうやらこいつのせいでPictureという型の再定義が生じてしまっていたようです.

すべて検索 "struct picture", サブ フォルダー, 検索結果 1, "すべての Visual C++ ディレクトリ", ""
  C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\include\comdef.h(506):    struct Picture : IPictureDisp {};


これに関してはcomdef.hをインクルードしないというわけにもいかないので、CPictureクラスにリネームすることで事なきを得ました.

まとめ

エラーの内容が唐突ですが、一度この手の罠にはまったことがあれば解決までに時間はかからないと思います. 予約語をすべて覚えることは無理なので、エラーに出くわしたときに対策できれば十分でしょう.

LNK4098 defaultlib 'MSVCRTD' は他のライブラリの使用と競合しています。/NODEFAULTLIB:library を使用してください。の解決方法

Visual Studio 2017で外部ライブラリをNuGetしたプロジェクトでビルドする際に「LNK4098 defaultlib 'MSVCRTD' は他のライブラリの使用と競合しています。/NODEFAULTLIB:library を使用してください。の解決方法」が発生した.


公式リファレンスによれば、互換性のないライブラリとリンクしようとしている場合に生じるエラーらしい. というのもVisual Studio でビルドすると規定のライブラリは自動的にリンクされるが、ビルド時にリンクする外部ライブラリによってはこの規定のライブラリのどれかをリンクしてはいけない組み合わせがあるのだという.


デフォルトでリンクされるライブラリをリンクさせないために、Visual Studioコマンドラインには/NODEFAULTLIBオプションがある. /NODEFAULTLIBオプションを指定すると外部参照を解決するときに検索するライブラリの一覧からすべての規定のライブラリを削除する. libraryパラメータを使用すると、指定した規定ライブラリだけをリンクしないようになる. 設定は簡単でプロジェクト->プロジェクトのプロパティ->リンカ->入力->特定の規定のライブラリを無視 の項目に除外したいライブラリ名を追加するだけ.

特定の規定のライブラリを無視にmsvcrtd.libを追加

参考

learn.microsoft.com

learn.microsoft.com

';'が'*'の前にありませんの解決方法

Visual StudioC++のプロジェクトをビルド中、「構文エラー: ';'が'*'の前にありません。」というエラーが出たときの対処法のメモ.

まずは文法的な間違いがないかを確認. どこかしらで括弧やセミコロンが抜けていればそこを修正すれば解決するかも.

しかし、文法的な誤りがどうしても見つからない場合、もしかしたら 循環参照 が悪さをしているかも. 2つのクラスのヘッダでお互いのクラスのヘッダが必要な状況を循環参照というが、この場合お互いのクラスの型のメンバを持つことが出来ない. ただし、クラスの型へのポインタであればメンバに持つことが出来る.

// 典型的な循環参照

// MyClassA.h
#include "MyClassB.h"
class MyClassA{
private:
          MyClassB* myClassB;
}

// MyClassB.h
#include "MyClassA.h"
class MyClassB{
private:
          MyClassA* myClassA;
}



ここからは半分勘なのだが、どうやら循環参照が生じているとクラス名をうまくクラスだと解釈されないことがある模様. なのでクラス名の前に明示的にclass キーワードをつけるとビルドが通った.

// メンバ変数の宣言時にもclassキーワードをつける

// MyClassA.h
#include "MyClassB.h"
class MyClassA{
private:
          class MyClassB* myClassB;
}

// MyClassB.h
#include "MyClassA.h"
class MyClassB{
private:
          class MyClassA* myClassA;
}



C++は(structと同様)クラス型の変数の宣言にclassキーワードをつけてもつけなくてもよいことになっているが、どうしてもわからないエラーに出くわしたときにとりあえずクラス名の前にclassキーワードをつけておくと解決することがあるので、覚えておいて損はないはず.

参考

marupeke296.com

myoga.web.fc2.com

gl.h included before glew.h の解決方法

Visual Studio 2017 でOpenGLを使ったアプリケーションを作成中 gl.h included before glew.h というエラーが出た. 原因はメッセージのままなのだが、gl.hがどこにあるのかわからない場合具体的な解決方法は次のようになる.

  • gl.hを含んでそうな適当なヘッダをインクルードする
  • インクルードする順番を変えてみる

私の場合glfw3.hとglew.hのインクルードする順番を入れ替えたらエラーが解決した.

// この順番だとエラー
#include "GLFW/glfw3.h"
#include "GL/glew.h"

// 順番を入れ替えると問題ない
#include "GL/glew.h"
#include "GLFW/glfw3.h"

MFCについてのメモ

MFC(Microsoft Foundation Class)についての知見がまとまってきたので、メモを残しておく. 主観や間違いが多分に含まれていると思うので、話半分に見てください.

MFCとは

MFC(Microsoft Foundation Class)とは、WindowsAPIをC++のクラスの形で提供するためのフレームワークである. MFCの先輩であるWindows SDKではWindowsAPIはC言語の関数として提供されていた.*1 Windows SDKではWindowsAPIをC言語の関数として直接*2呼び出していたが、VC++で扱いやすいようにクラス化したものがMFCである.

ぶっちゃけオブジェクト指向でプログラミングできるからWindows SDKより扱いやすいというのは幻想だと思うが、MFCの最たる利点はVisual Studioとの親和性にあると思う. 実際、1つのウィンドウをただ表示するだけならWindows SDKを使った方がソースコードは少なくて済む. しかし、ライブラリの呼び出しやウィンドウを生成するための儀式は冗長で、関数の引数にとりあえずNULLを指定するということもしばしば.

そこでMFC、もといVisual Studioの出番というわけである. Visual Studioのウィザードを通してプロジェクトを生成すればスケルトンプログラムを用意してもらえるので、コードを一から組み立てるというより、コードを追加していけばそれなりのGUIアプリケーションを作成することが出来る.ではWindows APIを全く知らなくてもMFCを扱えるかというとむしろ逆で、むしろMFCの方がOS特有の罠に陥りやすいのではないかとも思う.

例えばCClientクラスはコンストラクタ内でGetDC関数を実行し、デストラクタ内でReleaseDC関数を実行する. この2つの関数の出自はもちろんWindows APIである. CClientクラスの生成と破棄時にどのようなことが行われているかは秘匿化され、リファレンスを読むかプログラムの実行中に問題が生じない限りは気にも留めないだろう. MFCプログラムの実行中に問題が生じたとき、それを解決するためには結局のところWindows APIの知識が必要になる. 結局のところMFCWindows API(SDK) の薄いラッパークラスライブラリに過ぎないのである.

さらにMFCをややこしくしているのは、クラスライブラリを謳っておきながら結構なグローバル関数、グローバル変数を含んでいることである. MFCアプリケーションのprintfことAfxMessageBox関数などが良い例だと思う. まあこれはMFCというよりC++/VC++の特徴なのかもしれない.

などとMFCをディスってしまったが、なんやかんや良いフレームワークだと思う. 例えば先ほど泥をかぶってもらったAfxMessageBox関数だが、デフォルト引数を持っているので必須の引数は1個だけである. あれ、やっぱりMFCというよりC++/VC++の仕様じゃん.

参考

ja.wikipedia.org

MFCとイベントドリブン

MFCが得意とするのはいわゆるイベント駆動プログラムである. 数値計算のようなプログラムはソースコードの書かれている順番がそのままプログラムの実行順になる. つまり、上から順に実行され、一番下まで来るとプログラムが終了してしまう. しかし、電卓のようなGUIアプリケーションがこのような仕様だと、電卓のイコールを押して計算し終えるとアプリケーションが終了してしまい、結果を確認することがほぼ不可能になってしまう.そこでイベント駆動プログラムの出番である. イベント駆動プログラムでは初期化を終えるとメッセージループに入り、メッセージを受信したときにメッセージの種類に応じた関数(イベントハンドラ)を実行し、その処理が終えたら再びメッセージループに入るという動作をする.

メッセージというのは具体的にはMSG構造体(winuser.h)のインスタンスのことで、ユーザーがマウスを動かしたりキーボード入力をするたびに、これらのデバイスは入力をMSG構造体のインスタンスに変換し、システムメッセージキューにプッシュしていく. キューからメッセージをポップした後、MSG構造体に含まれる宛先ウィンドウを調べ、適切なウィンドウにメッセージを送る. ここまではWindowsが自動的に行ってくれるので、アプリケーションを作るプログラマーはメッセージを受信したときのメッセージごとのハンドラをプログラムすればよい.

メッセージはシステム側で用意されているものと、ユーザーが定義できるものがあるがいずれも実態はUINT型の数値である. 前者をシステムメッセージとでも呼ぶと、システムメッセージのうちWM_という接頭辞で始まるメッセージに対するハンドラはすでに名前が決まっている(CWndクラス内で定義されている). 有名どころだと次のようなシステムメッセージとハンドラの組がある.

メッセージ名 HEX 意味 ハンドラ名
WM_SIZE 0x0005 ウィンドウサイズが変更されたとき OnSize
WM_PAINT 0x000F ウィンドウに無効領域が発生したとき(再描画が必要な時) OnPaint
WM_CHAR 0x0102 キーボード入力したとき OnChar

システムメッセージは基本的には受信することだけ考えていればいいが、任意のタイミングで任意のウィンドウに送信することもできる. 例えば、ウィンドウの右上の×ボタンを押すとアプリケーションは自分自身にWM_CLOSEメッセージを発信する.

また、他プロセスのプロセスハンドルを取得すれば、自プロセス以外のウィンドウにメッセージを送ることも可能である.

参考

wiki.winehq.org

chokuto.ifdef.jp

MFC命名規則

ハンガリアン記法なので変数名から型が推定できる.

接頭辞
b ブーリアン
h ハンドル
p, lp ポインタ
n DWORD

型の接頭辞でLやWがついているときがあるが、16-bit のMS-DOSの時の名残らしく現在はLもWも32bitを表しているのであまり気にしなくてよい.

メソッドはパスカルケースで、クラスのメンバーにはm_という接頭辞がついている

変数 意味
CWnd::m_hWnd HWND CWndインスタンスにアタッチされているウィンドウのハンドル
CWinThread::m_hThread HANDLE 現在のスレッドへのハンドル
CWinThread::m_pMainWnd CWnd* アプリケーションのメインウィンドウへのポインター
CWinApp::m_hInstance HINSTANCE アプリケーションの現在のインスタンス

HWND(ウィンドウハンドル)やHINSTANCE(インスタンスハンドル)は結局のところすべてHANDLE型のtypedefとして定義されていて、HANDLE型自体はvoid型へのポインタとして定義されているらしい. MFC命名規則とは関係ないけど結構重要.

説明
HWND ウィンドウハンドル(メッセージの送り先などで利用)
HHOOK フックハンドル(イベントをアプリケーションがインターセプトできる)
HEVENT イベントハンドル(WaitForSingleObject等で利用)
HDC バイスコンテキスト(描画等で利用)
HGLRC OpenGLレンダリングコンテキスト(デバイスコンテキストとはまた別物)
HINSTANCE インスタンスハンドル(アプリケーションやDLLの指定に利用)

参考

learn.microsoft.com

kaitei.net

www.inasoft.org

www.wdic.org

MSG構造体とメッセージ

イベントドリブン型アプリケーションのプログラムは、メッセージの通知とハンドラを追加していくのが主な作業となる. メッセージの実体であるMSG構造体はリファレンスによると以下のように定義されている.

typedef struct tagMSG {
  HWND   hwnd;
  UINT   message;
  WPARAM wParam;
  LPARAM lParam;
  DWORD  time;
  POINT  pt;
  DWORD  lPrivate;
} MSG, *PMSG, *NPMSG, *LPMSG;

だが、winuser.hを見てみると以下のように定義されている.

/*
 * Message structure
 */
typedef struct tagMSG {
    HWND        hwnd;
    UINT        message;
    WPARAM      wParam;
    LPARAM      lParam;
    DWORD       time;
    POINT       pt;
#ifdef _MAC
    DWORD       lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
メンバ名 説明
hwnd HWND メッセージを受信するウィンドウへのハンドル. メッセージがスレッドメッセージの場合NULL
message UINT WM_XXなどのメッセージ
wParam WPARAM メッセージについての追加情報.
lParam LPARAM メッセージについての追加情報
time DWORD メッセージが投稿された時刻
pt POINT メッセージが投稿された時のカーソル位置
lPrivate DWORD リファレンスに記述無し

調べたところによると_MACmac製品のためのdefineらしく、Windowsでは有効になってない模様. 実際Visual Studio上ではlPrivateメンバにアクセスできなかった. 公式リファレンスにもlPrivateの詳しいことは書いてないので、Windows環境ではないものと考えてよい.

WindowsではlPrivateメンバは有効になってない

Windows上でマウスを動かしたりキーボードを打ったりウィンドウを移動したり縮小したり....、といろいろなタイミングでこのMSG構造体がやり取りされている.

これらのメッセージはシステムのみならず、アプリケーションから送ることもできる.そのためにはSendMessage()またはPostMessage関数を利用する. 両者の違いはメッセージ送信元にコントロールが戻るタイミングが違うだけなので、SendMessage関数の使い方を見ていく. リファレンスには次のように書かれている.

LRESULT SendMessage(
  HWND   hWnd,
  UINT   Msg,
  WPARAM wParam,
  LPARAM lParam
);

MSG構造体の一部を引数にとることが分かる. hWndをHWND_BROADCAST((HWND)0xffff)にするとシステム内のすべてのメインウィンドウに送信される(ブロードキャスト). wParamとlParamの使い方はMsgによって異なるので、その都度調べるのが良いと思う.

受信側はWndproc関数を使う. リファレンスによる定義はこちら.

LRESULT Wndproc(
  HWND unnamedParam1,
  UINT unnamedParam2,
  WPARAM unnamedParam3,
  LPARAM unnamedParam4
)

各引数の意味はSendMessage関数と同じようなので省略. Wndproc関数内でswitch~case文を用いて届いたメッセージごとの処理を実装する.

参考

learn.microsoft.com

stackoverflow.com

MFCで重要なクラス

MFCで特に重要なCWndクラス、CDialogクラス、CWinAppクラス、は押さえておきたい.

CWndクラス

CWndクラスは名前の通りウィンドウオブジェクトを表すためのオブジェクトクラスである. ダイアログ、ボタン、ピクチャーコントロールなど目に見えるものはすべてこのクラスから派生している. ここで疑問なのが、(Windowsでは)目に見えるもの=ウィンドウオブジェクトとうことらしい. ウィンドウオブジェクトといわゆるウィンドウとはまた少し違う概念のようで、いわゆるウィンドウのことはフレームウィンドウというらしい. CClientDCクラスのコンストラクタにCWnd型へのポインタを渡したり、CWndクラスでOnPaint関数が定義されていることからも、CWnd型=目に見えるものという考えでよいのだと思う.

CWndクラスには多くのメッセージハンドラが実装されているが、メッセージハンドラのように特定のタイミングで呼び出される関数も定義されている. 両者の違いはプロトタイプ宣言でafx_msgというキーワードがついているかで区別することが出来る. afx_msgキーワードがついている関数はメッセージマップに対応するメッセージを登録しなければならない. リファレンスによればafx_msgキーワードはvirtualキーワードの効果を示唆しているが実際には仮想関数ではないらしい. いずれにせよオーバーライドのようなことが出来る.

プロトタイプ宣言 呼ばれるタイミング メッセージマップへの登録
afx_msg void OnPaint() ウィンドウの一部を再描画する要求が投稿されたとき 必要
virtual void PreSubclassWindow() ウィンドウがサブクラス化される前 不要

CDialogクラス

CWndクラスを継承したダイアログボックスを表すクラス. CWndはウィンドウオブジェクト全般を表すようだが、CDialogクラスはダイアログ、いわゆる普通のウィンドウを表すために使用される. ダイアログにモーダルとモードレスの2種類がある. モーダルはダイアログボックスを閉じるまでアプリケーションが停止するが、モードレスダイアログはダイアログボックスを開いてもアプリケーションが停止することがない.

モーダルダイアログを開くときは次のようにする.

CDialog dlg;
INT_PTR nResponse = dlg.DoModal();
// これより下にはモーダルを削除しないと進まない

モードレスダイアログを開くときは次のようにする

CDialog dlg;
// CWnd* pParentWndは省略可
dlg.Create(IDD_XX, pParentWnd);
dlg.ShowWindow(SW_SHOW);

基本的には親子関係を持たせるために親となる作成元のCWnd型へのポインタを渡してダイアログを作成するが、必須ではない模様. 継承の概念が分かっていたら言うまでもないが、CWnd型へのポインタを渡さなければならない場合、CWnd型を継承した型へのポインタを渡しても問題ない.

CWinApp

アプリケーションそのものを表すクラス. おそらく一つのプロジェクトに一つしかない. mfcライブラリによってインスタンス化される. 語弊を恐れずに言うと、MFCアプリケーションにはエントリポイントが存在しない. 正確に言えば、MFCアプリケーションとはmfcvxx.dll (or .lib)をリンクしたアプリケーションで、このライブラリの中にエントリポイントが含まれているらしい.まあでもC言語のアプリケーションもライブラリにあるスタートアップルーチンをリンクしてビルドしているので、MFCの場合スタートアップルーチンの中にt_mainないしmain関数まで定義されていると考えておけば十分な気がする. mfcライブラリ内のメイン関数にはCWinAppをインスタンス化し、InitInstance関数を呼び出すという処理がすでに実装されている. したがってMFCアプリケーションの実質的なエントリポイントはCWinApp::InitInstance関数である.

参考

www.bnote.net

ダイアログエディターの使い方

ダイアログの編集はダイアログエディターを利用してグラフィカルに行う*3.

ダイアログをダブルクリックするとダイアログエディターが開く

ツールボックスからコントロールドラッグ&ドロップ

コントロールには必ず一意のIDをつけなければならない. このIDはリソースエディタだけでなくソースファイルを含めたプロジェクト全体で共有される. コントロールを追加するとResource.hにそのコントロールのIDに対応した数値を自動でdefineしてくれる. ダイアログエディターから追加したコントロールは1000から順に数値が割り振られる様子.

// Resource.h

// プロジェクト生成時から定義されている
#define IDR_MAINFRAME                   128
#define IDM_ABOUTBOX                    0x0010
#define IDD_ABOUTBOX                    100
#define IDS_ABOUTBOX                    101
#define IDD_HATENATEST_DIALOG               102

// ダイアログエディタから自動追加
#define IDC_BUTTON1                     1000


// プロジェクト生成時から定義されている
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS

#define _APS_NEXT_RESOURCE_VALUE    129
#define _APS_NEXT_CONTROL_VALUE     1000
#define _APS_NEXT_SYMED_VALUE       101
#define _APS_NEXT_COMMAND_VALUE     32771
#endif
#endif

不思議なことにIDOK(1として#defineされている)とIDCANCEL(2として#defineされている)のdefineの定義がResource.hにされていないが、どこかで別途定義されているらしい.

再びダイアログエディタに戻り、ダイアログ上を右クリックするといくつかの項目が表示されている. この中でもよく使う3つを取り上げる. その3つの項目とはすなわち次である.

イベントハンドラーの追加はコントロールを右クリックしないと表示されないので注意. ウィザードではイベントハンドラを紐づけるダイアログのクラス、コントロールがダイアログに発信するメッセージの種類、イベントハンドラ名を決めることが出来る. イベントハンドラを追加すると、以下の3点が自動的に追加される.

逆に言うと、イベントハンドラを消去したい場合は上記の3点を削除すればよい.

クラスの追加だが、なぜかコントロールではなくダイアログのクラスの追加しかできない. コントロール独自のクラスを実装したい場合は、リソースエディタからではなくソリューションエクスプローラー->右クリック->追加->クラス からウィザードを使ってソースファイルを生成したのちに変数の追加を行うとよい.

変数の追加も通常コントロール上で右クリックすれば表示される. ウィザードでは変数として追加するコントロールのID、変数の型、変数名を指定することが出来る. 変数の型は通常いじらなくてもよいが、自作したクラスの型として定義したい場合は自分で選択する必要がある.

また、WMから始まるメッセージのハンドラをダイアログに追加したい場合は、ダイアログのプロパティ->メッセージを編集すればよい.

WMから始まるメッセージのハンドラはプロパティから追加できる

あるあるなのだが、リソースエディタでコントロールを追加したのにIDが定義されていないといわれることがある. その時はだいたいResource.hをincludeし忘れている.

参考

www.bnote.net

まとめ

ネタがたまったらより詳細なMFCに関する記事を書いていきたい

MFC、思ってたよりも奥が深い. でもやっぱり肝となるのはWindowsAPIだなと感じた.

*1:ここでいうWindows SDKVisual Studio 6.0 などに同梱されていたいわゆる古いSDKを指す. 文脈によっては.NETと統合された新しいSDKのことを指すかもしれない..

*2:実体はkernel.dll(or .lib)やuser.dll(or .lib)などのライブラリ

*3:.rc というファイルにはコントロールの位置などの情報がテキスト形式で書かれているので、位置などを微調整したいときはこのファイルを直接いじるのもあり?