Visual Studio 2017 Visual C++ による MFC デスクトップアプリ開発 (SDI)

#はじめに
MFC とは Micorosft Foundation Class の略で、.NET Framework 登場前は Windows アプリ開発の主役でした。現在では MFC を使っての新規開発はもうないでしょうが、古いアプリを流用したり、保守したりということは時々あるかと思います。

しかし、.NET での開発が主流となって、MFC を扱える人もどんどん減っていると思われます。ここでは、Visual Studio 2017 を使って MFC によるデスクトップアプリ開発はこんな感じという程度の内容ですが、紹介したいと思います。

前に [Visual Studio 2017 Visual C++ による MFC デスクトップアプリ開発](https://qiita.com/tadnakam/items/13738da833801f297d9b) という投稿を行ったのですが、これは「ダイアログベース」のアプリを例にしていました。これだと、MFC デスクトップアプリでよく使用される DOC/VIEW モデルを使用していないので、いかにも MFC デスクトップアプリ 紹介記事としては足りないかと思い、この投稿記事を作りました。

ここでは、SDI (Single Document Interface) 形式のアプリを構築する方法を紹介します。この SDI という用語ですが、 Visual Studio 2017 Update2 以前では使用されていましたが、最近では死語となったのか、Update3以降は使用されなくなりました。

このように Visual Studio 2017 Update3 の前後で操作画面が大幅に変わっているので注意が必要です。(普通、マイナーアップデートで、そこまでやらないでしょう！)


(注意) MFC 開発機能はデフォルトでインストールされていません。[Visual Studio インストーラでインストールする](https://qiita.com/tadnakam/items/042c7a19bf115e3b096f)必要があります。


#DOC/VIEW モデルとは
DOC/VIEW モデルとは、見ての通りドキュメント(データ) と 画面(ビュー) を分けてアプリケーションを構築しましょうという考え方で特別なものではありません。

MFC ではドキュメントの基本クラスとして CDocument 、ビューの基本クラスとして CView というのが用意されていて、これらのクラスからアプリケーション独自のドキュメントとビュークラスを派生させてプログラムを構築します。

この方法でアプリケーションを構築すると、基本クラスに含まれるメソッドを利用できるのでコーディング量が減りますが、CDocument と CView を理解していないとプログラミングができないので、初心者が軽くコーディングしてアプリを作ろうという訳にはいかないです。

#プロジェクトの作成
まず、「新しいプロジェクト」で「MFC アプリケーション」を選択し、OK ボタンをクリックします。

![NewProject.png](https://qiita-image-store.s3.amazonaws.com/0/62802/7d267203-6c9f-1e5b-76f6-a5d0b9fff4fc.png)



「アプリケーションの種類」ダイアログが表示されたら、「アプリケーションの種類」を「シングルドキュメント」にします。(その他は必要な場合のみ変更)

![SdiOption.png](https://qiita-image-store.s3.amazonaws.com/0/62802/c1db2ef9-1517-c100-a8de-059beae44955.png)



「次へ」ボタンをクリックすると、別のダイアログに切り替わります。次のダイアログボックスが表示されるまで、必要な場合のみ項目の変更を行います。

「高度な機能オプション」ダイアログが開いたら、「高度なフレームオプション」のチェックをすべて外して「完了」ボタンをクリックします。

![SdiHOption.png](https://qiita-image-store.s3.amazonaws.com/0/62802/87c9aafc-ddc3-511c-da27-902ca96f4db0.png)



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

![uploading-0]()![SolutionExplorer2.png](https://qiita-image-store.s3.amazonaws.com/0/62802/f6a8e331-f9f8-d438-48d5-e423e52ccdc9.png)



#サンプルプログラム
次のサンプルプログラムは、線画データを読み込んで図形を描画します。起動すると下のような画面が表示されます。

![SdiSample1.png](https://qiita-image-store.s3.amazonaws.com/0/62802/3547d606-7944-e9b1-cd44-f3136fea5961.png)



「ファイルを開く」ダイアログで線画データを読み込むと、データに従って図形を画面に描画します。(図形は一筆書きで可能なもののみ)

![SdiSample1A.png](https://qiita-image-store.s3.amazonaws.com/0/62802/812572a8-2008-dad1-5ccb-786d4b35a90a.png)



##ドキュメント
ドキュメントのヘッダーファイルですが、「属性」のところに、図形情報を追加しました。さらに「操作」のところに、図形の各点を取得するメソッドを追加しました。

###ドキュメントクラスのヘッダー

```C++
// MFCApplication2Doc.h : CMFCApplication2Doc クラスのインターフェイス
//
#pragma once

class CMFCApplication2Doc : public CDocument
{
protected: // シリアル化からのみ作成します。
	CMFCApplication2Doc();
	DECLARE_DYNCREATE(CMFCApplication2Doc)

// 属性
public:
	INT m_nPoints;	// 点の数
	CSize m_sizeDoc;  // 最大サイズ
	INT m_getptr;     // 次の点(番号)
	CPoint m_points[1024];	// 図形の点

// 操作
public:
	CPoint * GetNext();	// 次の点を得る。
	CPoint* GetFirst();	// 最初の点を得る。

// オーバーライド
public:
	virtual BOOL OnNewDocument();
	virtual void Serialize(CArchive& ar);
#ifdef SHARED_HANDLERS
	virtual void InitializeSearchContent();
	virtual void OnDrawThumbnail(CDC& dc, LPRECT lprcBounds);
#endif // SHARED_HANDLERS

// 実装
public:
	virtual ~CMFCApplication2Doc();
#ifdef _DEBUG
	virtual void AssertValid() const;
	virtual void Dump(CDumpContext& dc) const;
#endif

protected:

// 生成された、メッセージ割り当て関数
protected:
	DECLARE_MESSAGE_MAP()

#ifdef SHARED_HANDLERS
	// 検索ハンドラーの検索コンテンツを設定するヘルパー関数
	void SetSearchContent(const CString& value);
#endif // SHARED_HANDLERS
};
```

###ドキュメントクラスの実装

```C++
// MFCApplication2Doc.cpp : CMFCApplication2Doc クラスの実装
//
#include "stdafx.h"
// SHARED_HANDLERS は、プレビュー、縮小版、および検索フィルター ハンドラーを実装している ATL プロジェクトで定義でき、
// そのプロジェクトとのドキュメント コードの共有を可能にします。
#ifndef SHARED_HANDLERS
#include "MFCApplication2.h"
#endif

#include "MFCApplication2Doc.h"

#include <propkey.h>

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

// CMFCApplication2Doc

IMPLEMENT_DYNCREATE(CMFCApplication2Doc, CDocument)

BEGIN_MESSAGE_MAP(CMFCApplication2Doc, CDocument)
END_MESSAGE_MAP()


// CMFCApplication2Doc コンストラクション/デストラクション

CMFCApplication2Doc::CMFCApplication2Doc()
{
	// TODO: この位置に 1 度だけ呼ばれる構築用のコードを追加してください。

}

CMFCApplication2Doc::~CMFCApplication2Doc()
{
}

BOOL CMFCApplication2Doc::OnNewDocument()
{
	if (!CDocument::OnNewDocument())
		return FALSE;

	// TODO: この位置に再初期化処理を追加してください。
	// (SDI ドキュメントはこのドキュメントを再利用します。
	m_nPoints = 0;
	m_sizeDoc = CSize(800, 600);

// テストデータを作成するときは、下のコメントを外して描画を行いファイル保存する。
/*
	m_points[0] = CSize(100, 100);
	m_points[1] = CSize(100, 300);
	m_points[2] = CSize(300, 300);
	m_points[3] = CSize(300, 100);
	m_points[4] = CSize(100, 100);
	m_nPoints = 5;
*/
	return TRUE;
}

// CMFCApplication2Doc のシリアル化
void CMFCApplication2Doc::Serialize(CArchive& ar)
{
	int i;

	if (ar.IsStoring())
	{
		// TODO: 格納するコードをここに追加してください。
		ar << m_nPoints;
		for (i = 0; i < m_nPoints; i++)
		{
			ar << m_points[i];
		}
	}
	else
	{
		// TODO: 読み込むコードをここに追加してください。
		ar >> m_nPoints;
		for (i = 0; i < m_nPoints; i++)
		{
			ar >> m_points[i];
		}
	}
}

// 最初の描画点を得る。
CPoint* CMFCApplication2Doc::GetFirst()
{
	if (m_nPoints == 0)
		return NULL;
	m_getptr = 0;
	return &m_points[0];
}

// 次の描画点を得る。
CPoint* CMFCApplication2Doc::GetNext()
{
	m_getptr++;
	if (m_getptr >= m_nPoints)
		return NULL;
	return &m_points[m_getptr];
}

#ifdef SHARED_HANDLERS

//縮小版のサポート
void CMFCApplication2Doc::OnDrawThumbnail(CDC& dc, LPRECT lprcBounds)
{
	// このコードを変更してドキュメントのデータを描画します
	dc.FillSolidRect(lprcBounds, RGB(255, 255, 255));

	CString strText = _T("TODO: implement thumbnail drawing here");
	LOGFONT lf;

	CFont* pDefaultGUIFont = CFont::FromHandle((HFONT) GetStockObject(DEFAULT_GUI_FONT));
	pDefaultGUIFont->GetLogFont(&lf);
	lf.lfHeight = 36;

	CFont fontDraw;
	fontDraw.CreateFontIndirect(&lf);

	CFont* pOldFont = dc.SelectObject(&fontDraw);
	dc.DrawText(strText, lprcBounds, DT_CENTER | DT_WORDBREAK);
	dc.SelectObject(pOldFont);
}

// 検索ハンドラーのサポート
void CMFCApplication2Doc::InitializeSearchContent()
{
	CString strSearchContent;
	// ドキュメントのデータから検索コンテンツを設定します。
	// コンテンツの各部分は ";" で区切る必要があります

	// 例:      strSearchContent = _T("point;rectangle;circle;ole object;");
	SetSearchContent(strSearchContent);
}

void CMFCApplication2Doc::SetSearchContent(const CString& value)
{
	if (value.IsEmpty())
	{
		RemoveChunk(PKEY_Search_Contents.fmtid, PKEY_Search_Contents.pid);
	}
	else
	{
		CMFCFilterChunkValueImpl *pChunk = NULL;
		ATLTRY(pChunk = new CMFCFilterChunkValueImpl);
		if (pChunk != NULL)
		{
			pChunk->SetTextValue(PKEY_Search_Contents, value, CHUNK_TEXT);
			SetChunkValue(pChunk);
		}
	}
}

#endif // SHARED_HANDLERS

// CMFCApplication2Doc の診断

#ifdef _DEBUG
void CMFCApplication2Doc::AssertValid() const
{
	CDocument::AssertValid();
}

void CMFCApplication2Doc::Dump(CDumpContext& dc) const
{
	CDocument::Dump(dc);
}
#endif //_DEBUG


// CMFCApplication2Doc コマンド
```

##ビュー
ビューのヘッダーファイルは、特に変更せずにそのまま使用します。

###ビュークラスのヘッダー

```C++
// MFCApplication2View.h : CMFCApplication2View クラスのインターフェイス
//
#pragma once

class CMFCApplication2View : public CView
{
protected: // シリアル化からのみ作成します。
	CMFCApplication2View();
	DECLARE_DYNCREATE(CMFCApplication2View)

// 属性
public:
	CMFCApplication2Doc* GetDocument() const;

// 操作
public:

// オーバーライド
public:
	virtual void OnDraw(CDC* pDC);  // このビューを描画するためにオーバーライドされます。
	virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
protected:

// 実装
public:
	virtual ~CMFCApplication2View();
#ifdef _DEBUG
	virtual void AssertValid() const;
	virtual void Dump(CDumpContext& dc) const;
#endif

protected:

// 生成された、メッセージ割り当て関数
protected:
	afx_msg void OnFilePrintPreview();
	afx_msg void OnRButtonUp(UINT nFlags, CPoint point);
	afx_msg void OnContextMenu(CWnd* pWnd, CPoint point);
	DECLARE_MESSAGE_MAP()
};

#ifndef _DEBUG  // MFCApplication2View.cpp のデバッグ バージョン
inline CMFCApplication2Doc* CMFCApplication2View::GetDocument() const
   { return reinterpret_cast<CMFCApplication2Doc*>(m_pDocument); }
#endif
```

###ビュークラスの実装

ビューの実装ですが、OnDraw メソッドでドキュメントのデータを取得して、一筆書き図形として描画しています。

```C++
// MFCApplication2View.cpp : CMFCApplication2View クラスの実装
//
#include "stdafx.h"
// SHARED_HANDLERS は、プレビュー、縮小版、および検索フィルター ハンドラーを実装している ATL プロジェクトで定義でき、
// そのプロジェクトとのドキュメント コードの共有を可能にします。
#ifndef SHARED_HANDLERS
#include "MFCApplication2.h"
#endif

#include "MFCApplication2Doc.h"
#include "MFCApplication2View.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

// CMFCApplication2View
IMPLEMENT_DYNCREATE(CMFCApplication2View, CView)

BEGIN_MESSAGE_MAP(CMFCApplication2View, CView)
	ON_WM_CONTEXTMENU()
	ON_WM_RBUTTONUP()
END_MESSAGE_MAP()

// CMFCApplication2View コンストラクション/デストラクション
CMFCApplication2View::CMFCApplication2View()
{
	// TODO: 構築コードをここに追加します。

}

CMFCApplication2View::~CMFCApplication2View()
{
}

BOOL CMFCApplication2View::PreCreateWindow(CREATESTRUCT& cs)
{
	// TODO: この位置で CREATESTRUCT cs を修正して Window クラスまたはスタイルを
	//  修正してください。

	return CView::PreCreateWindow(cs);
}

// CMFCApplication2View 描画
void CMFCApplication2View::OnDraw(CDC* pDC)
{
	CMFCApplication2Doc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;

	// TODO: この場所にネイティブ データ用の描画コードを追加します。
	CPoint* p1;
	CPoint* p0 = pDoc->GetFirst();

	if (p0 == NULL)
		return;

	pDC->MoveTo(*p0);

	while (true)
	{
		p1 = pDoc->GetNext();
		if (p1 == NULL)
			return;
		pDC->LineTo(*p1);
		p0 = p1;
		pDC->MoveTo(*p0);
	}
}

void CMFCApplication2View::OnRButtonUp(UINT /* nFlags */, CPoint point)
{
	ClientToScreen(&point);
	OnContextMenu(this, point);
}

void CMFCApplication2View::OnContextMenu(CWnd* /* pWnd */, CPoint point)
{
#ifndef SHARED_HANDLERS
	theApp.GetContextMenuManager()->ShowPopupMenu(IDR_POPUP_EDIT, point.x, point.y, this, TRUE);
#endif
}


// CMFCApplication2View の診断

#ifdef _DEBUG
void CMFCApplication2View::AssertValid() const
{
	CView::AssertValid();
}

void CMFCApplication2View::Dump(CDumpContext& dc) const
{
	CView::Dump(dc);
}

CMFCApplication2Doc* CMFCApplication2View::GetDocument() const // デバッグ以外のバージョンはインラインです。
{
	ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CMFCApplication2Doc)));
	return (CMFCApplication2Doc*)m_pDocument;
}
#endif //_DEBUG


// CMFCApplication2View メッセージ ハンドラー
```

##メインフレーム
デフォルトだとメインフレームのサイズを変更できません。これは、WS_SIZEBOX 属性がウィンドウスタイルに含まれないためです。よって、PreCreateWindow メソッドを修正して、WS_SIZEBOX 属性をスタイルに追加します。

また、デフォルトではかなり大きなウィンドウが表示されるので、SVGA サイズ (800 x 600)に変更しています。結局、MainFrm.cpp の PreCreateWindow を次のように変更します。

```C++
//　ウィンドウサイズとスタイルを変える場合は、このメソッドを修正する。
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
	if( !CFrameWndEx::PreCreateWindow(cs) )
		return FALSE;
	// TODO: この位置で CREATESTRUCT cs を修正して Window クラスまたはスタイルを
	//  修正してください。
	cs.cx = 800;
	cs.cy = 600;
	//  WS_SIZEBOX を追加しないと、ウィンドウの大きさを変えられない。
	cs.style = WS_OVERLAPPED | WS_CAPTION | FWS_ADDTOTITLE | WS_SIZEBOX
		 | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU;

	return TRUE;
}
```

-