2013年7月1日月曜日

constructor 制約について

Facebook の Delphi Talks で「型制約について、疑問があります」というスレッドが立ちました。
内容は「ジェネリクス型で型指定するときに、レコード型と値型と文字列型、のみ指定することは可能か?」という話でした。
これについては、スレッド中で「できない」と、結論が出ました。
ただ、このスレッド中で「constructor 制約がついたジェネリクス型に文字列型や値型が渡せる」という事が判りました(僕が知りました)。
と、いうことで、ちょっと調べてみました。

constructor 制約とは、下記のようにジェネリクスの型指定に constructor と書く事で定義される制約です。

type
TConstructorConstraint<T: constructor> = class
end;

具体的には、この制約を課されると「引数無しの Create を持った型しか指定できない」という制約です。
この制約の最大の特徴は T が 「どんな型か全く知らなくても」インスタンスを 生成できる、ということです。
これによって、何も知らなくても有効なインスタンスの存在を保証できます。

……と、まあ原義はさておき、上述の様にプリミティブ型である String 型や Integer 型を指定できるのです。
当然プリミティブな型なので Create なんてコンストラクタメソッドを持っているわけがありません。
これはどういった事でしょうか。
これを検証するために、下記のコードを書きました。

unit Unit1;
 
interface
 
type
// interface 部に定義すると全員に見える
// 引数無しの Create は持っていないクラス
TBar = class
public
constructor Create(const iDummy: Integer); reintroduce;
end;
 
procedure Test;
 
implementation
 
uses
System.Rtti;
 
type
// constructor 制約を課したジェネリック型クラス
TConstructorConstraint<T: constructor> = class
private
FValue: T;
public
constructor Create; reintroduce;
function ToString: String; override;
end;
 
// 引数無しの Create を持つクラス
TFoo = class
public
constructor Create; reintroduce;
end;
 
// 動的配列型
TStringDynArray = array of String;
 
// 集合型
TFactor = (Windows, MacOSX, Android, iOS, WindowsPhone);
TSet = set of TFactor;
 
{ TConstructorConstraint<T> }
 
constructor TConstructorConstraint<T>.Create;
begin
inherited Create;
 
// T の型は知らないけど Create を呼び出せる!
FValue := T.Create;
end;
 
// T の型情報を出力する
function TConstructorConstraint<T>.ToString: String;
var
Rtti: TRttiContext;
Field: TRttiField;
FieldType: TRttiType;
begin
Result := '';
 
Rtti := TRttiContext.Create;
try
Field := Rtti.GetType(ClassInfo).GetField('FValue');
FieldType := Field.FieldType;
 
Result := Field.Name + ': ' + FieldType.ToString + ';';
 
if (FieldType.IsPublicType) then
Result := Result + ' Public;';
 
if (FieldType.IsManaged) then
Result := Result + ' Manged;';
 
if (FieldType.IsInstance) then
Result := Result + ' Instance;';
 
if (FieldType.IsOrdinal) then
Result := Result + ' Ordinal;';
 
// Record は constructor 制約では指定できないので、ここは表示されない
if (FieldType.IsRecord) then
Result := Result + ' Record;';
 
if (FieldType.IsSet) then
Result := Result + ' Set;';
finally
Rtti.Free;
end;
end;
 
{ TFoo }
 
constructor TFoo.Create;
begin
inherited Create;
 
// 生成時に表示される
Writeln('TFoo Created !');
end;
 
{ TBar }
 
constructor TBar.Create(const iDummy: Integer);
begin
inherited Create;
 
// 引数無しの Create ではないため、表示されない
Writeln('TBar Created !');
end;
 
