MFCツールバーのファイルを開くボタンに最近使用したファイルメニューを追加する方法

# はじめに
MFC Feature Packを使って、ツールバーのファイルを開くボタンに最近使用したファイルメニューをつけてみます。
描画を自分で実装する目的で、フルパス表示も行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523741/c4166179-d055-6fef-6cd9-09e01dbb1270.png)
この記事では、Visual Studio 2019のウィザードでメニューバーとツールバーを使用するMFCプロジェクトを使用しています。

# ボタンにメニューを追加
Visual Studio 2019のウィザードでリボンじゃないメニューバーとツールバーを使用するMFCプロジェクトを作成します。
プロジェクト名は、「CWindowsMenuSample」とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523741/02505984-c1cd-1976-97e8-fd7512c7acef.png)
作成後、MainFrm.cppのCMainFrame::OnCreate()関数の最後にID_FILE_OPENのIDを持ったボタンをメニューボタンに差し替えるコードを追記します。

```c++:MainFrm.cpp
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    ...
	CMenu menu;
	menu.CreatePopupMenu();
	menu.AppendMenu(MF_STRING, ID_FILE_MRU_FILE1, L"");
	CMFCToolBarMenuButton menuButton(ID_FILE_OPEN, menu.GetSafeHmenu(), -1);
	m_wndToolBar.ReplaceButton(ID_FILE_OPEN, menuButton);
	m_wndToolBar.RecalcLayout();
	menu.DestroyMenu();
}
```
そのまま実行した場合、メニューが表示されない場合があります。
レジストリにボタンの状態が書き込まれてしまっているためなので、一度レジストリを削除します。
削除するディレクトリは、
コンピューター\HKEY_CURRENT_USER\Software\アプリケーション ウィザードで生成されたローカル アプリケーション\プロジェクト名\Workspace
となります。
実行すると、ファイルを開くボタンに最近使用したファイルメニューが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523741/a490f463-1bc2-894f-fa52-77998182be40.png)

# メニューを自分のクラスに変更
メニューを自分で描画するために、CMFCToolBarMenuButtonクラスの派生クラスとなるメニューボタンクラスを作成してボタンに割り当てます。

## メニューボタンクラスを作成
何もしない、CMFCToolBarMenuButtonクラスの派生クラスを作成します。
追加する際にCreateObject() でインスタンスが生成されるため、デフォルトコンストラクタを実装し、DECLARE_SERIAL、IMPLEMENT_SERIALマクロを使用します。
デフォルトコンストラクタ以外に、メニューIDとメニュー文字列を引数に持つコンストラクタを作成します。

```c++;CMyRecentFileMenu.h
#pragma once
class CMyRecentFileMenu : public CMFCToolBarMenuButton
{
	DECLARE_SERIAL(CMyRecentFileMenu)
public:
	CMyRecentFileMenu();
	CMyRecentFileMenu(UINT uiID, LPCTSTR lpszText);
	virtual ~CMyRecentFileMenu();
};
```
```c++;CMyRecentFileMenu.cpp
#include "pch.h"
#include "WindowsMenuSample.h"
#include "CMyRecentFileMenu.h"

IMPLEMENT_SERIAL(CMyRecentFileMenu, CMFCToolBarMenuButton, 1)
CMyRecentFileMenu::CMyRecentFileMenu() : CMFCToolBarMenuButton()
{
}

CMyRecentFileMenu::CMyRecentFileMenu(UINT uiID, LPCTSTR lpszText)
	: CMFCToolBarMenuButton(uiID, NULL, -1, lpszText)
{
}

CMyRecentFileMenu::~CMyRecentFileMenu()
{
}
```

## メニューボタンクラスを作成したクラスに差し替え
作成したクラスを、メニューボタンに差し替えます。
差し替えは、CMainFrameのOnShowPopupMenu() 関数をオーバーライドして実装します。
OnShowPopupMenu() は、ポップアップメニューがアクティブになるときに呼び出される関数です。
CMainFrame上にあるすべてのポップアップメニューに対して呼ばれるため、IDを見てボタンからのメッセージを判断します。
最近使用したファイルは、ID_FILE_MRU_FILE1~ID_FILE_MRU_LASTとなります。

```c++:MainFrm.h
class CMainFrame : public CMDIFrameWndEx
{
	...
	virtual BOOL OnShowPopupMenu(CMFCPopupMenu* pMenuPopup);
}
```

