C++
VisualStudio
MFC
dll

Visual Studio 2017 Visual C++ による MFC DLL の開発 (失敗例含む)

はじめに

DLL を作成する場合、MFC を利用すると、Win32 API を使う DLL より簡単になることが多いです。ここでは、Visual C++ で MFC を利用した DLL の開発について簡単に説明します。

最近では、この機能を使う人も少ないようで、Visual Studio 2017 の動作もおかしいところがありました。(こちらの問題かもしれませんが)

プロジェクトの作成

MFC DLL プロジェクトを作成するには、「新しいプロジェクト」ダイアログで "MFC" を選択し、一覧から "MFC DLL" を選んで OK をクリックします。

VCProj_MFCDLL.png

下のようなダイアログが開くので、必要なオプションを設定し、OK をクリックします。

VCProj_MFCDLL2.png

ソリューションエクスプローラに次のようなプロジェクトが追加されます。

MFCDLL_Solution.png

(注意) このプロジェクトツリーで Source.cpp/h は、後から追加したものです。

次のソースは上のプロジェクトツリーの MFCLibrary2.cpp の内容です。ここでは、DLL の初期化を行います。

このファイルはたいていの場合、編集の必要はありません。

// MFCLibrary2.cpp : DLL の初期化ルーチンを定義します。
//

#include "stdafx.h"
#include "MFCLibrary2.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

//
//TODO: この DLL が MFC DLL に対して動的にリンクされる場合、
//      MFC 内で呼び出されるこの DLL からエクスポートされたどの関数も
//      関数の最初に追加される AFX_MANAGE_STATE マクロを
//      持たなければなりません。
//
//      例:
//
//      extern "C" BOOL PASCAL EXPORT ExportedFunction()
//      {
//          AFX_MANAGE_STATE(AfxGetStaticModuleState());
//          // 通常関数の本体はこの位置にあります
//      }
//
//      このマクロが各関数に含まれていること、MFC 内の
//      どの呼び出しより優先することは非常に重要です。
//      it は、次の範囲内で最初のステートメントとして表示されるべきです
//      らないことを意味します、コンストラクターが MFC
//      DLL 内への呼び出しを行う可能性があるので、オブ
//      ジェクト変数の宣言よりも前でなければなりません。
//
//      詳細については MFC テクニカル ノート 33 および
//      58 を参照してください。
//

// CMFCLibrary2App

BEGIN_MESSAGE_MAP(CMFCLibrary2App, CWinApp)
END_MESSAGE_MAP()


// CMFCLibrary2App の構築

CMFCLibrary2App::CMFCLibrary2App()
{
    // TODO: この位置に構築用コードを追加してください。
    // ここに InitInstance 中の重要な初期化処理をすべて記述してください。
}


// 唯一の CMFCLibrary2App オブジェクト

CMFCLibrary2App theApp;


// CMFCLibrary2App の初期化

BOOL CMFCLibrary2App::InitInstance()
{
    CWinApp::InitInstance();

    return TRUE;
}

サンプル (フレームワークの機能を利用しない)

次のサンプルは、DLL で現在の日時を提供する関数を作り、それらを呼び出して、MFC デスクトップ・アプリケーション (テストプログラム) で今日の日付を表示するものです。

サンプル(1) ではフレームワークの機能を利用せず、エクスポート関数を従来通りの方法で実装してみます。

ソリューションエクスプローラでプロジェクトを選択して「追加」コンテキストメニューから C++ ソースとヘッダーファイルを追加します。ここでは、デフォルトのままの Source.cpp / Source.h としています。

プロジェクトでは、MFC ヘッダーやライブラリのリンクが設定されているので、ソース内で MFC の機能を利用できます。

C++ ヘッダー

#pragma once

CTime Now();
int Day(CTime) ;
int Month(CTime) ;
int Year(CTime) ;

C++ ソース
#include "stdafx.h"

// 現在の日時を得る。
CTime Now()
{
    return CTime::GetCurrentTime();
}

// 日時オブジェクトから日を得る。
int Day(CTime t)
{
    return t.GetDay();
}

// 日時オブジェクトから月を得る。
int Month(CTime t)
{
    return t.GetMonth();
}

// 日時オブジェクトから年を得る。
int Year(CTime t)
{
    return t.GetYear();
}

モジュール定義ファイル

; MFCLibrary2.def : DLL のモジュール パラメーターを宣言します。

LIBRARY MFCLibrary2

EXPORTS
    Now @1
    Day @2
    Month @3
    Year @4