// 生成して情報を出力する
procedure Test;
var
Foo: TConstructorConstraint<TFoo>;
Bar: TConstructorConstraint<TBar>;
Str: TConstructorConstraint<String>;
Int: TConstructorConstraint<Integer>;
Ary: TConstructorConstraint<TStringDynArray>;
Sets: TConstructorConstraint<TSet>;
begin
Foo := nil;
Bar := nil;
Str := nil;
Int := nil;
Ary := nil;
Sets := nil;
try
Foo := TConstructorConstraint<TFoo>.Create;
Bar := TConstructorConstraint<TBar>.Create;
Str := TConstructorConstraint<String>.Create;
Int := TConstructorConstraint<Integer>.Create;
Ary := TConstructorConstraint<TStringDynArray>.Create;
Sets := TConstructorConstraint<TSet>.Create;
 
Writeln(Foo.ToString);
Writeln(Bar.ToString);
Writeln(Str.ToString);
Writeln(Int.ToString);
Writeln(Ary.ToString);
Writeln(Sets.ToString);
 
Readln;
finally
Sets.Free;
Ary.Free;
Int.Free;
Str.Free;
Bar.Free;
Foo.Free;
end;
end;
 
end.

様々なジェネリック型を定義して、生成、その情報を出力、とするだけのプログラムです。
ここでは、クラス型、文字列型、整数型、動的配列型、集合型、を指定してみました。
重要なのは、クラス型以外、Create なんてメソッドは持っていない!ということです。

では、このプログラムを動かしてみると…

TFoo Created !
FValue: TFoo; Instance;
FValue: TBar; Public; Instance;
FValue: string; Public; Manged;
FValue: Integer; Public; Ordinal;
FValue: TStringDynArray; Manged;
FValue: TSet; Set;

こんな風になりました!
TFoo, TBar はクラスなので "Instance" と表示されています。
また、TBar は、interface 部で定義されているので公開されている型 "Public" と表示されています。
そして、string と動的配列型は "Managed" と表示されています。これはコンパイラがその型の生成と廃棄を担っていることを示します。
つまり、string と動的配列型は、本当は管理されたメモリを持つ参照型であるため、このように表示されています。
また、Integer は Ordinal…順序型, 集合型は Set と出ました。これはそれぞれの型そのものですね。

ということで、本当に値型や文字列型が Create というメソッドで生成されてしまいました。
本来、そのようなメソッドを持たない型にも関わらず、です。

では、これらの型の生成は実際にはどのようなコードになっているのでしょうか?
それを見るために CPU ビューで逆アセンブルされたコードを見てみました。

// TFoo の生成
Project1.dpr.28: FValue := T.Create;
00407D7A B201             mov dl,$01
00407D7C A158724000       mov eax,[$00407258]
00407D81 E85AF6FFFF       call TFoo.Create      // 初期化コード
00407D86 8B55FC           mov edx,[ebp-$04]
00407D89 894204           mov [edx+$04],eax


// TBar の生成
Project1.dpr.28: FValue := T.Create;
00407DCE B201             mov dl,$01
00407DD0 A114734000       mov eax,[$00407314]
00407DD5 E8BACDFFFF       call TObject.Create   // 初期化コード
00407DDA 8B55FC           mov edx,[ebp-$04]
00407DDD 894204           mov [edx+$04],eax


// 文字列の生成
Project1.dpr.28: FValue := T.Create;
00407E22 8B45FC           mov eax,[ebp-$04]
00407E25 83C004           add eax,$04
00407E28 E843DDFFFF       call @UStrClr         // 初期化コード


// 整数型(順序型)の生成
Project1.dpr.28: FValue := T.Create;
00407E6E 8B45FC           mov eax,[ebp-$04]
00407E71 33D2             xor edx,edx           // 初期化コード
00407E73 895004           mov [eax+$04],edx


// 動的配列の生成
Unit1.pas.50: FValue := T.Create;
004D50F6 8B45FC           mov eax,[ebp-$04]
004D50F9 83C004           add eax,$04
004D50FC 8B1500394D00     mov edx,[$004d3900]
004D5102 E87D68F3FF       call @DynArrayClear   // 初期化コード


