[Delphi] FireMonkey の座標系が DP に変わるけど心配要らないと言ったな。あれは嘘だ。

# はじめに

今回の話は **Windows 限定**です。

# Px to DP

Delphi 11.0 Alexandria から FireMonkey は High DPI 対応により Pixel ベースから DP (Device Independent Pixels) ベースに変わりました。
Android 開発ではおなじみの dp (dip とも)、デバイス独立ピクセルです。

ただ元々 FireMonkey は仮想解像度 (どんな環境でも 96 DPIで描画する) で動作していたためほとんど影響は無いように思われました。

しかし！いくつかのアプリケーションを Alexandria でビルドしてみて気づいてしまったのです。
「あれ？なんか思った位置にフォームが表示されないな…」

# High DPI 対応の恩恵

まずは PxToDP の背景になった High DPI 対応で何がおきたかです。

1. ディスプレイのスケールが 100% 以外でも正しい座標計算が可能になった
1. マルチディスプレイ環境でディスプレイ毎の解像度に対応した
1. Px から Dp になったことで Constraints が追加された

です。

**1.ディスプレイのスケールが 100% 以外でも正しい座標計算が可能になった**

いつかのバージョンからか [TCommonCustomForm](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Forms.TCommonCustomForm) を継承した Form に [SetBounds](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Forms.TCommonCustomForm.SetBounds) 等で座標を与えると正しい位置に表示されていませんでした。

より具体的に言うと、TCommonCustomForm.Position プロパティに DesktopCenter, ScreenCenter, MainFormCenter を指定した時にスケール分ズレて表示されていました。
10.4 Rio までは

```pascal
  // Scale はメインディスプレイのスケールで 1.25 などが入る
  Left := Trunc(Left / Scale);   // Left, Top は Integer なので Trunc で整数化
  Width := Trunc(Width / Scale); // Width, Height も Integer なので Trunc で整数化
```

などとして自分で計算する必要がありました。

さらに致命的だったのが [TPopup](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Controls.TPopup) です。
TPopup は実行時に動的にフォームを生成して、そのフォーム上にコントロールを配置してポップアップぽいコントロールを作るベースとなるコンポーネントです。
例えば、TComboBox で表示されるドロップボックスや、TPopupMenu 等が TPopup をベースに構築されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/bb7b70c6-9891-418e-eaca-66c50b090cdb.png)

これらの TPopup もまた Scale が 100% では無い時に意図しない位置に表示されてしまうという問題がありました。特に ComboBox なんてコントロール本体から右下にずれた位置に表示されるという非常に問題のある動作になっていました(10.4.1 や 10.4.2 で一部修正されています)。
これが Alexandria では修正されています。

**2. マルチディスプレイ環境でディスプレイ毎の解像度に対応した**

ディスプレイ毎にスケールが異なる場合でも正しく表示されるようになりました。

10.4 Rio 以前はメインディスプレイのスケールを使って計算・表示していたため、マルチディスプレイ環境で別のスケールを持つディスプレイにフォームを持って行ったときに、ぼやけたり位置がおかしかったりすることがありました。

Alexandria では、フォームが半分以上表示されているディスプレイのスケールで計算され、正しくキレイに表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/106dd636-3ed7-5d62-696f-b68978e1e519.png)

**3.Px から Dp になったことで Constraints が追加された**