テストプログラム

テストプログラムは、下のイメージのように今日の日付を表示するだけです。DLL の公開関数をコールして、年、月、日を取得し表示します。

このテストプログラムのプロジェクトのプロパティを開き、リンカー/入力で「追加の依存ファイル」に DLL のインポートライブラリ (この例では MFCLibrary2.lib) を追加しておく必要があります。このファイルは、DLL プロジェクトを含むソリューションフォルダの Debug (または Release) フォルダ内にできているはずです。

テストプログラムのソースを下に示します。日付表示を行っているのは、一番最後の OnShowWindow ハンドラの中です。(OnInitDialog の中でも可能なはずですが、ウィンドウオブジェクトができた後の方が確実かと思ってそうしています)

もしかしたら、Visual Studio 2017 のバグかと思いますが、リソースをビジュアル編集すると不要な日本語の文字列がリソースに挿入されて、エラーになるケースがあります。リソースファイルの後の方に変な文字列が含まれていたら削除します。

// TestMFCApp1Dlg.cpp : 実装ファイル
//

#include "stdafx.h"
#include "TestMFCApp1.h"
#include "TestMFCApp1Dlg.h"
#include "afxdialogex.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif
CTime Now();
int Day(CTime);
int Month(CTime);
int Year(CTime);

// アプリケーションのバージョン情報に使われる CAboutDlg ダイアログ

class CAboutDlg : public CDialogEx
{
public:
    CAboutDlg();

// ダイアログ データ
#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_ABOUTBOX };
#endif

    protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV サポート

// 実装
protected:
    DECLARE_MESSAGE_MAP()
};

CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX)
{
}

void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
END_MESSAGE_MAP()

// CTestMFCApp1Dlg ダイアログ

CTestMFCApp1Dlg::CTestMFCApp1Dlg(CWnd* pParent /*=NULL*/)
    : CDialog(IDD_TESTMFCAPP1_DIALOG, pParent)
{
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CTestMFCApp1Dlg::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CTestMFCApp1Dlg, CDialog)
    ON_WM_SYSCOMMAND()
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_WM_CREATE()
    ON_WM_SHOWWINDOW()
END_MESSAGE_MAP()

// CTestMFCApp1Dlg メッセージ ハンドラー

BOOL CTestMFCApp1Dlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    // "バージョン情報..." メニューをシステム メニューに追加します。

    // IDM_ABOUTBOX は、システム コマンドの範囲内になければなりません。
    ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
    ASSERT(IDM_ABOUTBOX < 0xF000);

    CMenu* pSysMenu = GetSystemMenu(FALSE);
    if (pSysMenu != NULL)
    {
        BOOL bNameValid;
        CString strAboutMenu;
        bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
        ASSERT(bNameValid);
        if (!strAboutMenu.IsEmpty())
        {
            pSysMenu->AppendMenu(MF_SEPARATOR);
            pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
        }
    }

    // このダイアログのアイコンを設定します。アプリケーションのメイン ウィンドウがダイアログでない場合、
    //  Framework は、この設定を自動的に行います。
    SetIcon(m_hIcon, TRUE);         // 大きいアイコンの設定
    SetIcon(m_hIcon, FALSE);        // 小さいアイコンの設定

    // TODO: 初期化をここに追加します。

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

void CTestMFCApp1Dlg::OnSysCommand(UINT nID, LPARAM lParam)
{
    if ((nID & 0xFFF0) == IDM_ABOUTBOX)
    {
        CAboutDlg dlgAbout;
        dlgAbout.DoModal();
    }
    else
    {
        CDialog::OnSysCommand(nID, lParam);
    }
}

// ダイアログに最小化ボタンを追加する場合、アイコンを描画するための
//  下のコードが必要です。ドキュメント/ビュー モデルを使う MFC アプリケーションの場合、
//  これは、Framework によって自動的に設定されます。

void CTestMFCApp1Dlg::OnPaint()
{
    if (IsIconic())
    {
        CPaintDC dc(this); // 描画のデバイス コンテキスト

        SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

        // クライアントの四角形領域内の中央
        int cxIcon = GetSystemMetrics(SM_CXICON);
        int cyIcon = GetSystemMetrics(SM_CYICON);
        CRect rect;
        GetClientRect(&rect);
        int x = (rect.Width() - cxIcon + 1) / 2;
        int y = (rect.Height() - cyIcon + 1) / 2;

        // アイコンの描画
        dc.DrawIcon(x, y, m_hIcon);
    }
    else
    {
        CDialog::OnPaint();
    }
}