// 集合の生成
Unit1.pas.50: FValue := T.Create;
004D540E 8B45FC           mov eax,[ebp-$04]
004D5411 8A153C544D00     mov dl,[$004d543c]    // 初期化コード?
004D5417 885004           mov [eax+$04],dl

なんと、コンパイラマジックによって自動的にそれぞれの型の初期化コードが走っていました。
文字列や動的配列であれば、それらの内容をクリアする関数をコールしています。
順序型は 0 が代入されています(同じレジスタを xor するとレジスタの内容は 0 になる)。
集合型については、ちょっと判りませんが……
また、TFoo, TBaz については、それぞれ「引数無しの Create」が呼ばれています。
TFoo は定義されているので良いですが TBaz の場合基底クラスである TObject の Create が呼ばれました。
クラス型は全て TObject から派生しているので、constructor で制限を掛けても全てのクラスが生成できてしまいます。その Create が有効如何に関わらずです。
これは、constructor 制約の意義に関わる注意点です……実質意味が無い気がします……

ということで、 constructor 制約で実際に生成されるコードを見てみました。
結論としては、コンパイラが上手いことやってくれてる、っていうだけのお話でした。

ちなみに <T: class, constructor> というように class の制限も一緒につけると値型や文字列型は指定できなくなります。
また、<T: record, constructor> とすると、プリミティブ型しか指定できなくなります。つまり、クラス型は指定できません。

constructor 制約に値型や文字列型がわたせるのは、上記の "class", "record" のどちらも指定していないため、どっちも通るよ!っていう事なのだと思います。

それと、軽く流してしまいましたがジェネリックで指定された型が何型なのかは拡張 RTTI メソッド群を使えば取得できます。
詳しくは、上記のコード中の TConstructorConstrain クラスの ToString メソッドを参照してください。

2013年6月15日土曜日

XE4 Update 1

2013/06/12 に XE4 Update 1 が発表されました。

XE4 Update 1

XE4 Update 1 リリースノート

これで、FireMonkey の「最小化問題」と「タスクバーのコンテキストメニュー問題」は解決したのですが、まだ問題が発生しているようです。
それは、タスクバーのボタンを押しても、最小化ができない、という問題です。
通常、タスクバーでアプリケーションのボタンを押すと、アプリケーションが最小化し、もう一度押すと元のサイズに戻る、という動作をしますが、今回の Update でも、この問題は治っていないようです。
僕も気づいていませんでしたが…。
というのも、前のバージョンでは先の記事通り、そもそもタスクバーのボタンが上手く動作していなかったためです。

多分、直すのはそんなに難しくないと思われるので、時間があったら FixFMXForm.pas をアップデートしたいと思います。

ちなみに、現状の FixFMXForm を使っても、治ると言えば治ります。

2013年5月28日火曜日

Delphi で iOS を開発するためのマシン構成について。

とても今更ですが、僕がどのような構成で iOS アプリを開発しているか、そのマシン構成図を上げておきます。
VAIO Z は "Power Media Dock" という拡張ドックがあるので、その HDMI / USB ポートと KVM スイッチが常時接続された状態になっています。

VAIO Z シリーズは、どうやら終了のようです。
外付け拡張ドックに GPU / HDMI / USB 3.0 / Blueray を逃すというアイデアは秀逸でした。
個人的には大好きな一台です。

ちなみに、これも Delphi ML で貼った図になります。
図中の Mac mini は、最低ランクの一番安いものです(型番:MD387J/A)。



2013年5月27日月曜日

FireMonkey の最小化問題と右クリック問題に対処する


元々リリースの早い段階から FireMonkey3 を Windows で使うと最小化時にタスクバーに収まらないという問題があることが知られていました。



今回さらに、Delphi ML で『タスクバーの右クリックで出てくるシステムメニューで「ウィンドウを閉じる」を選択してもアプリが終わらない』という問題が指摘されました。



実は2つとも原因は同じです。
原因は、TApplication の設計上のミスです。

