MFCのCMFCLinkCtrlでCMFCButton::SelectFontがコンパイルエラーになる原因と対処法(SelectFontマクロ衝突)

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)を見直すと効果的

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次