Delphi のフレームワーク
Delphi には現在以下の3つのフレームワークが載っています。
- RTL (Runtime Library)
- VCL (Visual Component Library)
- FireMonkey (FMX)
このうち RTL は基本的なライブラリで VCL, FMX どちらからも使われます。
VCL は Delphi 1.0 から載っている Windows 専用のライブラリです。
そして、FireMonkey は XE2 から搭載されたマルチプラットフォーム専用のライブラリです。
FMX と略される事も多く、ネームスペースも FMX です。
無料の Delphi Community Edition によって久しぶりに Delphi に帰ってきた方や、もちろんずっと Delphi を使っている人も、マルチプラットフォーム開発環境となった Delphi で様々な OS 向けのアプリケーションを作ってみたい!と思うのでは無いでしょうか?
その時に VCL の考え方のまま FireMonkey を使うと陥りがちな罠を紹介してみたいと思います。
罠1:コントロールの絶対位置指定
VCL では普通にボタンが置きたければボタンをドラッグ&ドロップして置いていました。
例えば、以下のような感じで絶対座標(Left = 240, Top = 24)の様に置いていました。
Align プロパティ
しかし、マルチプラットフォームアプリケーションでは、実際に表示される端末の大きさは実行されるまで判りません。
Windows のデスクトップとして表示されるかも知れませんし、iPhone に表示されるかも知れません。
先ほどの例で Left = 240 に置きましたが横が 240 あるとは限りませんし、逆に横が非常に長いかも知れません。
では、どうするかというと Align プロパティを使います。
次の画像を見てください。
ここでは、Layout1 の中に Button1 を入れて配置しています。
そして、このとき Align は下記の様になっています。
コントロール名 | Align |
---|---|
Layout1 | Top |
Button1 | Center |
Layout1.Align = Top になっていることで、どんな解像度どんなサイズの端末でも Layout1 は常に上部に表示されます。
同様に Button1 は、どんな端末でも必ず Layout1 の中心に表示されます。
このような Align を使ったコントロールの配置を心がけることで、様々な端末に対応させられます。
また、TLayout や TGridLayout 等の Layout 系コントロールは単純に Align を効かせるために存在するコントロールです。これらを上手く使うと下記の様に複雑なレイアウトも比較的簡単に設定できます(ちなみに、このレイアウトは画面下端に「はい」「いいえ」などのボタンを置く時などに使えます)。
Margins プロパティ
また、Margins と Padding を使うと余白を調整できます。
例えば Align = Left でボタンを置くと
こんな風に左側にフィットしつつ親コントロールの高さになります。
もちろん、それで構わない場合もありますが、余白が欲しい時もあります。
そこで、Margins プロパティです。
先ほどの Button1 の Margins に全て 8 を設定してみました。
すると↓こんな風に余白が空きます。
Padding プロパティ
もう一つ Padding プロパティも余白として使えます。
これは Margins とは逆に親が子に対して指定する物です。
下の例では、Button1.Align = Client になっていますが、親領域全部を覆っていません。
これは、Layer1.Padding に 8 を設定した結果、Client 領域が 8 ずつ小さくなっているためです。
DocWiki の画像が判りやすいので貼っておきます。
位置指定のまとめ
Layout, Margins, Padding を使うと柔軟にレイアウトを調整できるので、絶対位置指定をせずとも意図に沿ったデザインが可能です。
罠2:コントロール毎のフォント指定
Delphi XE4 から FMX.Controls.TTextControl を継承するコントロールには TextSettings プロパティが追加されました。
TextSettings プロパティ
TextSettings プロパティを使うと、フォントのファミリ、大きさ、スタイル、色、そして、テキストの配置を制御できます。
StyledControl の場合は、StyledSettings プロパティで TextSettings のどの値を有効にするか、を指定します。
例えば、TextSettings.Font.Size を有効にしたい場合は
StyledSettings := StyledSettings - [TStyledSetting.FontSize];
とします。
TextSettings で全部指定するからスタイル全部要らない!と
StyledSettings := [];
こんな風にすることもできます。
…ですが、本当にそれでいいのでしょうか?
このアプリケーションが動作する端末のデフォルトの背景色は白系でしょうか?黒系でしょうか?
そのフォントサイズで端末に適した大きさになっているのでしょうか…?
Style
スタイルは見た目が大きく変わるため、そこばかりフォーカスされますが、Style の本来の目的は統一された見た目の提供です。
どういうことかというと Style によって背景が固定されるため、そこに表示する文字の色が決まる、ということです。
例えば、白系の背景のスタイルでは黒系の文字が、黒系の背景のスタイルでは白系の文字が指定されています。
FireMonkey のコントロールはほとんどが TStyledControl から派生しています。
つまり、ここから読み取れるのはコントロールには Style を適用すべき、ということです。
これは近年のウェブと非常に似ているアーキテクチャです。
つまり HTML で構造を CSS で見た目を、と機能を分離することでデザインを独立させています。
FireMonkey も同じ思想で設計されています。
でも「このコントロールのフォントカラーは赤にしたい!」という思いもあると思います。
ですが、その「赤」は何のために使うのでしょうか?
注意(アクセントカラー)を示すためでしょうか?
↓これは、Ubuntu っぽい見た目のスタイルです。
この様な背景ではアクセントカラーは赤では無いことが判ります。
そのため、どのスタイルを使うのかを決めて、そのスタイルにアクセントカラー用のスタイル、例えば「accentcolorlabel」といったスタイルを作り、そのスタイルで映える色を指定し、それを Label に適用してやる、という風にしてアクセントカラーを実現します。
TextSettings で安易に変えてしまうと全部の OS で適切な見た目になるとは限りません。
FMX.Objects
とはいえ、ここには白い矩形領域の中に赤で文字を出す必要があるんだ!ということもあるかもしれません。
その場合、もちろんスタイルでやっても良いのですが、TStyledControl を継承していないコントロールを使うのも手です。
例えば、下の設計画面は TRectangle の上に TText を載せています。どちらも StyledControl ではありません。
フォントのまとめ
基本的には Style を使います。
もしも固定された配色やサイズがある場合は TStyledControl を継承していない TRectangle や TText を使います。
罠3:コンテナコントロール
VCL ではコンテナとなれるコントロールは決まっていました。
それは、Windows のコントロールがベースにあるからです。
FireMonkey では全てのコントロールが親となれるので、こんなことも可能です。
Edit1 の上に Button1 を載せて Button1.Align = Right にし、更にその上に TPath を載せて SVG を描画しています(なお、ちゃんとやるにはスタイルを作ってテキストエリアが右の画像に被らないようにしてあげないといけません)。
ただし、このままではボタンが押せません。
HitTest プロパティ
TPath には HitTest プロパティが存在します。
HitTest プロパティはマウスクリックやタップイベントをこのコントロールが受け取るかどうかを示します。
つまり、TPath の HitTest が True の場合 Button ではなく TPath がマウスクリックを受け取ってしまうため、ボタンが押せないのです。
そこで、TPath の HitTest を False に指定してあげると、Button にイベントが発行されるようになります。
注意
ただし、WebBrowser や MapView など一部のコントロールの上に物は置けません。
WebBrowser や MapView はネイティブコントロールと呼ばれるコントロールで各 OS の要素をそのまま表示します。そのため、FireMonkey では、その上にコントロールを載せられません(下図は過去スライドより抜粋)
OS から見ると FireMonkey のコントロールは「ただの絵」であることに注意してください(FireMonkey は OS のコントロールを利用せずコントロールを自分で描画しています)
コンテナのまとめ
全てのコントロールがコンテナになれるので、組み合わせるだけで様々なコントロールをその場で作り出せます。
VCL では良く自作のコントロールを作っていましたが、コレを知っていると自分でコントロールを作る頻度は劇的に下がるのでは無いでしょうか?。
罠4:非同期ダイアログ
VCL ではダイアログと言えば Modal ダイアログで同期的でした。
例えば
procedure TForm1.Butto1Click(Sender: TObject);
begin
ShowMessage('Clicked!');
Button1.Caption := 'Clicked !'; // ここはダイアログが閉じるまで呼ばれない!
end;
といった感じでダイアログ表示中は処理が次に進みませんでした。
しかし FireMonkey では、そうもいきません。
というのは Android や iOS では処理をスタックさせると OS がフリーズしていると判断してアプリを落としてしまうためです。
有名なのは Android の ANR (Application Not Responding) です。
これが発生してしまうと「アプリケーションは応答していません」とユーザーに表示され落とされてしまいます。
そのため、FireMonkey では非同期ダイアログの仕組みが組み込まれています。
ただ、この部分は少し面倒です。
というのは同期的にダイアログを表示する OS と非同期的にダイアログを表示する OS が存在するためです。
OS | 同期的? |
---|---|
Windows | Yes |
macOS | Yes |
iOS | No |
Android | No |
同期・非同期ダイアログの使い分けが必要になります。
使い分けるために、IFMXDialogService インターフェースを使う方法などがありますが一番簡単なのは TDialogService を使う方法です。
TDialogService は内部で同期型と非同期型のダイアログを自動的に使い分けてくれて、しかも、それらを統一的に呼び出せます。
uses
FMX.DialogService;
procedure TForm1.Button1Click(Sender: TObject);
begin
TDialogService.MessageDialog(
'メッセージ',
TMsgDlgType.mtConfirmation, // ダイアログのタイプ
mbOKCancel, // 表示するボタンの集合
TMsgDlgBtn.mbCancel, // デフォルトでフォーカスを持っているボタン
0, // HelpContext
procedure(const AResult: TModalResult) // 応答を受け取るハンドラー
begin
if AResult = mrOk then
Button1.Text := 'Clicked!';
end
);
end;
動かして Button1 を押すと、下図のようにダイアログが表示されます。
そして、OK ボタンを押すと、
ハンドラが呼び出され、Button1 のキャプションが変わりました。
この方法は Windows でも、他の OS でも同じように使えます。
ダイアログのまとめ
TDialogService を使うと、同期非同期を自動的に判断してくれて、かつ、ダイアログが閉じたときの処理も一元化できます。
今まで同期ダイアログを使っていて、ダイアログが閉じたときに走るようになっていた処理を、ハンドラーの中に移すだけ、という手軽さで同期ダイアログ非同期ダイアログの両方に対応できてしまいます。
罠5:Windows 文化の持ち込み
ダメ、絶対。
Windows では、こうだったから Android でも同じようにしよう!という考え方は非常に危険です。
例えば、Android や iOS には TOpenDialog, TSaveDialog などがありません。
それは、モバイル OS では、そもそもファイルへのアクセスが制限されているため、特定のディレクトリにしかアクセス出来なかったり、写真はカメラロールに保存する、など機能が制限されているためです。
また、Android は単機能のアプリを沢山作ってそれを Intent で繋げていく、という面白い機構を持っている OS です。
Android では「画像を開く」という処理をする場合 Windows のように TOpenPictureDialog を使うわけではなく「画像を開く」アプリを起動するのです。
procedure StartIntent;
var
Intent: JIntent;
begin
Intent := TJIntent.Create;
Intent
.setAction(TJIntent.JavaClass.ACTION_OPEN_DOCUMENT)
.addCategory(TJIntent.JavaClass.CATEGORY_OPENABLE)
.setType('image/*');
TAndroidHelper.Activity.startActivity(intent);
end;
ですが、OS 毎にこういった処理を書くと各 OS 専用のコードができてしまい面倒です。
では、どうするかといえば、標準アクションやコンポーネントを使います。
例えば、標準アクションには TTakePhotoFromLibraryAction(画像を開くアプリを呼び出し画像を取得するアクション)や TShowShareSheetAction(ShareSheet / Application Chooser を呼び出してデータを渡す), TPhoneCallAction(電話を掛ける)といったアクションが用意されています。
また、TAddressBook(アドレス帳)や TMapView(標準の地図を表示する)といったコントロールも用意されています。
これらを使えばプラットフォームに依存せず、これらの機能を利用できます。
Windows の文化のまとめ
各 OS の文化を知って、郷に入れば郷に従え、の精神を忘れないようにしたいですね。
まとめ
VCL と FireMonkey はコンポーネントの名前が似ているものも多く誤解されがちなのですが、同根のライブラリではありません。
VCL と FireMonkey は全く別のライブラリです。
そのため、それぞれの思想にあった実装が求められます。
FireMonkey でアプリケーションを組む場合「VCL ではどうだっけ?」と考えない方が作業がスムーズに進むと思います。