このバグが出た背景として、MacOS X 対応が上げられます。
MacOS X は、メインフォームを閉じてもアプリが終了しません。
あくまで、メニューからアプリケーションの終了を選ばないと、終了しないのです!
しかし、Windows では、メインフォームの終了は即ちアプリケーションの終了です。
TApplication は Win と Mac 2つの環境で違う立ち振る舞いをすべきですが、今回は Windows への対応が甘くなっていました。
それは、ベータテスターの主な注目点が iOS 対応だったためだと考えています。
僕自身も Windows でのテストはせずに iOS と MacOS X 部分のみテストしていました。
今後のベータテストでは、Windows もしっかりと見ていく必要がありそうです。

最小化できない問題について


1.TForm の Owner に TApplication が設定されている
2.TForm の WndParent には DesktopWindow が設定されている

という2つの事象から発生しています。
オーナーが設定されている、かつ、拡張ウィンドウスタイルに WS_EX_APPWINDOW が設定されていないので、タスクバーにはオーナーウィンドウ(TApplication)のみが表示されます。
しかし、WndParent(親ウィンドウ)に DesktopWindow が指定されているため、最小化するとデスクトップウィンドウ内の子フォームとして最小化されてしまいます。

システムメニューで閉じない問題について


最小化できない問題のところで、タスクバーにはオーナーウィンドウ(TApplication)が表示されていると記しました。
もうおわかりかと思いますが、タスクバーを右クリックして出てきたシステムメニューは TApplication のモノです。



システムメニューをクリックしても TForm に WM_SYSCOMMAND メッセージは送出されず、TApplication に対して送出されます。
TApplication は、受け取った WM_SYSCOMMAND を、そのまま DefWindowProc に流しているだけなので、TApplication のウィンドウハンドルは閉じてしまい、無効になります。
しかし、プロセスを終了させていないので、プロセスは残り続けます。
この問題を解決するためには TApplication が WM_SYSCOMMAND を受け取った時に メインフォームの Close を呼ぶようにしてやるだけです。

ただ、それだけだと最小化の問題は解決できません。
そこで、今回、下記のユニットを作りました。
ユニット uFixFMXForm.pas は、uses するだけで、上記の2つの問題を解決します。
このユニットは Application のタスクバーボタンを消して、フォームのタスクバーボタンを表示する、という解決方法をとりました。

ただし、副作用があって TApplication がオーナーのウィンドウは全てトップレベルウィンドウになります。
TApplication がオーナーでは無い ShowMessage などのダイアログ系はトップレベルにならないので、実用上の問題にはならないでしょう。

詳細はコード中のコメントを参照してください。

unit uFixFMXForm;
 
interface
 
implementation
 
// Win32 API を使いまくるので Windows 以外ではコンパイルされないようにする
{$IFDEF MSWINDOWS}
uses
System.SysUtils,
Winapi.Messages, Winapi.Windows;
 
var
GHookHandle: HHOOK; // フックハンドル
GAppWnd: HWND = 0; // TApplication のハンドル
 
// SendMessage でメッセージが送られたときに呼ばれる
function CallWndProc(
iNCode: Integer;
iWParam: WPARAM;
iLParam: LPARAM): LRESULT; stdcall;
var
ActiveThreadID: DWORD;
TargetID: DWORD;
begin
// フックチェインの他のフックハンドラを先に呼んでしまう
Result := CallNextHookEx(GHookHandle, iNCode, iWParam, iLParam);
 
// nCode が 0 以下の時は処理してはいけない
if (iNCode < 0) then
Exit;
 
// iLParam には CWPSTRUCT 型へのポインタが格納されている
// この型には SendMessage で送られたメッセージの詳細が入っている
with PCWPStruct(iLParam)^ do begin
case message of
// ウィンドウができるとき
WM_CREATE: begin
with PCREATESTRUCT(lParam)^ do begin
// まだ TApplication が生成されていない、かつ「ウィンドウクラス」が
// TFMAppClass(FireMonkey の TApplication のクラス名)だったとき
if (GAppWnd = 0) and (StrComp(lpszClass, 'TFMAppClass') = 0) then
// hwnd を TApplication のウィンドウハンドルとして保存しておく
GAppWnd := hwnd
else begin
// もしも TApplication が visible(=タスクバーに表示されている)
// なら、非表示にする!
if (GAppWnd <> 0) and (IsWindowVisible(GAppWnd)) then
ShowWindow(GAppWnd, SW_HIDE);
 
