CMFCPropertyGridCtrlの選択ハイライトを抑える:MFCプロパティグリッドの常に選択状態を回避する方法

CMFCPropertyGridCtrl(MFCのプロパティグリッド)は、フォーカスが外れても選択行のハイライトが強く残りがちです。ライトテーマでは気にならなくても、ダークテーマでは「選択が貼り付いた」ように見えてUIの一体感を損ねます。本記事では、専用APIがない前提で、フォーカス喪失時に目立たない項目へ選択を退避させる実用的な回避策を解説します。

目次

CMFCPropertyGridCtrlで「選択が常に残る」現象とは

Windows/MFCで設定画面やインスペクタ(プロパティ表示パネル)を作るとき、CMFCPropertyGridCtrlは手軽で便利です。ところが、標準のままだと次のような見た目になりがちです。

  • プロパティグリッド内で項目を選択すると、行全体がしっかりハイライトされる
  • 別のコントロール(ボタン、エディット、リスト等)へフォーカスを移しても、選択行のハイライトがそのまま残る
  • ライトテーマでは違和感が小さいが、ダークテーマでは「そこだけ強く明るい/濃い色が残る」ため目立ちすぎる

結果として、「今このプロパティグリッドがアクティブなのか?」が分かりにくくなり、UI全体の統一感が崩れます。Visual Studioのプロパティグリッドのように、フォーカスを失った選択行はもう少し控えめな色になってほしい、というニーズが出てきます。

LVS_SHOWSELALWAYS/TVS_SHOWSELALWAYSのように制御できない理由

同じ「行選択」でも、標準コントロールには “フォーカスが外れても選択を見せ続けるか” を切り替えるスタイルがあります。

コントロール代表的なスタイル意味
CListCtrlLVS_SHOWSELALWAYSフォーカスが外れても選択ハイライトを表示し続ける
CTreeCtrlTVS_SHOWSELALWAYSフォーカスが外れても選択ハイライトを表示し続ける
CMFCPropertyGridCtrl相当する公開スタイルなし「常に選択状態」挙動のオン/オフを直接切り替える手段が用意されていない

CMFCPropertyGridCtrlには、上記のような “Show Always / Stay Selected”的な挙動を直接オフにするフラグが見当たりません。さらに、配色をまとめて変えるAPIはあっても、

  • フォーカスがあるときの選択色
  • フォーカスを失ったときの選択色(いわゆる不活性選択)

を別々に指定できる専用プロパティも提供されていないのが現状です。つまり「その色だけ変えたい」と思っても、標準APIだけで狙い撃ちするのは難しい、というのが出発点になります。

結論:専用のオフスイッチはない。見た目を“そう見せない”のが現実解

この問題は、挙動を根本的に変えるのではなく、見た目上そう見えないようにするのが最短ルートです。代表的な落としどころは次の2つです。

アプローチ概要メリット注意点
選択を退避する(本記事の方法)フォーカス喪失時に、目立たない項目(グループ行等)へ選択を移す内部描画に踏み込まずに実装できる/差分が小さい選択が移るため「最後に編集していた項目」を選択状態のまま残したい場合は工夫が必要
描画を差し替える(高度)選択描画ロジックに介入して、不活性選択色を独自に描くVisual Studioに近い表現ができる可能性MFC内部実装への依存が強くなり、将来の更新で壊れやすい

ここでは、実務で採用しやすい「選択退避」ワークアラウンドを中心に紹介します。

ワークアラウンド:フォーカス喪失時に選択を“退避”させる

CMFCPropertyGridCtrlは「選択されている行」を強調表示します。ならば逆に、フォーカスを失った瞬間に強調されても困らない行へ選択を移してしまえば、結果的に “選択が残って目立つ” 状態を回避できます。

退避先として使いやすいのは「グループタイトル行」

プロパティグリッドはカテゴリ分けのためにグループ(折りたたみ可能な見出し)を持てます。多くのUIではグループ行は通常プロパティより控えめな背景になりやすく、ダークテーマでも違和感が出にくい傾向があります。そこで、フォーカス喪失時に先頭グループへ選択を移すのがシンプルで効果的です。

