VCLのスタイルをFMXに変換し，リソースファイルを小さくしてダークモード切替に追随するアプリを作る



本投稿は [Delphi Advent Calendar 2022 #10](https://qiita.com/advent-calendar/2022/delphi) の記事です。

# はじめに
Delphi 11.2では，GetItを使うとFMXで使うスタイルを手に入れられます。
しかし，まだWindows11のデーマがまだありません。VCLにはライトとダークのスタイルがあるのに，，
そこで，VCLのスタイルを，FMXのスタイルに変換します。
また，FMXのスタイルファイルは，テキストベースで，イメージがテキストにエンコードされています。
これをリソースとして使うとEXEファイルのサイズが大きくなってしまいます。
そこで，FMXのスタイルデザイナでFireMonkeyスタイルバイナリファイル(*.fsf)として保存し，リソースに追加したいと思います。

# 環境
Windows 10 Pro 21H2 / Delphi 11.2 Alexandria

:::note warn
Delphi Community Editionではテストしていません。
:::

# 作業の手順
手順は以下のようになります。
1. Windows11 Modern VCLスタイルをGetItで導入する。
1. ビットマップスタイルデザイナでWindows11 Modern VCLスタイルをFMXスタイルに変換する。
1. ダミーのFMXプロジェクトを作り，FireMonkeyスタイルバイナリを生成する
1. スタイルを確認する。
1. ダークモード切替するプロジェクトを作成する。

では始めましょう。
## 1. Windows11 Modern VCLスタイルをGetItで導入する。
DelphiのIDEを起動し，**ツール**メニューの**GetItパッケージマネージャ**を選択します。
表示された**GetItパッケージマネージャ**の左ペインを以下のように設定します。
　**絞り込み:** すべて
　**ソート順:** 名前
　**カテゴリ:** Styles
すると，**VCL Style - Windows11Dark 1.0**と**VCL Style - Windows11Dark 1.0**
が表示されますのでこの２つを指示に従ってインストールします。
VCL スタイルは，パブリックのドキュメントの Embarcadero\Studio\22.0\Styles にインストールされます。

## 2. ビットマップスタイルデザイナでWindows11 Modern VCLスタイルをFMXスタイルに変換する。
DelphiのIDEを起動し，**ツール**メニューの**ビットマップスタイルデザイナ**を選択します。
![Snap0001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/3faecf54-acaf-e90f-eac5-9f51ab4996a2.png)
するとビットマップスタイルデザイナが起動します。
![Snap0002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/cc89a81c-800e-04e8-bd35-4df435702e2e.png)
**ファイル**メニューの**開く**を選択して，パブリックのドキュメントの Embarcadero\Studio\22.0\Styles を開きます。
![Snap0003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/7c6f8aa8-db8e-3b69-c9e9-975cf2109d9d.png)
VCLのWindows11のスタイルを選択します。モードとファイル名は以下の関係になります。
| モード    | ファイル名               |
|:---------:|:-----------------------:|
|ライトモード|Windows11_Modern_Light.vsf|
|ダークモード|Windows11_Modern_Dark.vsf |

読み込んだ後，オブジェクトペインでImagesをクリックし，対象の画像をエクスポートして，画像を編集し更新することができます。
(試しに緑色で表示されるプログレスバーを青くしてみたところうまくいきました。)
**ファイル**メニューの**名前を付けて保存**を選択します。
![Snap0004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/443abff2-edf3-4170-f2c5-7966a63211ae.png)
ファイルの種類で**FireMonkeyスタイル(*.style)を選択して保存します。ファイル名は以下のようにしておくと分かりやすいです。
![Snap0017.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/f47fef89-3c5d-e5e5-7213-8ac9d933a5f0.png)
| モード    | ファイル名  |
|:---------:|:----------:|
|ライトモード|Win11L.style|
|ダークモード|Win11D.style|

## 3. ダミーのFMXプロジェクトを作り，FireMonkeyスタイルバイナリを生成する。
FMXスタイルデザイナは，FMXフォームのStyleBookからでないと起動できません。
上記の作業でIDEが起動したままですので，IDEの**ファイル**メニューの**新規作成**ー**マルチデバイスアプリケーション**を選択します。
![Snap0005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/fc8668ef-05e4-1f29-ae32-b02cfc4cbc31.png)
**マルチデバイスアプリケーション**ダイアログの空のアプリケーションを選択し**OKボタン**をクリックします。
![Snap0006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/9115b0fd-34e6-652a-a1b4-2ab2021252f8.png)
フォームにStyleBookを張り付け，張り付けられたStyleBook1のアイコンをダブルクリックして，FMXスタイルデザイナを起動します。
![Snap0007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/3516472a-a92b-523c-dfbf-0bb5f4ef7fa1.png)
スタイルデザイナタブの**開く**ボタンをクリックして，前の作業で保存したFMXスタイルファイルを指定します。
![Snap0008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/236e7e36-4ee5-dd36-d1c5-a7d90b633e8c.png)
スタイルデザイナタブの**保存**ボタンをクリックします。
![Snap0009.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/3cd69135-d407-e7e4-1550-81b07387d415.png)
ファイルの種類でFireMonkeyスタイルバイナリ(*.fsf)を選択して保存します。以下のように拡張子は必ず書いてください。
![Snap0016.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/ab958c86-026a-c3e2-e2c6-73003901e948.png)
| モード    | ファイル名  |
|:---------:|:---------:|
|ライトモード|Win11L.fsf|
|ダークモード|Win11D.fsf|

FMXスタイルデザイナを閉じます。そのとき，変更を反映させてください。

## 4. スタイルを確認する。
Form1のオブジェクトインスペクタで，StyleBookに変更を反映させたStyleBook1を指定すると，編集画面でフォームの外観以外のコントロールのスタイルが確認できます。
![Snap0012.png 編集画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/c204a243-3eec-261c-3aaf-82698513c944.png)
![Snap0013.png 実行画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/54e27ea8-7493-a9e6-dc03-9e39f0a257e7.png)
このような仮のプロジェクトファイルを使いまわせば，FMXフォームのスタイルを確認しながらFireMonkeyスタイルバイナリに変換できるでしょう。
確認できたら，プロジェクトを閉じます。

## 5. ダークモード切替するプロジェクトを作成する。

[昨年のアドベントの記事](https://qiita.com/p_kato/items/c11cc733803a396884f9)で紹介したuThemeFMX.pasユニットを使って，ダークモード切替に津出生するアプリを作ります。
IDEの**ファイル**メニューの**新規作成**－**マルチデバイスアプリケーション**を選択します。
IDEの**ファイル**メニューの**新規作成**－**ユニット**を選択し，以下のコードをコピー＆ペーストします。
Alexandriaで以前のコードがエラーを出していましたので修正しました。(元のコードも修正しています。)

```uThemeFMX.pas
unit uThemeFMX;

interface

uses
  FMX.Forms, FMX.Platform.Win,
  Winapi.Windows,Winapi.Messages;

type
  TisATF=(fdAuto,fdTrue,fdFalse);           // 自動と強制設定を管理する

type
  TColorF=record
  case Cardinal of
    0: (C: Cardinal);
    1: (R, G, B, A: System.Byte);
  end;

var
  isWindows10:boolean; // Windows10か？
  AccentColor:TColorF; // アクセントカラー

function isLightTheme:boolean;              // LightThemeかDarkThemeか
procedure isLightThemeSet(AisATF:TisATF);

function ThemetFormWndProc(var uMsg:DWORD;var wParam:WPARAM):boolean; // テーマ更新の監視

implementation

uses
  System.SysUtils,
  System.Win.Registry;


procedure isWindows10Set; // Windows10か？
begin
  isWindows10:=CheckWin32Version(10, 0);
end;

function SysAccentColor:TColorF; // アクセントカラー
var
  Registry: TRegistry;
  C:TColorF;
begin
   Result.C:=$00000000;
   Registry := TRegistry.Create(KEY_READ);
   try
     Registry.RootKey := HKEY_CURRENT_USER;
     Registry.OpenKey('Software\Microsoft\Windows\DWM', False);
     C.C :=(Registry.ReadInteger('AccentColor') or $FF000000);
     Result.R:=C.B;
     Result.G:=C.G;
     Result.B:=C.R;
     Result.A:=$FF;
   finally
     Registry.Free;
   end;
end;

procedure SysAccentColorUpdate(wParam:NativeUInt);
var
  d:TColorF;
begin
  d.C:=wParam or $FF000000;
  AccentColor.R:=d.R;
  AccentColor.G:=d.G;
  AccentColor.B:=d.B;
  AccentColor.A:=$FF;
end;

function isSysLightTheme:boolean; // テーマは Light / Dark か？
var
 Registry: TRegistry;
begin
  Result:=True;
  Registry := TRegistry.Create(KEY_READ);
  try
    Registry.RootKey:= HKEY_CURRENT_USER;
    if Registry.OpenKey('Software\Microsoft\Windows\CurrentVersion\Themes\Personalize', False) then begin
      Result := (Registry.ReadInteger('AppsUseLightTheme')=1);
    end;
  finally
    Registry.Free;
  end;
end;

var
  FisLightTheme:integer=1;

function isLightTheme:boolean;
begin
  Result:=((FisLightTheme and 1)=1)or(not isWindows10); // Windows7は常にLightTheme
end;

procedure isLightThemeAutoSet;
begin
  if isSysLightTheme then begin
    FisLightTheme:=1;
  end else begin
    FisLightTheme:=0;
  end;
end;

procedure isLightThemeSet(AisATF:TisATF); // テーマをセットする
begin
  case AisATF of
    fdAuto: isLightThemeAutoSet;
    fdTrue: FisLightTheme:=3;
   fdFalse: FisLightTheme:=2;
  end;
end;

procedure isLightThemeUpdate; // 自動更新なら更新する
begin
  if (FisLightTheme and 2)=0 then isLightThemeAutoSet;
end;

function AccentColorWndProc(var uMsg:DWORD;wParam:WPARAM):boolean; // アクセントカラーの監視
var
  ba:boolean;
begin
  ba:=uMsg = WM_DWMCOLORIZATIONCOLORCHANGED;
  if ba then begin
    SysAccentColorUpdate(wParam);  // アクセントカラーの変更
  end;
  Result:=ba;
end;

function ThemetFormWndProc(var uMsg:DWORD;var wParam:WPARAM):boolean; // テーマ更新の監視
// 親フォームのWndProcで監視する
var
  ba,bt:boolean;
begin
  ba:=AccentColorWndProc(uMsg,wParam); // アクセントカラーの監視
  bt:=uMsg = WM_SETTINGCHANGE;
  if bt then begin
    isLightThemeUpdate; // テーマ カラーの変更
  end;
  Result:=ba or bt;
end;

initialization
  isWindows10Set; // isWindwos10をセット
  AccentColor:=SysAccentColor; // アクセントカラー
  isLightThemeUpdate; // テーマ カラーの変更
end.
```

uThemeFMX.pasという名前で保存します。

切り替えるFMXスタイルバイナリをリソースに読み込みます。**プロジェクト**メニューから**リソースと画像**を選びます。
![Snap0014.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/7105d447-3791-7270-0fb6-ab196c7b5be4.png)
以下のようにスタイルバイナリを追加し，識別子を入力します
| スタイル | 識別子 |
|:-------:|:------:|
|Win11L.fsf|Win11L |
|Win11D.fsf|Win11D |

![Snap0015.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33902/570faa40-6b9f-4423-3caa-10f4dded860e.png)

Form1にRectangleコンポーネントを追加し，Unit1を以下のように編集します。
```unit1.pas
unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.StdCtrls,
  FMX.Edit, FMX.Controls.Presentation, FMX.Objects;

type
  TForm1 = class(TForm)
    ProgressBar1: TProgressBar;
    Edit1: TEdit;
    Button1: TButton;
    Rectangle1: TRectangle;
    StyleBook1: TStyleBook;
    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
  private
    { private 宣言 }
  public
    { public 宣言 }
    procedure ThemeUpdate;
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

uses
  FMX.Styles, FMX.Platform.Win,
  Winapi.Windows,
  uThemeFMX;

var
  WndProc:Pointer;

procedure ThemeUpdate(AForm:TForm1);
begin
  AForm.ThemeUpdate;
end;

function ThemeWndProc( hWnd: HWND; uMsg: DWORD; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
begin
  if ThemetFormWndProc(uMsg,wParam) then begin
    ThemeUpdate(Form1);
  end;
  Result := CallWindowProc(WndProc, hWnd, uMsg, wParam, lParam);
end;

function AddThemeWndProc(const AForm: TCommonCustomForm): Boolean;
var
  Wnd: HWND;
begin
  Wnd := FormToHWND(AForm);
  WndProc := Pointer(SetWindowLong(Wnd, GWL_WNDPROC, Integer(@ThemeWndProc)));
  Exit(WndProc <> nil);
end;

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  AddThemeWndProc(Self);
end;

procedure TForm1.FormShow(Sender: TObject);
begin
  ThemeUpdate;
end;

procedure TForm1.ThemeUpdate;
var
  FMXStyle: TFmxObject;
begin
  if isLightTheme then begin
    FMXStyle:= TStyleStreaming.LoadFromResource(HInstance,'Win11L',RT_RCDATA);
  end else begin
    FMXStyle:= TStyleStreaming.LoadFromResource(HInstance,'Win11D',RT_RCDATA);
  end;
  TStyleManager.SetStyle(FMXStyle);
  Rectangle1.Fill.Color:=AccentColor.C;
  Invalidate;
end;

end.
```

動作中でもダークモード切替が動くようにしていますが，少し不安定な気がします。単体では安定して動作しているのですが，，
デーマ切換のメッセージに反応する部分は実装しないで，起動時の未反応するようにしたほうが良いのかもしれません。

ファイルサイズは，styleファイルをリソースとして使ったときよりfsfファイルをリソースとして使った方が約600KB小さくなりました。
1つのスタイル当たり300KBの減量です。今回はVCLのスタイルを使ったためイメージファイルが大きかったのですが，もともとFMXスタイルだったものであれば，その違いはもっと小さくなるのではないかと思います。

# 謝辞(引用元および参考文献)
- [FMXでWindows10のダークモード(ダークテーマ)の切替に追随するアプリを作る - Qiita p_kato](https://qiita.com/p_kato/items/c11cc733803a396884f9)
- [Delphi11で追加されたVCLのWindows11スタイルを利用する - Qiita p_kato](https://qiita.com/p_kato/items/3bb93616d28bc415f585)
- [ビットマップ スタイル デザイナ - docwiki](https://docwiki.embarcadero.com/RADStudio/Alexandria/ja/%EF%BC%BB%E3%83%93%E3%83%83%E3%83%88%E3%83%9E%E3%83%83%E3%83%97_%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB_%E3%83%87%E3%82%B6%E3%82%A4%E3%83%8A%EF%BC%BD)
- [VCL スタイルから FireMonkey スタイルへの変換 - docwiki](https://docwiki.embarcadero.com/RADStudio/Alexandria/ja/VCL_%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E3%81%8B%E3%82%89_FireMonkey_%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E3%81%B8%E3%81%AE%E5%A4%89%E6%8F%9B)
- [スタイル デザイナ - docwiki](https://docwiki.embarcadero.com/RADStudio/Alexandria/ja/FireMonkey_%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB_%E3%83%87%E3%82%B6%E3%82%A4%E3%83%8A)

- [Windows 10 でUIをダークテーマ (ダークモード) に変更する (Windows 10 Tips) - iPentec](https://www.ipentec.com/document/windows-windows-10-change-dark-theme)
- [クラシック デスクトップ アプリの Windows テーマ追従 - grabacr.nét PaaS (Patchouli as a Shachiku)](http://grabacr.net/archives/6671)
- [簡単にUIの印象を変える方法 - Styleの適用 - FireMonkey と VCL (Japan) - Embarcadero Blogs](https://community.idera.com/developer-tools/b/blog/posts/ui---style---firemonkey-vcl-japan)
- [Delphi 10.1 BerlinのFireMonkeyアプリケーションでスタイルをリソースから読み込んで適用する - 山本隆の開発日誌](https://www.gesource.jp/weblog/?p=7556)
- [FireMonkey と Windows - Qiita - @pik](https://qiita.com/pik/items/b184f5ffb34576ed15dc) 

貴重な情報を提供してくださった皆様，ありがとうございます。感謝します。