// ユーザーが最小化したウィンドウをドラッグしているときに表示するカーソルを取得するために、
//  システムがこの関数を呼び出します。
HCURSOR CTestMFCApp1Dlg::OnQueryDragIcon()
{
    return static_cast<HCURSOR>(m_hIcon);
}

int CTestMFCApp1Dlg::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CDialog::OnCreate(lpCreateStruct) == -1)
        return -1;

    // TODO: ここに特定な作成コードを追加してください。
    return 0;
}

void CTestMFCApp1Dlg::OnShowWindow(BOOL bShow, UINT nStatus)
{
    CDialog::OnShowWindow(bShow, nStatus);

    // TODO: ここにメッセージ ハンドラー コードを追加します。
    WCHAR buff[20];
    CTime tm = Now();
    int day = Day(tm);
    int month = Month(tm);
    int year = Year(tm);
    wsprintf(buff, L"今日は %04d-%02d-%02d です。", year, month, day);
    SetDlgItemText(IDC_STATIC1, buff);
    Invalidate();
    UpdateWindow();
}

サンプル (フレームワークの機能を利用する)

この方法は結局、うまくいきませんでした。

下のソース(ヘッダーファイル)のように CWinApp から派生した CMFCLibrary3App (これは、プロジェクトを作成したとき自動でプロジェクトに挿入される) クラスにパブリックなメソッド GetTimeString() を定義する。

class CMFCLibrary3App : public CWinApp
{
public:
    CMFCLibrary3App();

    // 日時文字列を得る。
    CString GetTimeString();

// オーバーライド
public:
    virtual BOOL InitInstance();

    DECLARE_MESSAGE_MAP()
};

GetTimeString() の実装は C++ ソースファイルで下のように行います。

//
//  クラスのメソッド: 現在の日時文字列を取得する。
//  =============================================
CString CMFCLibrary3App::GetTimeString()
{
    char buff[64];
    SYSTEMTIME sysTime;
    GetLocalTime(&sysTime);

    sprintf_s(buff, "%04d/%02d/%02d %02d:%02d%02d",
        sysTime.wYear,
        sysTime.wMonth,
        sysTime.wDay,
        sysTime.wHour,
        sysTime.wMinute,
        sysTime.wSecond);
    CString sTime(buff);

    return sTime;
}

// 唯一の CMFCLibrary3App オブジェクト
CMFCLibrary3App theApp;

C++ ソースファイルに「唯一の CMFCLibrary3App オブジェクト」というコメントと共に theApp というオブジェクトがあるので、これをエクスポートします。変数を直接エクスポートすることはできないので、公開関数 GetDllApp() というのを作ってエクスポートします。

この関数は、エクスポートするので、モジュール定義ファイルに含めておく必要があります。

CMFCLibrary3App* WINAPI GetDllApp()
{
    return &theApp;
}

テストプログラム側 (DLL を使用する側) では、この関数をインポートできるように プロジェクトのプロパティでリンカーの設定を行い、ソースの中で import 属性を付けてプロトタイプ宣言します。

この関数を呼び出して、theApp を取得し、次のように CMFCLibrary3App.GetTimeString() をコールします。

この方法だと、コンパイルは通るもののリンカーエラーになってしまいます。つまり、GetTimeString が見つからないと怒られます。

// DLL からの関数
extern "C" __declspec(dllimport) WINAPI GetDllApp();
...

CMFCLibrary3App* pDll = GetDllApp();
CString str = pDll->GetTimeString();

それではと、CMFCLibrary3App 側で GetTimeString を呼び出して日時文字列を取得して、結果だけを返すエクスポート関数 GetNow (下記) を定義して日時文字列を取得する方法を取ってみました。

extern "C" __declspec(dllimport) void WINAPI GetNow(LPWSTR, int);
この方法だと、ビルドエラーはなく実行すると確かに日時文字列が取得できるのですが、どこかのメモリを壊してしまうようで、この関数実行後に実行エラーになってしまいました。

次のソースは DLL 側のヘッダーファイルと C++ ソースファイルです。

// MFCLibrary3.h : MFCLibrary3 DLL のメイン ヘッダー ファイル
//

#pragma once

#ifndef __AFXWIN_H__
    #error "PCH に対してこのファイルをインクルードする前に 'stdafx.h' をインクルードしてください"
#endif

#include "resource.h"       // メイン シンボル