どのタイミングで退避が必要か

ポイントは「フォーカスが別コントロールへ移った瞬間」を確実に拾うことです。プロパティグリッドはインプレース編集(セル内エディット)を使うため、フォーカスがグリッド本体ではなく子ウィンドウ(エディット)にあることもあります。

フォーカスの起点発生しやすい状況拾うべき通知/メッセージ狙い
グリッド本体行選択だけして編集はしていないWM_KILLFOCUS(OnKillFocus)コントロール外へフォーカスが移ったときに退避
インプレース編集(エディット)値を編集中に別コントロールをクリックしたEN_KILLFOCUS(OnCommandで拾う)編集用子ウィンドウから外へ出た瞬間に退避

実装手順:CMFCPropertyGridCtrlを派生してOnKillFocusとOnCommandを追加する

ここからは、実際に動く形を意識して実装例を示します。狙いはシンプルです。

  • フォーカスがグリッドの外に出たら、選択をグループ行へ移す
  • ただし、グリッド内(子ウィンドウ含む)へ移っただけなら何もしない

1) 派生クラスの骨組み

まずはCMFCPropertyGridCtrlを継承したクラスを作ります。WM_KILLFOCUSを確実に拾うため、メッセージマップも用意しておくと安全です。

// CMyPropertyGridCtrl.h
#pragma once

#include <afxpropertygridctrl.h>

class CMyPropertyGridCtrl : public CMFCPropertyGridCtrl
{
    DECLARE_DYNAMIC(CMyPropertyGridCtrl)

public:
    CMyPropertyGridCtrl() = default;
    virtual ~CMyPropertyGridCtrl() = default;

protected:
    afx_msg void OnKillFocus(CWnd* pNewWnd);
    virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam);

    // 退避先の項目を返す(最小構成では GetProperty(0) を返すだけでOK)
    CMFCPropertyGridProperty* GetFallbackProperty();

    DECLARE_MESSAGE_MAP()
};
// CMyPropertyGridCtrl.cpp
#include "pch.h"
#include "CMyPropertyGridCtrl.h"

IMPLEMENT_DYNAMIC(CMyPropertyGridCtrl, CMFCPropertyGridCtrl)

BEGIN_MESSAGE_MAP(CMyPropertyGridCtrl, CMFCPropertyGridCtrl)
    ON_WM_KILLFOCUS()
END_MESSAGE_MAP()

2) グリッド本体のOnKillFocusで「外へ出た」ことを判定する

OnKillFocusには「次にフォーカスを受け取るウィンドウ」が渡されます。ここで、フォーカス先が自分(または子ウィンドウ)なら無視し、外なら退避します。

void CMyPropertyGridCtrl::OnKillFocus(CWnd* pNewWnd)
{
    // まずは標準処理を実行
    CMFCPropertyGridCtrl::OnKillFocus(pNewWnd);

    if (pNewWnd == nullptr || pNewWnd->GetSafeHwnd() == nullptr)
    {
        return;
    }

    const HWND hwndNew = pNewWnd->GetSafeHwnd();

    // 自分自身へ戻るだけなら何もしない
    if (hwndNew == this->GetSafeHwnd())
    {
        return;
    }

    // 子ウィンドウ(インプレース編集など)へ移っただけなら何もしない
    if (::IsChild(this->GetSafeHwnd(), hwndNew))
    {
        return;
    }

    // ここに来たら「コントロール外へフォーカスが移動した」可能性が高い
    if (CMFCPropertyGridProperty* pFallback = GetFallbackProperty())
    {
        SetCurSel(pFallback, TRUE);
    }
}

この段階でも「行選択だけしていたケース」では、かなりの確率で期待通りに動きます。ただし、値を編集中(インプレース編集がフォーカスを持っている状態)で外へクリックした場合、WM_KILLFOCUSがグリッド本体ではなく子ウィンドウ側で発生することがあります。そこで次の手当が必要です。

3) インプレース編集のEN_KILLFOCUSをOnCommandで拾う