```c++:MainFrm.cpp
#include "CMyRecentFileMenu.h"
...
BOOL CMainFrame::OnShowPopupMenu(CMFCPopupMenu* pMenuPopup)
{
	if(!pMenuPopup) {
		return CMDIFrameWndEx::OnShowPopupMenu(pMenuPopup);
	}

	if(pMenuPopup->GetParentButton()->m_nID == ID_FILE_OPEN) {
		// ファイルを開くボタンからのメニュー
		// 最近使用したファイル
		int nId, index, nCount = ID_FILE_MRU_LAST - ID_FILE_MRU_FIRST + 1;
		for(int i = 0; i < nCount; i++) {
			nId = ID_FILE_MRU_FILE1 + i;
			index = pMenuPopup->GetMenuBar()->CommandToIndex(nId);
			if(0 <= index) {
				// カスタムメニューを追加
				CMFCToolBarButton* pBtn = pMenuPopup->GetMenuBar()->GetButton(index);
				CString strOrg = pBtn->m_strText;
				pMenuPopup->RemoveItem(index);
				pMenuPopup->InsertItem(CMyRecentFileMenu(nId, strOrg));
			}
		}
	}
	return CMDIFrameWndEx::OnShowPopupMenu(pMenuPopup);
}
```
派生クラスでは何も実装していないため、差し替え後も動きに変化はありません。

# 自分でフルパスを描画
最近使用したファイルメニューにフルパスを描画します。
フルパスを描画するために、フルパスリストを取得します。
取得後、フルパスをメニューボタンクラスに格納します。
描画範囲を確保するためメニューサイズを変更し、格納したフルパスを描画します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523741/c4166179-d055-6fef-6cd9-09e01dbb1270.png)

## フルパスリストを取得
最近使用したファイルのフルパスは、CWinApp::m_pRecentFileList にprotectで格納されています。
CWinAppの派生クラスCWindowsMenuSampleAppにフルパス配列を取得する関数 GetRecentFileNames(CStringArray& strFileNames) を追加します。

```c++:CWindowsMenuSampleApp.h
class CWindowsMenuSampleApp : public CWinAppEx
{
	...
public:
	int GetRecentFileNames(CStringArray& strFileNames);
	...
};
```

```c++:CWindowsMenuSampleApp.cpp
int CWindowsMenuSampleApp::GetRecentFileNames(CStringArray& strFileNames)
{
	int i, nCount = m_pRecentFileList->m_nSize;
	strFileNames.RemoveAll();
	for(i = 0; i < nCount; i++) {
		if(m_pRecentFileList->m_arrNames[i].IsEmpty()) {
			break;
		}
		strFileNames.Add(m_pRecentFileList->m_arrNames[i]);
	}
	return strFileNames.GetSize();
}
```

## フルパスをメニューボタンクラスに格納
フルパスをメニューボタンクラスに格納します。
格納先となる変数を、自分のクラスに作成してコンストラクタに追加します。
また、このクラスを追加しているCMFCPopupMenu::InsertItem(const CMFCToolBarMenuButton& button, int iInsertAt) 関数は、引数で渡すCMFCToolBarMenuButton クラスがCreateObject() でインスタンス化されるため、デフォルトコンストラクタで生成したインスタンスが格納されます。
CreateObject() でインスタンス生成後にCMFCToolBarMenuButton::CopyFrom(const CMFCToolBarButton& src) 関数が呼ばれるため、CopyFrom() 関数をオーバーライドして、追加したメンバ変数の格納を実装します。

```c++:CMyRecentFileMenu.h
class CMyRecentFileMenu : public CMFCToolBarMenuButton
{
	...
public:
	CString m_strFileName;
public:
	CMyRecentFileMenu();
	CMyRecentFileMenu(UINT uiID, LPCTSTR lpszText, const CString& strFileName);
	...
	virtual void CopyFrom(const CMFCToolBarButton& src) override;
	...
};
```

```c++:CMyRecentFileMenu.cpp
CMyRecentFileMenu::CMyRecentFileMenu(UINT uiID, LPCTSTR lpszText, const CString& strFileName)
	: CMFCToolBarMenuButton(uiID, NULL, -1, lpszText)
{
	m_strFileName = strFileName;
}

void CMyRecentFileMenu::CopyFrom(const CMFCToolBarButton& src)
{
	CMFCToolBarMenuButton::CopyFrom(src);
	if(src.IsKindOf(RUNTIME_CLASS(CMyRecentFileMenu))) {
		CMyRecentFileMenu* pMenu = (CMyRecentFileMenu*)&src;
		m_strFileName = pMenu->m_strFileName;
	}
}
```
フルパスをCMainFrame::OnShowPopupMenu() を書き換えてメニューボタンクラスに登録します。

