CMFCLinkCtrlをダークモード対応のために独自描画しようとして、CMFCButton::SelectFont(pDC)を呼んだ途端に謎のコンパイルエラーが出ることがあります。原因はMFCではなく、Windowsヘッダー側のSelectFontマクロ衝突です。再現方法から安全な回避策、実装例までまとめます。
起きている現象:CMFCButton::SelectFont が呼べない
MFCのCMFCLinkCtrlはリンク表示用のコントロールですが、文字色などがGetGlobalData()(実体はafxGlobalData)の色に依存する場面があり、アプリ独自の配色やダークモードに合わせづらいことがあります。
そこで、CMFCLinkCtrlを派生してOnDraw()をオーバーライドし、「色は自前」「レイアウトやフォント選択は既存のMFC流儀に合わせたい」という意図で、描画中にCMFCButton::SelectFont(pDC)を呼ぶと、次のようなエラーでビルドが止まります。
| 代表的なエラーメッセージ | 現場での見え方 |
|---|---|
| not enough arguments for function-like macro invocation ‘SelectFont’ | 「SelectFont は引数が足りない」と言われる |
| illegal token on right side of ‘::’ | CMFCButton::SelectFontの::付近で壊れる |
| syntax error … | 本当の原因が見えない形で連鎖的に文法エラーが出る |
ポイントは「afxbutton.hも入れている」「MFCにSelectFontという仮想関数が実在する」のに、コンパイラが“関数呼び出し”として扱ってくれないことです。
結論:原因は Windows ヘッダーの SelectFont マクロ衝突
この症状の正体は、関数ではなくマクロです。Windowsのヘッダー(多くの場合はwindowsx.h)に、次のような「関数形式マクロ(function-like macro)」が定義されています。
/* windowsx.h の代表例(環境により表記は多少異なります) */
#define SelectFont(hdc, hfont) ((HFONT)SelectObject((hdc), (HGDIOBJ)(HFONT)(hfont)))
中身はSelectObject()を呼ぶだけのヘルパーで、Win32 APIを素のHDC/HFONTで扱う時に便利、という位置付けです。
問題は、C/C++のプリプロセッサがスコープ修飾を一切考慮しないことにあります。つまり、CMFCButton::SelectFont(pDC)のように「クラス名+スコープ演算子+関数名」で書いても、プリプロセッサは容赦なくSelectFontトークンを見つけて「マクロ呼び出し」だと判断します。
そして、Windows側のマクロは引数が2つ必要です。
SelectFont(hdc, hfont)… 引数2個CMFCButton::SelectFont(pDC)… 引数1個
結果として、プリプロセッサ段階で「引数が足りない」「::の右側が変だ」といったエラーが発生し、MFCの関数に到達する前にソースが崩壊します。コンパイルエラーが“マクロっぽい”のは、このためです。
なぜ「クラス名::関数名」でもマクロ展開されるのか
マクロ展開は「トークン列」を対象に行われます。識別子トークンがマクロ名に一致し、かつ(関数形式マクロなら)直後に(が続けば、前後の記号(::や->など)に関係なく展開されます。
つまり、次のように見えているコードは、プリプロセッサにとっては「SelectFontという名前が見えている」以上アウトです。
CMFCButton::SelectFont(pDC)
// ^^^^^^^^ ここがマクロ名と一致してしまう
この挙動は、よく知られたmin/maxマクロとstd::min/std::maxの衝突と同じ種類の事故です。違いは、今回は“SelectFontはあまり有名でない”ために原因に辿り着きにくい点です。
まずは確認:自分のプロジェクトで SelectFont がマクロかチェックする
「本当にマクロが定義されているのか」を確認すると、原因特定が一気に早くなります。確認は簡単で、該当の.cpp(または問題が出ているヘッダー)に次を一時的に入れます。
#ifdef SelectFont
#pragma message("SelectFont is defined as a macro")
#endif
ビルドログにメッセージが出れば、ほぼ確定です。さらに突き止めたい場合は、Visual Studioの「検索(Find in Files)」で#define SelectFontを探すか、プリプロセス後のファイル(/P)を出力してSelectFont周辺の展開結果を確認すると、どのヘッダーから入ってきたかが見えます。
| 調査方法 | 目的 | 手早さ |
|---|---|---|
#ifdef SelectFontで検出 | マクロ有無の即判定 | 最速 |
Find in Filesで#define SelectFont検索 | どのヘッダーが定義したか推測 | 速い |
| プリプロセス出力(/P)を読む | 展開結果を確実に追う | 確実だが時間はかかる |
解決策:SelectFont マクロを無効化する
最も分かりやすい解決は、SelectFontマクロを解除することです。MFCのCMFCButton::SelectFontを使う箇所(またはその直前)で、次のように#undefします。
#ifdef SelectFont
#undef SelectFont
#endif
この1行(+ガード)で、プリプロセッサはSelectFontをマクロとして扱わなくなり、CMFCButton::SelectFont(pDC)が正しく「メンバ関数呼び出し」として解釈されます。
影響範囲を狭めたい場合:push_macro / pop_macro
#undefは「その行以降の翻訳単位全体」に影響するため、ヘッダーでやると波及が大きくなることがあります。影響範囲を限定したい場合は、MSVCで使える#pragma push_macro/#pragma pop_macroが便利です。
#pragma push_macro("SelectFont")
#undef SelectFont
// ここで CMFCButton::SelectFont などを使う
#pragma pop_macro("SelectFont")
この方法なら、局所的にマクロを外して安全にMFC側の関数を呼び、終わったら元のマクロ定義を復元できます。複数のヘッダーが絡む大規模プロジェクトでも、事故を起こしにくい選択肢です。
どうしても undef できない場合:括弧でマクロ展開を回避
第三者のヘッダーがSelectFontマクロに依存していて、#undefしたくない(できない)ケースもあります。その場合は、関数名を括弧で包んで「マクロ呼び出しの形」を崩す方法が使えます。
(CMFCButton::SelectFont)(pDC);
関数形式マクロは「マクロ名の直後に(が来た時」だけ展開されます。上の書き方だと、SelectFontトークンの直後は)になるため、プリプロセッサはマクロ呼び出しとして認識できず、結果的にC++の関数呼び出しとして解釈されます。(std::min)(a,b)と同じ定番テクニックです。
| 対処法 | メリット | デメリット | おすすめ場面 |
|---|---|---|---|
#undef SelectFont | シンプルで分かりやすい | 以降の範囲に影響 | .cpp内で完結する修正 |
push_macro/pop_macro | 影響範囲を局所化できる | MSVC依存(移植性は落ちる) | ヘッダーや共通部品で安全に使いたい |
(CMFCButton::SelectFont)(...) | マクロ定義を変えずに回避 | 意図が伝わりにくいことがある | どうしてもundefできない/したくない |
実装例:ダークモード配色の CMFCLinkCtrl を自前描画する
ここからは、実際にCMFCLinkCtrlを派生して独自描画する例です。ポイントは次の通りです。
- 文字色は状態(通常/ホバー/無効)で自前決定する
- フォント選択はMFC既存のロジックを使い、見た目の統一感を維持する
- GDI状態は
SaveDC/RestoreDCで確実に戻す(旧フォントの扱いで悩まない) SelectFontマクロ衝突を確実に避ける
// 例: CMyLinkCtrl.h
#pragma once
#include <afxlinkctrl.h>
#include <afxbutton.h> // CMFCButton
class CMyLinkCtrl : public CMFCLinkCtrl
{
DECLARE_DYNAMIC(CMyLinkCtrl)
public:
CMyLinkCtrl() = default;
virtual ~CMyLinkCtrl() = default;
protected:
virtual void OnDraw(CDC* pDC, const CRect& rect, UINT uiState) override;
COLORREF GetTextColor(UINT uiState) const;
};
// 例: CMyLinkCtrl.cpp
#include "pch.h"
#include "CMyLinkCtrl.h"
// ここでマクロ衝突を潰しておく(この.cpp内だけならこれが一番楽)
#ifdef SelectFont
#undef SelectFont
#endif
IMPLEMENT_DYNAMIC(CMyLinkCtrl, CMFCLinkCtrl)
COLORREF CMyLinkCtrl::GetTextColor(UINT uiState) const
{
// 例として「ダークモード/ライトモード切替フラグ」をアプリ側で持っている想定
const bool dark = false; // 実際は設定値などから取得
// 状態(uiState)は CMFCButton と同じ考え方で渡されます
const bool disabled = (uiState & ODS_DISABLED) != 0;
const bool hot = (uiState & ODS_HOTLIGHT) != 0;
const bool selected = (uiState & ODS_SELECTED) != 0;
if (disabled)
return dark ? RGB(120, 120, 120) : RGB(160, 160, 160);
if (selected)
return dark ? RGB(180, 200, 255) : RGB(0, 80, 160);
if (hot)
return dark ? RGB(130, 170, 255) : RGB(0, 110, 230);
return dark ? RGB(200, 200, 200) : RGB(0, 0, 0);
}
void CMyLinkCtrl::OnDraw(CDC* pDC, const CRect& rect, UINT uiState)
{
if (pDC == nullptr)
return;
const int saved = pDC->SaveDC();
// 背景は必要に応じて塗る(ここでは透明前提)
// pDC->FillSolidRect(rect, ...);
// フォントはMFCのロジックを利用(ここが今回の本題)
CMFCButton::SelectFont(pDC);
// テキスト色を自前で決める
const COLORREF clrText = GetTextColor(uiState);
pDC->SetTextColor(clrText);
pDC->SetBkMode(TRANSPARENT);
CString text;
GetWindowText(text);
CRect rcText = rect;
rcText.DeflateRect(2, 1);
UINT format = DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS;
pDC->DrawText(text, rcText, format);
pDC->RestoreDC(saved);
}
上の例は配色ロジックを分離しているため、ダークモード判定やテーマ色の取得を差し替えやすいのが利点です。実プロジェクトでは、アプリのテーマ管理(設定値、システム設定、独自スキンなど)に合わせてdarkや各色を決定してください。
よくあるハマりどころと補足
「afxbutton.h を include しているのに…」は関係ない
今回の問題は、MFC側の宣言が見えていないのではなく、プリプロセッサが先にソースを破壊していることが原因です。afxbutton.hを追加しても、マクロが生きている限り同じ場所で落ちます。
windowsx.h はどこから入ってくる?
windowsx.hは多くのプロジェクトで「便利マクロ集」として明示的にインクルードされます。特に、プリコンパイル済みヘッダー(pch.hやstdafx.h)に入っていると、すべての.cppに影響が波及します。
「自分は直接書いていない」という場合も、共通ヘッダーや他者が追加したユーティリティヘッダーが原因になっていることが多いので、まずはPCHを疑うのが近道です。
なぜエラーメッセージがバラバラなのか
マクロ展開が失敗すると、以降のトークン列がC++として成立しなくなり、コンパイラは雪崩のように文法エラーを報告します。そのため、根本原因がSelectFontマクロだと気づきにくく、「::がおかしい」「謎のsyntax error」といった一見関係ない指摘が増えます。
| 見えている症状 | 実際に起きていること | 対策の方向性 |
|---|---|---|
| 引数が足りないと言われる | SelectFont(hdc, hfont)マクロが反応している | マクロを無効化/回避する |
::の右側が不正と言われる | スコープ修飾を含むトークン列がマクロ展開で崩れる | 同上 |
| 関連しない行までsyntax errorになる | プリプロセッサ段階でコード構造が破壊され、以降が連鎖的に崩れる | 最初に落ちた箇所をマクロ疑いで調べる |
再発防止:マクロ衝突を起こしにくいヘッダー運用
今回のようなマクロ衝突は、解決できても「また別の場所で別の識別子が衝突する」形で再発しがちです。運用面でできる対策も押さえておくと、長期的に効きます。
windowsx.h を PCH に入れない
便利だからといってPCHに入れると、プロジェクト全体にマクロ汚染が広がります。必要な.cppだけで#include <windowsx.h>する、という方針にすると衝突の範囲が限定されます。
どうしても共通で必要なら「入れた直後に undef」
共通ヘッダーに入れる必要がある場合は、windowsx.hの直後に「衝突しやすいマクロ」をまとめて解除するのも現実的です。例えば、Windows側のmin/maxに対してはNOMINMAXを使うのが定番ですが、SelectFontは個別に#undefするのが確実です。
// 例: pch.h や共通ヘッダーでの運用イメージ
#include <windows.h>
#include <windowsx.h>
#ifdef SelectFont
#undef SelectFont
#endif
「括弧で回避」をチームの約束事にする場合の注意
(CMFCButton::SelectFont)(pDC)のような書き方は強力ですが、知らないメンバーには意図が伝わりにくいことがあります。採用するなら、コードレビューで「マクロ衝突回避のため」というコメントを添えたり、コーディング規約に理由を書いておくと保守性が上がります。
まとめ
CMFCButton::SelectFontが呼べずに出る一連のコンパイルエラーは、MFCではなくWindowsヘッダーのSelectFontマクロ衝突が原因- スコープ修飾(
Class::Func)でもプリプロセッサは容赦なくマクロ展開する - 対処は
#undef SelectFontが最短。影響範囲を狭めたいならpush_macro/pop_macro、どうしても触れないなら(CMFCButton::SelectFont)(pDC) - 再発防止として、
windowsx.hの扱い(PCHに入れない/入れた直後にundef)を見直すと効果的

コメント