CMFCPropertyGridCtrl内のエディットがフォーカスを失った瞬間にも、同様に退避処理をかけます。EN_KILLFOCUSはコントロール通知として親(=プロパティグリッド)へ届くため、OnCommandをオーバーライドして捕まえるのが手堅い方法です。

BOOL CMyPropertyGridCtrl::OnCommand(WPARAM wParam, LPARAM lParam)
{
    const UINT notifyCode = HIWORD(wParam);

    if (notifyCode == EN_KILLFOCUS)
    {
        // EN_KILLFOCUSが来た時点で、新しいフォーカス先はすでに変わっていることが多い
        const HWND hwndNewFocus = ::GetFocus();

        if (hwndNewFocus != nullptr)
        {
            // 自分自身、または子ウィンドウにフォーカスがあるなら「グリッド内部の遷移」
            if (hwndNewFocus == this->GetSafeHwnd() ||
                ::IsChild(this->GetSafeHwnd(), hwndNewFocus))
            {
                return CMFCPropertyGridCtrl::OnCommand(wParam, lParam);
            }
        }

        // コントロール外へ出たと判断 → 退避
        if (CMFCPropertyGridProperty* pFallback = GetFallbackProperty())
        {
            SetCurSel(pFallback, TRUE);
            return TRUE; // ここで処理済みにしておくと再入を避けやすい
        }
    }

    return CMFCPropertyGridCtrl::OnCommand(wParam, lParam);
}

これで「編集していたプロパティ行」からフォーカスが外れても、ハイライトが強く残る行が画面に居座りにくくなります。実際には、選択がグループ行へ移るため、視覚的には “選択が解除された” に近い印象になります。

4) GetFallbackPropertyの実装:まずはGetProperty(0)でOK

最も簡単な退避先は「先頭のグループ(または項目)」です。まずはGetProperty(0)を返すだけで十分なケースが多いでしょう。

CMFCPropertyGridProperty* CMyPropertyGridCtrl::GetFallbackProperty()
{
    // 先頭がグループになっている構成なら、そのままグループ行へ退避できる
    return GetProperty(0);
}

ただし、先頭が通常プロパティの場合は “目立たない退避先” にならないことがあります。そんなときは、ダミーのグループを先頭に追加して、そこへ退避する設計が扱いやすいです。

先頭に「ダミーグループ」を置く例

初期化時に折りたたみ可能なグループを先頭に追加し、以降のプロパティをその下にぶら下げる、あるいは通常どおり並べつつ先頭だけダミーにする、という方法があります。ここでは後者(先頭にダミーを置くだけ)のイメージを示します。

// 例:初期化時に呼ぶ関数
void InitPropertyGrid(CMyPropertyGridCtrl& grid)
{
    // 目立たない退避先として使う「ダミーグループ」
    auto* pDummyGroup = new CMFCPropertyGridProperty(_T(" ")); // タイトルを空に近づける
    grid.AddProperty(pDummyGroup, FALSE, FALSE);

    // 通常のグループ・プロパティを追加
    auto* pGroup1 = new CMFCPropertyGridProperty(_T("General"));
    pGroup1->AddSubItem(new CMFCPropertyGridProperty(_T("Name"), _T("Sample")));
    pGroup1->AddSubItem(new CMFCPropertyGridProperty(_T("Enabled"), (_variant_t)true, _T("")));
    grid.AddProperty(pGroup1);

    grid.AdjustLayout();
}

そして退避先は「ダミーグループ」を返すようにします(ここでは“先頭=ダミー”前提)。

CMFCPropertyGridProperty* CMyPropertyGridCtrl::GetFallbackProperty()
{
    return GetProperty(0); // 先頭をダミーとして運用
}

ダミーを入れると、どの画面でも退避先が一定になるため「たまたま先頭が通常プロパティで目立ってしまう」事故を防げます。

どの退避先がベスト?用途別の選び方

退避先はプロジェクトのUI設計次第です。おすすめパターンを整理すると判断が早くなります。