// オーナーウィンドウが TApplication なら TForm のインスタンスと
// みなして拡張ウィンドウスタイルに WS_EX_APPWINDOW を設定する
// WS_EX_APPWINDOW が設定されたフォームは、トップレベルウィンドウ
// となるので、タスクバーに表示される
if (GetWindow(hwnd, GW_OWNER) = GAppWnd) then
SetWindowLong(
hwnd,
GWL_EXSTYLE,
GetWindowLong(hwnd, GWL_EXSTYLE) or WS_EX_APPWINDOW);
end;
end;
end;
 
// ウィンドウが表示されるとき
WM_SHOWWINDOW: begin
// オーナーがあるのに拡張スタイルに WS_EX_APPWINDOW を指定していると
// 最前面に表示されない事があるので、ウィンドウが表示されるときは
// 強制的に最前面にする
// 強制最前面化処理は、下記のように AttachThreadInput を使うが
// 詳細は省略
if (GetWindow(hwnd, GW_OWNER) = GAppWnd) then begin
ActiveThreadID := GetWindowThreadProcessId(GetForegroundWindow, nil);
TargetID := GetWindowThreadProcessId(hwnd, nil);
 
AttachThreadInput(TargetID, ActiveThreadID, True);
try
SetForegroundWindow(hwnd);
SetActiveWindow(hwnd);
finally
AttachThreadInput(TargetID, ActiveThreadID, False);
end;
end;
end;
end;
end;
end;
 
initialization
begin
// WH_CALLWNDPROC フックを仕掛ける
// WH_CALLWNDPROC は SendMessage が呼ばれたときに呼ばれるフック
GHookHandle :=
SetWindowsHookEx(WH_CALLWNDPROC, CallWndProc, 0, GetCurrentThreadID);
end;
 
finalization
begin
// フックを解放
UnhookWIndowsHookEx(GHookHandle);
end;
{$ENDIF}
 
end.

と、まあ今回このようなユニットを作りましたが、近日リリースされるであろう XE4 Update1 で、このバグは治っている事でしょう。
ですから、無理してこのユニットを使わず、Update1 を待っても良いかも知れません。

2013年4月13日土曜日

Prezi を使ってみたよ

Prezi を使ってみたよ。

Prezi のサンプルなので中身の説明は超いい加減!


2013年4月3日水曜日

もうすぐ再開します!

もう4月!
この一月以上更新が無かった理由が、もうすぐ終わるので、それに関連した話題とかも上げていきます!
しばし待たれよ!

2013年2月13日水曜日

IME のメッセージを Windows Hook で取得する


Delphi-ML で、IME の変換スタートと終了を知りたい、という投稿がありました。
全ての Edit をサブクラス化して実装しようとされていたのですが、それでは非常に大変だと思い、Windows Hook による方法を投稿しました。

それが、以下のコードです。

unit uIMEStartEnd;
 
interface
 
uses
Winapi.Windows;
 
type
// IME の開始と終了を知らせるイベント
TIMEStartEndNotifyEvent =
procedure(const iWnd: HWND; const iStart: Boolean) of object;
 
// イベントを受け取るイベントリスナを設定・削除する
procedure AddIMEEventListener(const iEvent: TIMEStartEndNotifyEvent);
procedure RemoveIMEEventListener(const iEvent: TIMEStartEndNotifyEvent);
 
implementation
 
uses
Winapi.Messages, Vcl.Controls, System.Generics.Collections;
 