前の記事「[[Delphi][小ネタ] Delphi 11 Alexandria で誰も気づいていない FMX Constraints](https://qiita.com/pik/items/2f34b85c59b3fa5c149d)」で、最後に

> むしろ最初から対応しておいてよ…

と書きました。
が、この機能は DP ベースに変わった事で、追加可能になったのです。

Constraints は Form の Width, Height に制限を課すプロパティです。
しかし、スケールの異なるディスプレイだと実際の大きさが異なってくるため、**MinWidth, MaxWidth などの最小値・最大値はどのスケールで設定された幅や高さなんだ？**という根源的な問題にぶちあたり、実装出来なかったと思われます。

今回 FireMonkey が DP ベースに移行したことでこれが解消され Constraints が実装されたのです。

# DP ベースに移行した結果おきたこと

Form の Left, Top, Width, Height が Single になりました。
プロパティ上では Integer のままですが内部は Single に変わっています。

どういうことかと言うと IFMXWindowService が返す値が TRectF に変わっています。
そして、Left や Top も FBounds という TRectF 型の値を Trunc して返すようになっています。

```pascal:FMX.Formsから抜粋ー11.0Alexandria
function TCommonCustomForm.GetLeft: Integer;
begin
  // (中略)
  Result := Trunc(FBounds.Left); // 整数化して返している
end;
```
ここは 10.4 Rio までは単純に 

```pascal:10.4Rioまで
function TCommonCustomForm.GetLeft: Integer;
begin
  // (中略)
  Result := FLeft; // FLeft は整数型
end;
```

となっていました。

完全に物理ディスプレイのくびきから解き放たれたため、逆に自分で Scale を使っての計算ができなくなりました。
つまり、先に紹介した自前の計算が最早正しく動きません。

```pascal
  Left := Trunc(Left / Scale);   // 小数を切り捨てると意図した位置にならない
  Width := Trunc(Width / Scale); // 小数を切り捨てると意図した幅にならない
```

DP ベースになったため小数を切り捨てたりして**整数化すると逆に座標が合わなくなってしまう**のです。

これがタイトルの「**FireMonkey の座標系が DP に変わるけど心配要らないと言ったな。あれは嘘だ。**」になります。

正しく表示されるように自分で計算していた場合、それらの計算ルーチンを取り払うか再考しないといけなくなりました。


# どこにも書いていない事

Dp ベースに移行したことで、以下の変化が起きています。

1. BoundsF, SetBoundsF の新設
1. Constraints の新設
1. Screen.DisplayFromForm 等の追加
1. FMX.Platform.Win PxToDP, DPToPx の追加

**1.BoundsF, SetBoundsF の新設**

Left, Top, Width, Height が内部では Single で値を保持するようになったため、今までの [Bounds](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Forms.TCommonCustomForm.Bounds) / [SetBounds](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Forms.TCommonCustomForm.SetBounds) では正しく座標を設定できなくなりました。
それらに変わって Single を指定可能な [BoundsF](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Forms.TCommonCustomForm.BoundsF), [SetBoundsF](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Forms.TCommonCustomForm.SetBoundsF) が新設されています。
まだ DocWiki にも載っていませんが FMX.Forms.pas に定義されているのがわかります。

```pascal:FMX.Forms.pasより抜粋BoundsF/SetBoundsFの追加
    /// <summary>Sets new form frame in DP.</summary>
    procedure SetBoundsF(const ALeft, ATop, AWidth, AHeight: Single); overload; virtual;
    /// <summary>Sets new form frame in DP.</summary>
    procedure SetBoundsF(const ARect: TRectF); overload;
    function GetBoundsF: TRectF; virtual;
```

**2.Constaints の新設**

これについては、[以前の記事](https://qiita.com/pik/items/2f34b85c59b3fa5c149d)をご覧下さい。

**3.Screen.DisplayFromForm 等の追加**

各ディスプレイのスケールに対応するため TScreen に

```pascal:FMX.Forms.pasより抜粋
    function DisplayFromPoint(const Point: TPoint): TDisplay; overload;
    function DisplayFromPoint(const Point: TPointF): TDisplay; overload;
    function DisplayFromRect(const Rect: TRect): TDisplay; overload;
    function DisplayFromRect(const Rect: TRectF): TDisplay; overload;
    function DisplayFromForm(const Form: TCommonCustomForm): TDisplay; overload;
    function DisplayFromForm(const Form: TCommonCustomForm; const Point: TPoint): TDisplay; overload;
    function DisplayFromForm(const Form: TCommonCustomForm; const Point: TPointF): TDisplay; overload;
```
といったメソッドが増えました。
座標が位置するディスプレイを返したり、Formが位置するディスプレイを返すメソッド群です。
これらのメソッドを活用すれば自分でディスプレイ毎に何かしようとした場合でも簡単に処理できます。

**4.FMX.Platform.Win PxToDP, DPToPx の追加**

先ほど、自分で Scale を使って計算できなくなったと書きましたが、その代わりに FMX.Platfrom.Win に

```pascal:FMX.Platform.Win.pasより抜粋
function PxToDp(const AValue: TPoint): TPointF;
function DpToPx(const AValue: TPointF): TPoint;
```

が増えています。
FMX.Platform.Win というプラットフォームに依存したパッケージを uses しないとこれらを使えないのは違和感がありますが、Windows と macOS では解像度に関する考え方が違う[^1]ため、PxToDp, DpToPx は Windows でしか必要なく Interface として宣言されずプラットフォーム依存パッケージという形になったと推測されます。

これが生きてくるのは Win32 API などを使った時です。
具体例を挙げると…


```pascal
procedure TForm1.Foo;
begin
  var Pt: TPoint;
  GetCursorPos(Pt); // Win32 API マウスカーソルの座標取得: Pixel 座標
  var PtF := PxToDp(Pt); // Pixel 座標を DP に変換, PtF の型は TPointF
  var Control := ObjectAtPoint(PtF); // マウスカーソル位置にあるコントロールを取得
end;
```

等と OS 側の Pixel ベースの API と相互に座標をやり取りするときです。


# おわりに

自分で座標計算している部分がある方々はソースを見直してください！
座標計算はしていなくても SetBounds で Width, Height を指定している場合なども見直しが必要です！
FireMonkey も最近は落ち着いてきて破壊的な変更がなくなったな～と思っていたらコレです！
ご注意を！

[^1]: macOS は疑似解像度という考え方で実際の解像度ではなく仮想的な解像度を提供しています。ディスプレイの大きさに関わらず画素密度が変わりません。アプリケーション開発者は x3, x2 といった画素密度に応じた各画像を用意しなければなりません。