退避先の候補向いているケースメリットデメリット
先頭グループすでにカテゴリ分けしている追加実装が最小/自然な見た目先頭グループが展開状態だと「選択枠」が見えることもある
ダミーグループ(空タイトル)どの画面でも退避を確実にしたい退避先が固定/最も目立ちにくく設計できる「空のグループ」が存在すること自体に抵抗がある場合がある
編集不可の説明行(ReadOnly項目)ヘルプ行や説明行を持つUIユーザーに意味のある行へ退避できる行が目立つ設計だと逆効果

動作イメージ:Before/Afterで見る「見た目の差」

操作標準の挙動(Before)退避方式(After)
行を選択 → 別コントロールをクリック選択行の強いハイライトが残り続けるグループ行へ選択が移り、強いハイライトが残りにくい
値を編集中 → 別コントロールをクリック編集していた行が強くハイライトされたまま残るEN_KILLFOCUSを拾って退避するため、編集行が残りにくい
グリッド内の別行へ移動(クリック/キー)通常どおり選択が移動する「外へ出たときだけ」退避するため、グリッド内操作は基本そのまま

ダークテーマでの考え方:色を変えるより「強い選択を残さない」

「フォーカスがない選択色だけ変えたい」という発想は自然ですが、CMFCPropertyGridCtrlではそこだけを狙って変えるAPIが見当たりません。一方で、全体配色(背景、テキスト、グループ行など)をまとめて調整するAPIは利用できることがあります。

ただし、全体の色をいじるのは影響範囲が広く、他のコントロールとの整合も難しくなりがちです。そこで本記事の退避方式は、

  • 「不活性選択色」の存在は受け入れる
  • その色が見える状態を作らない

という方針で、最小変更で見た目だけを改善します。ダークテーマで特に効くのは、強いコントラストが残る面積(=行全体の塗り)を画面から消せる点です。

ハマりどころと対策

フォーカス判定は「子ウィンドウ」だけだと不足することがある

プロパティの種類によっては、エディット以外にコンボボックス、チェックボックス、ボタンなど、さまざまなインプレースUIが登場します。通知がEN_KILLFOCUS以外で飛ぶケースもあるため、もし特定のプロパティだけ退避が効かない場合は、次の観点で拡張します。

  • どの通知コード(例:コンボボックスのKILLFOCUS通知)が届くかをデバッガで確認する
  • OnCommandで複数の通知コードを拾えるようにする
  • 「コントロール外へ出た」判定を、IsChildだけでなくウィンドウクラス名等で補強する

「選択が勝手に移った」と感じさせない工夫

退避方式は便利ですが、操作によってはユーザーが「最後に触っていた項目が選択されたままでいてほしい」と感じることもあります。そんなときは次の工夫が効きます。

  • 退避先を“空のダミーグループ”にして、視覚的に選択が消えたように見せる
  • プロパティグリッドが再びフォーカスを得たときに、直前のプロパティへ選択を戻す(必要なら保持する)
  • フォーカス喪失時の退避は「マウス操作で外をクリックしたときだけ」など条件を絞る(誤爆がある場合)

メモリ管理(newしたプロパティ)の扱い

MFCのプロパティグリッドは、AddPropertyに渡したCMFCPropertyGridPropertyをコントロール側が管理する設計になっていることが多い一方、プロジェクトの方針やMFCのバージョン、独自拡張によって扱いが変わる場合もあります。ダミーグループ等を追加する場合は、既存コードの管理方針(所有権)に合わせてください。

まとめ

CMFCPropertyGridCtrlの「フォーカスが外れても選択が強く残る」問題は、CListCtrl/CTreeCtrlのようにスタイルで簡単に切り替えられません。ダークテーマでは特に目立つため、実務では次のように割り切るのが効果的です。

  • 専用APIがない前提で、フォーカス喪失時に選択を目立たない行へ退避する
  • WM_KILLFOCUS(グリッド本体)とEN_KILLFOCUS(インプレース編集)の両方を拾う
  • 退避先は先頭グループ、またはダミーグループにして安定させる

理想は「不活性選択色だけ」を調整することですが、手堅く・壊れにくく・短時間で改善するなら、退避方式は十分実用的です。まずは最小構成で導入し、必要に応じて退避条件や退避先をチューニングしてみてください。

この記事を書いた人

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

コメント

コメントする

目次