Delphiプログラマを見分ける10の質問。
16/07/20追記
真っ当な設計をしてたら出会わない挙動が多く、ネタとしてわざと難しい質問にしてるので、間違ったら恥ずかしいなんて躊躇せず、ぜひとも勢いよく突っ込んで爆発四散してください!
はじめに
ものすごーく前に「○○プログラマを見分ける10の質問」みたいなのが流行った時に質問を数個考えてそのまま放置してたメモを発掘したんですが、せっかくなので残りを適当に考えて10個用意してみたので我こそはという方は挑戦してみてください><
なお、質問のほとんどがネタ要素であり、知らなくても問題ない、僕の勘違いが多分に含まれる、答えがひとつじゃない、等々あると思いますが、あくまでもネタであるということを念頭にお付き合いいただき、ついでに勘違い部分に関しては正しいツッコミを入れてもらえると嬉しいです。
10の質問
- 「const」と「var、out」の違いを参照という観点でひとつ挙げよ。またその違いを無くすためにはどうすれば良いか説明せよ。
- 文字列や動的配列などの型は自動で初期化されるため自分でnil等を代入する必要がないが、初めての使用時にnilや空文字列で初期化されていない場合があるのはどんな時か?またその理由を説明せよ。
- 複数の文字列変数を連結するとき、「sA := s1 + s2; sB := s3 + s4; s := sA + sB;」と「s := s1 + s2 + s3 + s4;」は足し算の数だけ見れば等価だが、後者の方が良いのは何故か。
- nilが代入されているインスタンスの(クラスメソッドではない)メソッドを呼び出そうとした場合でも読み取り違反などのエラーが発生しないのは主にどんな状況か?またその理由を説明せよ。
- 関数内関数をコールバックを必要とする関数の引数として渡そうとすると「ローカル手続き/関数を手続き変数に代入しました」というエラーが発生してコンパイルできないが、これを回避するにはどうすれば良いか。またその際気を付けることは何か。
- TComponent.FOwnerをはじめ、10 SeattleではWeak属性が指定されていたフィールドが10.1 BerlinではUnsafe属性に置き換えられているが、これはどういった理由が考えられるか。
- recordでインターフェース(例えばIInterface)を実装する方法を簡単に説明せよ。
- 通常inlineが指定された関数・メソッドは処理内容がインライン展開可能な条件であれば呼び出しはインライン展開されるが、呼び出し方によってインライン展開される場合とされない場合が発生するのはどのような状況か述べよ。複数あればなお良い。
- デフォルトのコンパイラ指令下において、RTTIでメソッド情報がほとんど取得できないのはどんな型か。またほとんどと書いたが、メソッドに関するどんな情報なら取得可能か。
- 無名メソッド型の実態はInvokeメソッドを持つインターフェースであり、通常はAnonMethod()のようにそのまま実行できるが、Invokeメソッドを明示的に呼び出さないと実行できない場合がある。どんな時か。
答えは近いうちに載せる予定。
TPdfDocument クラス。
これは何?
前回の成果を簡単なクラスにまとめて、Delphi的にCreateしてLoadFromFileで読み込み的なやつです。読み込んでExportAsImageで画像化するシンプルなクラスになってます。
使い方
デフォルト設定で使う場合はめちゃくちゃシンプルです。
uses ..., PdfDoc; // <-今回作成したユニット var pdf: TPdfDocument; begin pdf := TPdfDocument.Create; try pdf.LoadFromFile('C:\Sample\Sample.pdf', procedure begin pdf[0].ExportAsImage('C:\Sample\Sample_page1.png'); end); fianlly pdf.Free; end; end;
以上のコードでPDFを読み込んで1ページ目をPNGファイルとして保存します。
LoadFromFileで無名メソッドを指定してますが、WinRTのファイル読み込みは非同期なのでLoadFromFile直後ではまだロードが終わっていない可能性があり、読み込み完了通知用のコールバックを指定する必要があります。また、ExportAsImageでのファイル保存も非同期なので、上記コードでは省略していますが同じようにコールバックを書いて保存終了通知を受け取ることができます。
ExportAsImageメソッドは省略されたオプションやオーバーロードがいくつかあり、正式には以下のようになっています。
procedure ExportAsImage(Adapter: IStream; FileType: TExportFileType = eftPng; DestWidth: UInt32 = 0; DestHeight: UInt32 = 0; CompleteProc: TProc = nil); overload; procedure ExportAsImage(Stream: TStream; FileType: TExportFileType = eftPng; DestWidth: UInt32 = 0; DestHeight: UInt32 = 0; CompleteProc: TProc = nil); overload; procedure ExportAsImage(const FileName: string; FileType: TExportFileType = eftPng; DestWidth: UInt32 = 0; DestHeight: UInt32 = 0; CompleteProc: TProc = nil); overload;
1つ目の引数は保存先の指定です。3つのオーバーロード中、StreamやFileNameは特に解説せずともよくあるストリームとファイルへの書き出しなので分かると思いますが、IStreamへの書き出しは単にStreamとFileNameのメソッドで内部的に使用するものです。privateにしても良かったんですが、IStreamを直接指定できた方が楽な場合もあるので一応。
2つ目のFileTypeですが、指定するとエクスポートする画像の形式を指定できます。デフォルトではPNG形式ですが、BMPやJPEGなど他の形式も指定可能です。
3つ目と4つ目のDestWidthとDestHeightは出力画像サイズになります。0を指定するとページの縦横サイズがそのまま使用されます。両方指定するとそのサイズに拡大縮小され、どちらか一方だけ指定するともう片方のサイズはアスペクト比を考慮して自動的に計算されます。例えば横:縦=3:2のPDFの場合、DestWidthに600を指定してDestHeightはデフォルトのまま(=0)にすると、DestHeightは自動的に400が指定されます。
CompleteProcは完了通知を受け取ります。TPdfDocument.OnExportCompleteイベントでも受け取れますが、両方指定した場合はこのCompleteProcが優先されます(LoadFromFileのCompleteProcとOnLoadCompleteイベントも同じ仕様です)。
なお、上記のコードはパスさえ通せばユニットをインストールせずとも使えますが、TPdfDocumentはTComponentを継承する非ビジュアルコンポーネントにもなっているので、インストールしてフォーム上にTPdfDocumentを貼り付けることで、IDE上でOnLoadCompleteやOnExportCompleteイベントをセットするような使い方も可能です。
ダウンロード
この中に入っているPdfDoc.pasが今回追加したユニットです。他のラッパーユニットと同じようにパスの通ったところに置くか、そのフォルダ自体をパスに追加して使ってください。サンプルの方はProject1.dprがDirect2Dを使った前回のサンプルで、Project2.dprがPdfDoc.pasを使った簡易PDFビューア的なサンプルになります。動作環境はOS側はWindows 10、Delphi側はWinRTが入手できるバージョン(後期のXEシリーズや10.x系)であれば動くと思います。サンプルコード内ではVCLを使ってますが、PdfDocユニット自体は特にVCL依存は無いので、FMXでも吐き出したTStreamを(FMXの)TBitmapクラスに食べさせれば画像を取得できるはず。
あとライセンスを書くのを忘れてましたが、Boost Software License 1.0です。
外部ライブラリ無しでPDFを描画する。
WinRTって知ってる?
WinRTとは、Windowsストアアプリ専用のAPIセット…ではないです。すでにトーストAPIなどを使っていてご存じの方もいると思いますが、デスクトップからも利用可能なCOMベースのAPIです。
Windows 8の時点だとWinRTは未来があるのか怪しいAPIでしたが、Windows 10がWindowsの最終バージョンであるとMSが宣言し、そのWindows 10に載っているAPIなのでWin32APIと同等に扱っても良さそうです。
WinRTでできること
ストアアプリは基本的にこのAPIを使って構築することになるので、上述のトーストAPIで画面に通知を出すこと以外にもとにかく色んなことができます。
で、その中にはPDFを扱うAPIもあります。こちらのブログ経由で知ったんですが、これを利用するとどうやら外部ライブラリを使わずにPDFを扱えるようです。
WinRTは腰の重いエンバカデロには珍しいことに、すでにラッパーユニットが用意されています。10 Seattle以降では標準搭載、それ以前ではGetIt経由で入手可能です。
しかし…
じゃあこれでPDF APIも使用可能かというと、残念ながらPDF関連のヘッダはほぼ全く移植されてませんでした(関連する列挙型が1つあるだけ)。
無いなら自分でやればいいということで、さくっと移植。これでようやくAPIが使える…かというとそうは問屋が卸さず。
PDF描画の心臓部であるIPdfRendererNativeインターフェースのRenderPageToDeviceContextメソッドで引数として使われているID2D1DeviceContextが存在しないとコンパイラ様が仰るわけです。
怠慢ですよねぇ
DelphiにはWinapi.D2D1ユニットがはじめから入ってます。Direct2D用のユニットです。これはd2d1.h(とそれに必要な他のいくつかのヘッダ)を移植したもので、これ自体には問題ありません*1。実はDirect2Dにはバージョンがあり、Vista/7で導入された1.0、8で導入された1.1、8.1で導入された1.2、10で導入された1.3があります。d2d1.hはこのうち1.0のみを扱っており、1.1はd2d1_1.hが、1.2はd2d1_2.hが、1.3はd2d1_3.hが必要になります。なりますが、Delphiにはそれらを移植したユニットは存在しません。
大事なことなのでもう一度言いますが、Delphiにはそれらを移植したユニットは存在しません。新バージョンが発売されて「WINDOWS 10の最新機能をサポート」なんてエンバカデロは言ってますが、やはりDelphiにはそれらを移植したユニットは存在しません。エンバカデロの「Windows XXに対応!」はセールス的に見栄えのするUI系やセンサーのサポートみたいなのはやってくれるんですけど、そのOSで追加された機能の大半、特に裏方的な機能はほんとに対応してくれません。PDFはまだいいとしても、Direct2DはGDI/GDI+の後継APIでかなり重要なので、こんな状態でサポートとか言うのほんと詐欺なんでやめて欲しい…。
もっと頑張って移植
なんて愚痴を言ってるときりがないので、諦めて自分で移植します。幸いなことにID2D1DeviceContextはDirect2D 1.1で導入されたので、d2d1_1.hだけを移植すれば良さそうです。「だけ」と言いつつ、d2d1_1.hは5000行を超すサイズなのでPDFヘッダを移植する時の手軽さでは全然ないんですが、これも何とか移植しました。
ようやく本編
これでPDF関連のユニット(WinAPI.Data.Pdf.pas, WinAPI.Data.Pdf.Interop.pas)とそれに必要なDirect2D用ユニット(Winapi.D2D1_1.pas)が揃いました。あとは前述のブログを参考に実装するだけです。本質部分とは関係ないDirect2Dの初期化だけで結構量があり、ここに載せるにはちょっと長いので必須ユニットとサンプルをまとめてアップしますが、こんな感じでフォームにPDFを表示できます。
ちなみに表示だけじゃなく右クリックで画像として保存可能ですが、フォーム上に表示するのと画像化では処理が異なり、前者はIPdfRendererNativeとIPdfDocumentインターフェースを、後者はIPdfDocumentインターフェースのみを使っています。前者はID2D1DeviceContextやIDXGISurfaceのように画面上に書き出すためのもので、後者はIRandomAccessStreamなどストリームに書き出すことができます。特に後者はDirect2D等を必要としないので、PdfDocumentのインスタンスさえ作れば簡単に扱うことが可能です。IStreamにも書き出せるので、IStreamを実装してTMemoryStream辺りに書き出すようにして、それをDelphi側の画像クラスに食わせて画面表示した方が全体としては楽そうです。
ダウンロード
ユニットとサンプルをダウンロード
サンプルを試してみる時は、libフォルダをパスに追加するか、中身をパスの通った場所に置いてください。
動作環境ですが、MSDNを見るとWindows 8.1でも使えるのかWindows 10じゃないと使えないのかAPIやページによってまちまちでよく分からないんですが、少なくとも手元のWindows 10+10.1 Berlin上では動いてます。
Oculus SDK 1.3.2 wrapper for Delphi (と、いくつかデモ)
自作プラグイン更新のお知らせ。
10.1 Berlinで動かなかったので直したもの
10.1 Berlinでもそのまま動いたもの
10.1 Berlinのclass helper仕様変更は全然改良じゃないと思った?
皆さんご存じかと思われますが、Delphi 10.1 Berlinがリリースされました。で、新機能の一覧の中にこんなことがさらっと書いてありました。
Delphi コンパイラのその他の改良点 (中略) 可視性のセマンティクスを実行するため、クラス ヘルパやレコード ヘルパでは、拡張元のクラスやレコードの private メンバにはアクセスできません。
なんと、class helperによるprivateメンバーアクセスが制限されてしまいました。class helper内では可視性がprivateとなっていて公開されていないフィールドの読み書きやメソッドのコールなどができるため、VCLやFMXが実装していない機能を自分で追加する時や未修正のバグの対処などで非常に役立つ機能でした。
これが制限されるとなると、こういった処理を記述したソースコードが軒並みコンパイルできなくなるという、バージョン1からの互換性を謳い文句にさえしていたDelphi*1とは思えない愚行です。
とはいえ制限された形でリリースされた以上、何らかの対応を取らなければならないわけで、早速Stack Overflowにも質問が寄せられてるみたいです。しかしそこで回答として寄せられている方法はclass helperを使わずに対処するものなので、それぞれ一長一短があります。
そこで制限を回避して、class helper内からprivateメンバーに再びアクセスする方法を紹介します。
この方法もx86/x64限定という短所はあるにはあるんですが、その代わり以前と同じようにclass helperからprivateメンバーへアクセスすることができます。答えとしては単純なもので、インラインアセンブラを使用するだけです。
unit UnitA; type THoge = class private FPrivateValue: Integer; procedure PrivateMethod; end; end. unit UnitB; type THogeHelper = class helper for THoge public function GetValue: Integer; procedure CallMethod; end; function THogeHelper.GetValue: Integer; asm MOV EAX,Self.FPrivateValue end; procedure THogeHelper.CallMethod; asm CALL THoge.PrivateMethod end;
このようにインラインアセンブラでアクセスすることで10.1 Berlinで導入された制限を回避できます。
たぶんインラインアセンブラで制限することを忘れてるだけなので、アップデートか何かで塞がれそうな気もしますが、少なくともそれまでの繋ぎとしては使えそうです。
(追記)
メソッドコールは引数があると全部アセンブラ上で積まないといけなくて面倒だと思うので、次のような方法を取るとわりと簡単にコールできます。
type THoge = class private procedure PrivateMethod(Arg1, Arg2, Arg3: Integer); end; // 方法1 // メソッドポインタだけ頂く(メソッドポインタをどこかに代入する必要がある等の場合) type THogePrivateProc = procedure(Self: THoge; Arg1, Arg2, Arg3: Integer); THogePrivateMethod = procedure(Arg1, Arg2, Arg3: Integer) of object; function THogeHelper.GetMethodAddr: Pointer; asm LEA EAX,THoge.PrivateMethod end; var hoge: THoge; proc: THogePrivateProc; method: THogePrivateMethod; begin // こっちの方法でもいいし、 proc := hoge.GetMethodAddr; proc(hoge, 1, 2, 3); // こっちの方法でもいい TMethod(method).Code := hoge.GetMethodAddr; TMethod(method).Data := hoge; method(1, 2, 3); end; // 方法2 // ジャンプさせる(単純に呼び出すだけならこっちが簡単) procedure THogeHelper.CallMethod(Arg1, Arg2, Arg3: Integer); asm JMP THoge.PrivateMethod end;
*1:まあ最近は破壊的仕様変更も結構多いけど…