// CMFCLibrary3App
// このクラスの実装に関しては MFCLibrary3.cpp をご覧ください
//

class CMFCLibrary3App : public CWinApp
{
public:
    CMFCLibrary3App();

    // 日時文字列を得る。
    CString GetTimeString();

// オーバーライド
public:
    virtual BOOL InitInstance();

    DECLARE_MESSAGE_MAP()
};
// MFCLibrary3.cpp : DLL の初期化ルーチンを定義します。
//

#include "stdafx.h"
#include "MFCLibrary3.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

//
//TODO: この DLL が MFC DLL に対して動的にリンクされる場合、
//      MFC 内で呼び出されるこの DLL からエクスポートされたどの関数も
//      関数の最初に追加される AFX_MANAGE_STATE マクロを
//      持たなければなりません。
//
//      例:
//
//      extern "C" BOOL PASCAL EXPORT ExportedFunction()
//      {
//          AFX_MANAGE_STATE(AfxGetStaticModuleState());
//          // 通常関数の本体はこの位置にあります
//      }
//
//      このマクロが各関数に含まれていること、MFC 内の
//      どの呼び出しより優先することは非常に重要です。
//      it は、次の範囲内で最初のステートメントとして表示されるべきです
//      らないことを意味します、コンストラクターが MFC
//      DLL 内への呼び出しを行う可能性があるので、オブ
//      ジェクト変数の宣言よりも前でなければなりません。
//
//      詳細については MFC テクニカル ノート 33 および
//      58 を参照してください。
//

// CMFCLibrary3App

BEGIN_MESSAGE_MAP(CMFCLibrary3App, CWinApp)
END_MESSAGE_MAP()


// CMFCLibrary3App の構築

CMFCLibrary3App::CMFCLibrary3App()
{
    // TODO: この位置に構築用コードを追加してください。
    // ここに InitInstance 中の重要な初期化処理をすべて記述してください。
}

extern "C" void WINAPI GetNow(LPWSTR str, int len);

//
//  クラスのメソッド: 現在の日時文字列を取得する。
//  =============================================
CString CMFCLibrary3App::GetTimeString()
{
    char buff[64];
    SYSTEMTIME sysTime;
    GetLocalTime(&sysTime);

    sprintf_s(buff, "%04d/%02d/%02d %02d:%02d%02d",
        sysTime.wYear,
        sysTime.wMonth,
        sysTime.wDay,
        sysTime.wHour,
        sysTime.wMinute,
        sysTime.wSecond);
    CString sTime(buff);

    return sTime;
}

// 唯一の CMFCLibrary3App オブジェクト
CMFCLibrary3App theApp;

// CMFCLibrary3App の初期化

BOOL CMFCLibrary3App::InitInstance()
{
    CWinApp::InitInstance();

    return TRUE;
}

//
//  現在の日時を得る。
//  ====================
void WINAPI GetNow(LPWSTR str, int len)
{
    CString now = theApp.GetTimeString();

    if (len > now.GetLength())
    {
        wcscpy_s(str, len, now.GetBuffer());
    }
    else
    {
        *str = '\0';
    }

    return;
}

テストプログラム側は下のようになっています。(ウィザードが吐き出したソースのうち、修正箇所のみ)

BOOL CTestMFCApp3Dlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    // "バージョン情報..." メニューをシステム メニューに追加します。

    // IDM_ABOUTBOX は、システム コマンドの範囲内になければなりません。
    ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
    ASSERT(IDM_ABOUTBOX < 0xF000);

    CMenu* pSysMenu = GetSystemMenu(FALSE);
    if (pSysMenu != NULL)
    {
        BOOL bNameValid;
        CString strAboutMenu;
        bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
        ASSERT(bNameValid);
        if (!strAboutMenu.IsEmpty())
        {
            pSysMenu->AppendMenu(MF_SEPARATOR);
            pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
        }
    }

    // このダイアログのアイコンを設定します。アプリケーションのメイン ウィンドウがダイアログでない場合、
    //  Framework は、この設定を自動的に行います。
    SetIcon(m_hIcon, TRUE);         // 大きいアイコンの設定
    SetIcon(m_hIcon, FALSE);        // 小さいアイコンの設定

    // TODO: 初期化をここに追加します。

    // DLL から日時文字列を得る。
    WCHAR buff[100];
    buff[0] = '\0';
    GetNow(buff, _countof(buff));
    SetDlgItemText(IDC_STATIC, buff);

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

実行例
TestMFCApp3.png

-