.NET Framework 4.8 のWindowsフォーム(WinForms)で、DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE)を使ってタイトルバーのダーク/ライトを切り替えても、フォームをリサイズするまで見た目が変わらないことがあります。本記事では原因を整理し、WM_NCACTIVATE を送って非クライアント領域を即時再描画させる方法を解説します。
症状:DWMWA_USE_IMMERSIVE_DARK_MODE を切り替えても反映が遅い
WinForms(.NET Framework 4.8)でタイトルバーをダークモード/ライトモードに切り替える際、以下のような挙動に遭遇しやすいです。
| 操作 | 期待する結果 | 実際に起きること |
|---|---|---|
| DwmSetWindowAttribute で属性 20(DWMWA_USE_IMMERSIVE_DARK_MODE)を True/False で切り替える | クリック直後にタイトルバーがダーク/ライトに切り替わる | フォームをリサイズするまでタイトルバーの色が変わらない(または一部だけ変わらない) |
| Form.Refresh() / Invalidate() を呼ぶ | 見た目が更新される | クライアント領域は更新されるが、タイトルバー(非クライアント領域)は更新されない |
「幅を +1 して戻す」「最小化→復元」「ウィンドウを一度非表示にして表示し直す」などで更新できる場合がありますが、どれも副作用があり、実装としてスマートではありません。
原因:変更対象が「非クライアント領域」なのに、再描画が発生していない
DWMWA_USE_IMMERSIVE_DARK_MODE が主に影響するのは、フォームのクライアント領域(背景やコントロール)ではなく、タイトルバーや枠線などの非クライアント領域です。WinForms 側の Refresh() や Invalidate() は基本的にクライアント領域の再描画を対象にしているため、DWM の属性を変えても「タイトルバーを描き直すきっかけ」がなければ見た目が更新されません。
フォームをリサイズすると、Windows はウィンドウの枠やタイトルバーを再計算・再描画する必要があるため、結果的にダーク/ライトの変更が反映されます。つまり、問題の本質は「属性は変わっているが、非クライアント領域の再描画トリガーがない」ことです。
- クライアント領域:
OnPaintなどでアプリが描く領域(フォーム内部) - 非クライアント領域:タイトルバー、枠線、システムメニュー、キャプションボタンなど(OS が主に描く)
このため、即時反映させたい場合は「非クライアント領域を再描画させる」メッセージや API を明示的に呼ぶのがポイントになります。
実装前に押さえるチェックポイント
反映遅延に加えて、環境や実装の差で「そもそも効かない」「一部の端末だけ失敗する」ケースもあります。まずは次のポイントを確認すると、切り分けがスムーズです。
| チェック項目 | よくある落とし穴 | 対策 |
|---|---|---|
| 呼び出しタイミング | フォームのハンドル作成前に呼び、期待どおりにならない | Shown / HandleCreated 後に実行する。イベントで切り替える場合も UI スレッドで行う |
| 属性番号(19/20) | Windows のビルド差で 19 が必要な例があり、20 固定だと失敗する環境がある | まず 20 を試し、失敗時に 19 を試すフォールバックを入れる |
| 渡す値の型とサイズ | Boolean のサイズやマーシャリングを意識せず渡して、端末によって挙動が変わる | 0/1 の Int32(4 バイト)で渡すと安定しやすい |
| ウィンドウスタイル | FormBorderStyle=None など、OS のタイトルバーを使っていない(= 変化が見えない) | システム描画のキャプションがあるフォームで確認する。カスタムタイトルバーの場合は別のアプローチが必要 |
| 期待する範囲 | 「フォーム全体がダークになる」と期待する | この属性が変えるのは主にタイトルバー。クライアント領域は BackColor や各コントロールの配色を別途対応する |
特に属性番号の 19/20 と、非クライアント領域の再描画は、現場でハマりやすいポイントです。
DWMWA_USE_IMMERSIVE_DARK_MODE の属性番号(19/20)と互換性の考え方
ネット上のサンプルを見ると、DWMWA_USE_IMMERSIVE_DARK_MODE の属性番号が 19 と書かれているもの、20 と書かれているものが混在しています。これは、Windows のビルドや時期によって「どの番号が有効だったか」の情報が揺れているためです。
実務では、OS 判定で分岐するよりも、20 を試してダメなら 19 を試すというフォールバックが最もトラブルが少ないです(本記事のサンプルもこの方針です)。
| 項目 | おすすめ | 理由 |
|---|---|---|
| 属性番号の決め方 | 20 → 失敗したら 19 | OS バージョン判定が不要で、配布先の端末差にも強い |
| OS バージョンの判定 | 安易に Environment.OSVersion に依存しない | .NET Framework はマニフェスト設定次第で正しいバージョンを返さないことがある |
| 「どの Windows で動くか」 | 対応 OS では最適化、未対応では通常表示のまま | 未対応環境で無理に統一しようとすると保守コストが上がる |
属性の有無は将来も含めて完全に保証されるものではないため、「失敗しても落ちない」「試せるだけ試す」という設計が、業務アプリでは現実的です。
解決策:WM_NCACTIVATE を送ってタイトルバーの再描画を強制する
最もシンプルで副作用が少ない対策は、フォームに WM_NCACTIVATE メッセージを送って、非クライアント領域(タイトルバー)の再描画を促す方法です。
WM_NCACTIVATE は「ウィンドウのアクティブ/非アクティブ状態が変わった」ことを通知するメッセージで、受け取った側はタイトルバーの見た目を更新します。そこで、ダーク/ライト切り替え直後にこのメッセージを非アクティブ→アクティブの順で送ると、タイトルバーの再描画が走り、変更が即時反映されます。
- フォームのサイズをいじる必要がない
- 最小化/復元のようなチラつきが起きにくい
- WinForms でも実装が小さく済む
実装例(VB.NET):DwmSetWindowAttribute の直後に RefreshCaption を呼ぶ
以下は VB.NET での実装例です。属性 20 を試し、失敗した場合に 19 をフォールバックします。最後に WM_NCACTIVATE を送って、タイトルバーを即時更新します。
Imports System.Runtime.InteropServices
Public Class Form1
' Windows 10/11 のタイトルバー(非クライアント領域)をダーク化する属性
Private Const DWMWA_USE_IMMERSIVE_DARK_MODE As Integer = 20
' 一部のWindows 10ビルドでは 19 を使う実装も存在するためフォールバック用に保持
Private Const DWMWA_USE_IMMERSIVE_DARK_MODE_OLD As Integer = 19
Private Const WM_NCACTIVATE As Integer = &H86
<DllImport("dwmapi.dll", PreserveSig:=True)>
Private Shared Function DwmSetWindowAttribute(
hWnd As IntPtr,
dwAttribute As Integer,
ByRef pvAttribute As Integer,
cbAttribute As Integer
) As Integer
End Function
<DllImport("user32.dll", CharSet:=CharSet.Auto)>
Private Shared Function SendMessage(
hWnd As IntPtr,
msg As Integer,
wParam As IntPtr,
lParam As IntPtr
) As IntPtr
End Function
' 例:ライトモードに変更
Private Sub ButtonLight_Click(sender As Object, e As EventArgs) Handles ButtonLight.Click
ApplyImmersiveDarkMode(False)
End Sub
' 例:ダークモードに変更
Private Sub ButtonDark_Click(sender As Object, e As EventArgs) Handles ButtonDark.Click
ApplyImmersiveDarkMode(True)
End Sub
Private Sub ApplyImmersiveDarkMode(enabled As Boolean)
' BOOL を意識して 0/1 の Int32 を渡す(サイズは 4 バイト)
Dim value As Integer = If(enabled, 1, 0)
' まずは 20 を試し、失敗したら 19 を試す
Dim hr As Integer = DwmSetWindowAttribute(Me.Handle, DWMWA_USE_IMMERSIVE_DARK_MODE, value, Marshal.SizeOf(GetType(Integer)))
If hr <> 0 Then
hr = DwmSetWindowAttribute(Me.Handle, DWMWA_USE_IMMERSIVE_DARK_MODE_OLD, value, Marshal.SizeOf(GetType(Integer)))
End If
' 非クライアント領域(タイトルバー)を即時再描画
RefreshCaption()
End Sub
Private Sub RefreshCaption()
' WM_NCACTIVATE を「非アクティブ→アクティブ」と送って
' タイトルバーの描画を強制的に更新させる
SendMessage(Me.Handle, WM_NCACTIVATE, IntPtr.Zero, IntPtr.Zero)
SendMessage(Me.Handle, WM_NCACTIVATE, New IntPtr(1), IntPtr.Zero)
End Sub
End Class
ボタン以外で切り替える場合(設定画面の「適用」ボタン、起動時の初期化など)も、DwmSetWindowAttribute の直後に RefreshCaption() を呼び出すだけで動作します。
なぜ WM_NCACTIVATE で効くのか
ポイントは「OS がタイトルバーを描き直すきっかけ」を作ることです。DwmSetWindowAttribute はあくまでウィンドウ属性を変更する API で、見た目の更新(再描画)を必ず即時に発生させるとは限りません。一方 WM_NCACTIVATE は非クライアント領域の状態変化を通知するため、タイトルバーの再描画処理が実行されやすく、結果としてダーク/ライトの切り替えがすぐ目に見える形で反映されます。
WinForms の Refresh() はクライアント領域中心のため、タイトルバーが変わらない現象が起きやすい、という構図です。
より堅牢にするための実運用ノウハウ
上のサンプルでも多くのケースは解決しますが、業務アプリとして配布する場合は「端末差」「起動直後」「複数フォーム」などを考慮して、もう一段だけ堅牢にしておくと安心です。
戻り値(HRESULT)をチェックして、失敗しても落とさない
DwmSetWindowAttribute は HRESULT を返します。古い Windows や、DWM の振る舞いが異なる環境では失敗する可能性があります。アプリ全体の挙動に影響しないよう、失敗しても例外で落とさず、可能な範囲でフォールバックする設計が現実的です。
- 成功:
S_OK (0) - 失敗例:
E_INVALIDARGなど(属性が未対応、サイズが違う 等)
「必ずダークになること」を要件にするのではなく、「対応 OS では最適化し、未対応では従来表示のまま動く」という方針にするとトラブルが減ります。
ハンドル作成後に適用する(起動時のおすすめ)
起動直後に適用したい場合は、フォームの Shown イベント、または HandleCreated 後に呼ぶのが安全です。Me.Handle に触れると強制的にハンドルが作成されるため「動いているように見える」こともありますが、フォームの生成タイミングや表示順によっては差が出ます。
複数フォーム(ダイアログ含む)に一貫して適用する
メインフォームだけではなく、設定ダイアログや検索ダイアログなど複数のフォームがあるアプリでは、フォーム生成時に共通メソッドで適用するのが管理しやすいです。例えば、基底フォーム(継承元)に ApplyImmersiveDarkMode を置く、または共通モジュールで拡張メソッド相当の関数を用意する、などが現場では扱いやすいです。
フォームが非アクティブの状態で呼ぶ場合は「最後の状態」を元に戻す
ボタン操作で切り替える場合は、フォームが前面(アクティブ)になっていることが多いため、非アクティブ → アクティブの送信で問題になりにくいです。ところが、設定変更をバックグラウンドから反映する、複数フォームを一括で適用する、といった場面では、最後に「アクティブ描画」で終わると非アクティブなのにアクティブ風のタイトルバーに見えることがあります。
その場合は、現在の前面ウィンドウかどうかを判定し、送信順を入れ替えて「最後の状態」を揃えると違和感が出にくくなります。
<DllImport("user32.dll")>
Private Shared Function GetForegroundWindow() As IntPtr
End Function
Private Sub RefreshCaptionKeepingState()
Dim isForeground As Boolean = (GetForegroundWindow() = Me.Handle)
If isForeground Then
' 非アクティブ → アクティブ(最後はアクティブで終える)
SendMessage(Me.Handle, WM_NCACTIVATE, IntPtr.Zero, IntPtr.Zero)
SendMessage(Me.Handle, WM_NCACTIVATE, New IntPtr(1), IntPtr.Zero)
Else
' アクティブ → 非アクティブ(最後は非アクティブで終える)
SendMessage(Me.Handle, WM_NCACTIVATE, New IntPtr(1), IntPtr.Zero)
SendMessage(Me.Handle, WM_NCACTIVATE, IntPtr.Zero, IntPtr.Zero)
End If
End Sub
この調整が面倒、またはアクティブ状態の見た目変化を避けたい場合は、次章の SWP_FRAMECHANGED 方式を検討してください。
別案:SWP_FRAMECHANGED で「枠の再計算」を起こす方法
WM_NCACTIVATE 以外にも、非クライアント領域を更新させる方法があります。代表例が SetWindowPos に SWP_FRAMECHANGED を付けて呼び、枠(フレーム)の再計算と再描画を促すやり方です。アクティブ状態を疑似的に切り替えないため、状況によってはこちらの方が安心なケースもあります。
Imports System.Runtime.InteropServices
Public Class Form1
Private Const SWP_NOSIZE As UInteger = &H1UI
Private Const SWP_NOMOVE As UInteger = &H2UI
Private Const SWP_NOZORDER As UInteger = &H4UI
Private Const SWP_FRAMECHANGED As UInteger = &H20UI
<DllImport("user32.dll", SetLastError:=True)>
Private Shared Function SetWindowPos(
hWnd As IntPtr,
hWndInsertAfter As IntPtr,
X As Integer,
Y As Integer,
cx As Integer,
cy As Integer,
uFlags As UInteger
) As Boolean
End Function
Private Sub RefreshNonClientByFrameChanged()
SetWindowPos(Me.Handle, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE Or SWP_NOSIZE Or SWP_NOZORDER Or SWP_FRAMECHANGED)
End Sub
End Class
ただし、どちらが「正解」というよりは、アプリの UI や更新タイミングに合わせて選び分けるのが実務的です。
| 手法 | 狙い | メリット | 注意点 |
|---|---|---|---|
| WM_NCACTIVATE を送る | タイトルバー状態変化を疑似的に起こす | 実装が最小。リサイズ不要で即時反映しやすい | 呼び出しタイミングによってはアクティブ表示のチラつきが気になることがある |
| SetWindowPos + SWP_FRAMECHANGED | フレーム再計算で非クライアント更新 | アクティブ/非アクティブの見た目を動かしにくい | 状況により更新範囲が広くなり、重い UI ではわずかな負荷が出ることがある |
参考:C# での実装例
検索では C# の例を探している方も多いので、同じ内容を C# でも載せておきます。やっていることは VB.NET と同じで、DwmSetWindowAttribute の直後に WM_NCACTIVATE を送って非クライアント領域を更新します。
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public partial class Form1 : Form
{
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
private const int DWMWA_USE_IMMERSIVE_DARK_MODE_OLD = 19;
private const int WM_NCACTIVATE = 0x0086;
[DllImport("dwmapi.dll", PreserveSig = true)]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
private void ApplyImmersiveDarkMode(bool enabled)
{
int value = enabled ? 1 : 0;
int hr = DwmSetWindowAttribute(this.Handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ref value, sizeof(int));
if (hr != 0)
hr = DwmSetWindowAttribute(this.Handle, DWMWA_USE_IMMERSIVE_DARK_MODE_OLD, ref value, sizeof(int));
// force repaint of title bar
SendMessage(this.Handle, WM_NCACTIVATE, IntPtr.Zero, IntPtr.Zero);
SendMessage(this.Handle, WM_NCACTIVATE, new IntPtr(1), IntPtr.Zero);
}
}
よくある質問
フォーム全体(背景やコントロール)も自動でダークになりますか?
いいえ。DWMWA_USE_IMMERSIVE_DARK_MODE は主にタイトルバーなどの非クライアント領域に影響します。フォーム内部をダークにするには、BackColor や各コントロールの配色、独自のテーマ管理(例:配色テーブルを持つ)など、別の対応が必要です。
呼んでいるのに何も変わりません
次を確認してください。
- フォームにシステムのタイトルバーが存在するか(
FormBorderStyle=Noneの場合は変化しません) - ハンドル作成後に呼んでいるか(
Shown以降で試す) - 属性 20 が失敗していないか(フォールバックで 19 を試す)
- 値のサイズが 4 バイトになっているか(
Int32の 0/1 を渡す)
WM_NCACTIVATE の送信で副作用はありますか?
一般的には軽量で、リサイズのような見た目の揺れも少ない手法です。ただし、アクティブ状態を「見た目だけ」切り替えるメッセージのため、更新タイミングによっては一瞬だけタイトルバーの状態が変わったように見える場合があります。気になる場合は、SWP_FRAMECHANGED 方式に切り替えると症状が出にくいことがあります。
Windows のダークモード設定変更(システム全体のテーマ変更)に追従できますか?
可能です。システム設定の変更を検知したら(例:設定変更通知を受け取ったら)、各フォームに対して DwmSetWindowAttribute と RefreshCaption を再実行します。追従の仕組み自体は別テーマになるため、本記事では割愛しますが、「切り替え後に非クライアント領域を再描画する」という考え方は同じです。
まとめ:属性変更+非クライアント再描画をセットで考える
DWMWA_USE_IMMERSIVE_DARK_MODE を使ったタイトルバーのダーク/ライト切り替えが「リサイズするまで反映されない」場合、原因は属性そのものではなく非クライアント領域の再描画が発生していないことがほとんどです。
DwmSetWindowAttributeで属性を切り替えるWM_NCACTIVATE(またはSWP_FRAMECHANGED)でタイトルバーの再描画を促す
この 2 つをセットにすると、フォームのサイズ変更に頼らずに、WinForms でも違和感の少ない即時反映が実現できます。ダークモード対応はユーザーの目に直結する改善なので、ぜひ自分のアプリの UI に合わせて取り入れてみてください。

コメント