```c++:MainFrm.cpp
BOOL CMainFrame::OnShowPopupMenu(CMFCPopupMenu* pMenuPopup)
{
	if(!pMenuPopup) {
		return CMDIFrameWndEx::OnShowPopupMenu(pMenuPopup);
	}

	if(pMenuPopup->GetParentButton()->m_nID == ID_FILE_OPEN) {
		// ファイルを開くボタンからのメニュー
		// ファイル名一覧を取得
		CWindowsMenuSampleApp* pApp = (CWindowsMenuSampleApp*)AfxGetApp();
		CStringArray strRecentFileNames;
		int nCount = pApp->GetRecentFileNames(strRecentFileNames);

		// 最近使用したファイル
		int nId, index;
		for(int i = 0; i < nCount; i++) {
			nId = ID_FILE_MRU_FILE1 + i;
			index = pMenuPopup->GetMenuBar()->CommandToIndex(nId);
			if(0 <= index) {
				// カスタムメニューを追加
				CMFCToolBarButton* pBtn = pMenuPopup->GetMenuBar()->GetButton(index);
				CString strOrg = pBtn->m_strText;
				CString strFileName = strRecentFileNames[i];
				pMenuPopup->RemoveItem(index);
				pMenuPopup->InsertItem(CMyRecentFileMenu(nId, strOrg, strFileName));
			}
		}
	}

	return CMDIFrameWndEx::OnShowPopupMenu(pMenuPopup);
}
```

## メニューサイズを変更
メニューサイズは、SIZE OnCalculateSize() 関数をオーバーライドして変更します。

```c++:CMyRecentFileMenu.h
class CMyRecentFileMenu : public CMFCToolBarMenuButton
{
	...
	virtual SIZE OnCalculateSize(CDC* pDC, const CSize& sizeDefault, BOOL bHorz) override;
	...
};
```

```c++:CMyRecentFileMenu.cpp
SIZE CMyRecentFileMenu::OnCalculateSize(CDC* pDC, const CSize& sizeDefault, BOOL bHorz)
{
	// 最大幅の計算
	long width = 32;
	SIZE sz;
	memset(&sz, 0, sizeof(sz));

	sz = pDC->GetTextExtent(m_strText);
	width = max(width, sz.cx);

	sz = pDC->GetTextExtent(m_strFileName);
	width = max(width, sz.cx);

	// 幅と高さの変更
	sz.cx = width + 4;
	sz.cy = sz.cy * 2 + 4;

	return sz;
}
```

## フルパスを描画
描画は、void OnDraw() 関数をオーバーライドして実装します。

```c++:CMyRecentFileMenu.h
class CMyRecentFileMenu : public CMFCToolBarMenuButton
{
	...
	virtual void OnDraw(CDC* pDC, const CRect& rect, CMFCToolBarImages* pImages, BOOL bHorz = TRUE,
                        BOOL bCustomizeMode = FALSE, BOOL bHighlight = FALSE, BOOL bDrawBorder = TRUE,
                        BOOL bGrayDisabledButtons = TRUE) override;
	...
};
```

```c++:CMyRecentFileMenu.cpp
void CMyRecentFileMenu::OnDraw(CDC* pDC, const CRect& rect, CMFCToolBarImages* pImages,
							   BOOL bHorz, BOOL bCustomizeMode,
							   BOOL bHighlight, BOOL bDrawBorder, BOOL bGrayDisabledButtons)
{
	// 背景
	pDC->FillSolidRect(rect, 0xf0f0f0);
	CBrush* pOldBrush = (CBrush*)pDC->SelectStockObject(WHITE_BRUSH);
	pDC->FrameRect(rect, pDC->GetCurrentBrush());
	pDC->SelectObject(pOldBrush);

	// 文字以外を描画
	CString strText = m_strText;
	m_strText = L"";
	CMFCToolBarMenuButton::OnDraw(pDC, rect, pImages, bHorz, bCustomizeMode, bHighlight,
                                  bDrawBorder, bGrayDisabledButtons);
	m_strText = strText;

	COLORREF clrText = CMFCVisualManager::GetInstance()->GetMenuItemTextColor(this, FALSE, FALSE);
	COLORREF clrTextOld = pDC->SetTextColor(clrText);

	strText.Replace(_T("&&"), AFX_DUMMY_AMPERSAND_SEQUENCE);
	strText.Remove(_T('&'));
	strText.Replace(AFX_DUMMY_AMPERSAND_SEQUENCE, _T("&&"));

	// 背景モード
	int nOldBkMode = pDC->SetBkMode(TRANSPARENT);

	SIZE sz = pDC->GetTextExtent(m_strFileName);
	int x = rect.left + 1, y = rect.top + 1;
	pDC->TextOut(x, y, strText);
	y += sz.cy;
	pDC->TextOut(x, y, m_strFileName);

	pDC->SetTextColor(clrTextOld);
	pDC->SetBkMode(nOldBkMode);
}
```
実行すると、ファイルを開くボタンのメニューにフルパスが表示されます。