var
// WindowsHook のハンドル
GHookHandle: HHOOK;
// イベントリスナのリスト
GHandlers: TList<TIMEStartEndNotifyEvent>;
// イベントリスナを追加
procedure AddIMEEventListener(const iEvent: TIMEStartEndNotifyEvent);
begin
if (GHandlers.IndexOf(iEvent) < 0) then
GHandlers.Add(iEvent);
end;
 
// イベントリスナを削除
procedure RemoveIMEEventListener(const iEvent: TIMEStartEndNotifyEvent);
begin
if (GHandlers.IndexOf(iEvent) > -1) then
GHandlers.Remove(iEvent);
end;
 
// イベントリスナを呼び出す
// iWnd IME メッセージを受け取ったウィンドウ
// iStart 開始の場合は True
procedure CallEventHandlers(const iWnd: HWND; const iStart: Boolean);
var
Handler: TIMEStartEndNotifyEvent;
begin
for Handler in GHandlers do
Handler(iWnd, iStart);
end;
 
// Hook のメイン関数
function CallWndProc(
iNCode: Integer;
iWParam: WPARAM;
iLParam: LPARAM): LRESULT; stdcall;
begin
// 先に、次のフックチェインを呼び出してしまう
Result := CallNextHookEx(GHookHandle, iNCode, iWParam, iLParam);
 
// iNCode が 0 以下ならフックは作業してはならない
if (iNCode < 0) then
Exit;
 
// lParam は CWPStruct 形式で、メッセージが入っている
with PCWPStruct(iLParam)^ do begin
case message of
WM_IME_STARTCOMPOSITION: begin
// IME 変換開始
CallEventHandlers(hwnd, True);
end;
 
WM_IME_ENDCOMPOSITION: begin
// IME 変換終了
CallEventHandlers(hwnd, False);
end;
end;
end;
end;
 
initialization
begin
// イベントハンドラを管理するリストを作成
GHandlers := TList<TIMEStartEndNotifyEvent>.Create;
 
// WindowsHook
GHookHandle :=
SetWindowsHookEx(WH_CALLWNDPROC, CallWndProc, 0, GetCurrentThreadID);
end;
 
finalization
begin
// Hook を解放
UnhookWIndowsHookEx(GHookHandle);
 
// リストを破棄
GHandlers.Free;
end;
 
end.

具体的にはソースコードのコメントを参照してほしいのですが、一点だけ Windows Hook について説明します。
Windows Hook は Windows の作業に割り込む機構です。
例えば、キーボードの入力があった時や、新しいウィンドウが開くときなど本来アプリケーション側からは見えない Windows の作業に割り込むことができます。
Windows Hook を設定するためには SetWindowsHookEx API を使います。
SetWindowsHookEx の説明を見て頂ければわかるように、様々なフックがあります。
今回は、その中で WH_CALLWNDPROC フックを使う事にしました。
このフックは Window Procedure にメッセージが渡されるタイミングで呼び出されます。
そのタイミングとはいつかというと、具体的には SendMessage API が呼ばれた時です。

PostMessage API を使った場合は、WH_GETMESSAGE フックが使えます。

WH_CALLWNDPROC フックを使うのは IME のメッセージは SendMessage で送られるためです。
そして、今回のソースでは SetWindowsHookEx の3番目の引数に 0 を指定しています。
ここに 0 を指定して、次の引数に現在の Thread ID を指定すると、現在のスレッドで処理される SendMessage についてフックされるます。
この Unit の Initialization 節はメインスレッドから呼ばれるので、メインスレッド(GUI の操作・表示をするスレッド)がメッセージを受け取る度に、CallWndProc 関数が呼ばれることになります。
その結果、Edit などで IME の処理が行われると、それを感知してイベントを発行できます。

また、SetWindowsHookEx の3番目の引数に HInstance を指定してフックする DLL を作ると、全プロセスに結びつくフックを作る事ができます。
これによって、他のプログラムのメッセージを見ることもできます。

これをグローバルフック DLL と呼びます。
グローバルフック DLL の作成については、また次回!