Delphi Cocoaフレーバード

Delphi Cocoaフレーバード






すべての人の生活の中で、オペレーティングシステムの使用に関する最新の世界統計を見て、彼は大きな変化の時が来たことに気づく時が来ます。 同時に、家、職場、妻を変える必要はありませんが、過去10年間で大幅に成長した視聴者にリーチする価値はあります。 これは、Delphi for macOS(OS X nee)での開発と、TamoSoftでのツールの選択、新しいものの習得、研究、爆撃、そしてそのプロセスの楽しみについてです。



挑戦する



出発点:当社の主力製品であるTamoGraph Site Surveyは、カバレッジマップの構築、アクセスポイントの場所の最適化、仮想信号伝搬モデルの作成、およびこの分野で働くエンジニアにとってより多くの有用なことを行うことができるWi-Fiネットワーク検査ツールです。 TamoGraphはWindows上で実行されます。 目的地:さて、あなたはそれを推測した。 TamoGraph。macOSで動作します。



製品は主にDelphiで書かれており、個々のモジュールはC ++で書かれています。 デルファイを選ぶ理由 (質問のバリエーション:彼はまだ死んでいるか? 11番目に最も人気のある言語)たくさん。 これは優れた生産性であり、はい、習慣の力、高速コンパイラーですが、主な理由は完全にテクノロジーの分野ではありません。 Delphiで書くのが好きで、これから話題になります。 そして、製品が喜びと愛をもって書かれているとき、それは通常うまくいきます。 ですから、私たちは宗教的な論争に従事するのではなく、直接問題に進みます。



したがって、出発点(Windows、Delphi)から、最短パス(macOS、まだ未知の言語/環境)で目的地に到達する必要があります。 以下の主なオプションが考慮されました。



1. SwiftまたはObjective-Cを使用してXcodeですべてをやり直します。

2.動的ライブラリ形式の既存のDelphiコードの一部を使用して、そのほとんどをXcodeに変換します。

3. FMXフレームワーク(FireMonkey)を使用して、そのほとんどをDelphiに変換し、コードのごく一部をObjective-Cに書き込み、動的ライブラリとして使用します。

4. Object Pascalの一種であるOxygeneを使用して、すべてをRemObjects Elementsに変換します。



もちろん、各オプションには多くの長所と短所がありました。 XcodeはGUIの完全なネイティブ性であり、オペレーティングシステムとやり取りする際に問題が発生しないこと、大量のサンプルコードとライブラリです。 しかし、これは非常に大きな「しかし」であり、これらすべてが「キット内」にあるため、多くのコードを別の言語に書き直す必要があります。 RemObjects Elementsは完全なネイティブGUIでもあり、Object Pascalと非常によく似た言語を使用します。つまり、GUIに関連しない既存のコードを比較的小さな変更で使用できます。 ただし、当時このツールをテストした人はいませんでした。 そして最後に、Delphi FMX。 プラス-既存のデバッグされたコードを完全で使い慣れた環境で使用しますが、同時に非ネイティブコントロール(判明したように、これは完全に真実ではありませんが、以下に詳しく説明します)、macOS APIとやり取りする際に起こりうる問題、および他の多くの疑問。



この記事のタイトルで推測したように、いくつかのテストを慎重に相談して実施したため、オプション(3)、つまり Delphi FMX。 非常に魅力的なのは、コードの大部分を書き換えないことです。 そして、私は認めなければなりません、私は本当に私が最初に傾いていたRemObjects Elementsが好きではありませんでした。 だから、選択が行われ、彼の袖をまくり、追い出されました...



アート準備



チームの一部は、少なくともmacOSとの緊密なコミュニケーションを既に経験しており、そのデバイスを十分に代表していました。 その一部は、理論的なトレーニングを必要とするまったく新しい人たちでした。 これらの目的のために、良い本はMac OS XとiOS Internals:To the Apple's Coreでした。 実際には、MacBookは必要なすべての人のために購入され、10.9から最新の10.12までのさまざまなバージョンのmacOSが仮想マシンに展開されました。



Delphi上のmacOSのプログラムをデバッグするプロセスは、Windowsの通常のプロセスとは異なります。Windowsの場合、原則として、Delphi環境が実行されているのと同じコンピューターでデバッグプログラムを実行します。 C macOSはもう少し複雑です:まず、macOS Platform Assistantを搭載したマシンにインストールします。 アプリケーションの展開とデバッグを提供する補助アプリケーション(Delphiの一部)、およびWindowsで既にDelphiの側から、プラットフォームアシスタントを実行しているマシンのIPアドレスを指定します。



デルファイプロトフォームアマスタント








その後、プログラムを起動するだけで、すぐにMacでの作業が開始されます。 当然、Windowsプログラムのデバッグに慣れているのとまったく同じ方法でデバッグできます。



FMXコントロール



これで、すべてが設定され、Macで最初の「Hello World」を実行できます。 GUIは、FMXのビジュアルコンポーネントを使用して、使い慣れたDelphiビジュアルエディターで実行されます。 FMXフレームワークは、2011年にDelphiバージョンのXE2に登場しました。 最初は非常にバグが多かったと言わざるを得ませんが、過去6年間で徹底的に書き直され、問題の数が大幅に削減されました。 これは、最も単純なTButtonからグリッド、リストビュー、およびその他の使い慣れたコントロールまで、完全に使用可能なコンポーネントのセットです。 したがって、今日のFMXインターフェイスの作成は非常に現実的で快適ですが、ここにはいくつかの機能があります。



まず、FMXコントロールはネイティブではありません。 これは、VCLで行われるようなシステムコントロールのラッパーではありません。たとえば、TButtonはWindowsが描画するシステムコントロールであり、Delphiではありません。 ここで、Delphiは、プログラムが実行されているmacOSのバージョンのスタイルと一致するスタイルを使用する独自のスタイルエンジンを使用して、コントロールを描画します。

Yosemiteのダイアログの例(10.10):



macOS 10.10のダイアログ






以下は、マーベリックス(10.9)の同じダイアログです。 GUI要素のスタイルは、Mavericksの「ネイティブ」スタイルに自動的に適合し、異なって見えます。



macOS 10.9のダイアログ






原則として、これはうまく機能しますが、スタイルの一部を調整する必要があります(または、以下で説明するようにネイティブコントロールを使用します)。 たとえば、ヨセミテに登場したmacOSの「グラフィカル」スタイルは、Delphiにはありません。独自に実行する必要がありました。 それには2人日かかりました。



2番目の問題は「小児疾患」です。 私が言ったように、子供(FMXフレームワーク)は6歳であり、エンバカデロの努力にもかかわらず、彼はまだ必要なものすべてに完全に病気ではありません。 たとえば、アプリケーションのメインメニューでは、最上位のアイテムを除くすべてのアイテムに対してOnClickイベントが発生します。 つまり [ファイル]→[開く]、[ファイル]→[保存]などのメニューがある場合、[開く]と[保存]をクリックするとOnClickイベントが発生しますが、サブバイトのリストが表示されたときに[ファイル]をクリックすると発生しません。 または、標準の[開く]および[保存]ダイアログを使用します。 まったく予想外に、ダイアログを表示すると、アプリケーションのイベントループが完全に「遮断」され、ダイアログが開いている間に何かが発生しなくなります(タイマーティックを含む)。 私の意見では、これはすべて、社内テストが弱すぎ、バグ報告に対するエンバカデロの反応が遅すぎる結果です。



これらの疾患は、システムユニットにパッチを当てることなく、実行時に処理されます。 TFMXMenuDelegateクラスの 'menuWillOpen:'の呼び出しをインターセプトすることでOnClickの不在を修復し、システムダイアログの表示全体を完全に書き直しましたが、バグを修正するために最初に爆発する必要があります。 警戒し、テストを怠らず、 quality.embarcadero.comでバグを報告することを忘れないでください。



最後に、FMXコントロールのトピックを閉じます。仮想モードで動作する優れたTreeViewなど、非常によく書かれた多くのビジュアルコンポーネントを含むTMS FMX UIパックをご覧になることをお勧めします。 これは、FMXの標準コンポーネントにはないものです。



実行時ライブラリ



Delphi RTLを使用することは、コードをmacOSに移植するときに最も手間がかからない部分であると予想されていました。 RTLは長い間マルチプラットフォーム向けに「シャープ化」されているため、関数や非ビジュアルクラスを変更せずに安全に使用できます。 たとえば、ハードコードされた区切り文字「\」ではなく、プラットフォームに依存しないIncludeTrailingPathDelimiterの使用など、些細なことを監視するだけで十分です。



macOS API



電卓よりも少し複雑なものを書くときは、遅かれ早かれ、ネイティブAPIを使用する必要があります。 WindowsでRTLとVCLのみを管理するのは非現実的であるように、RTLのみとFMXフレームワークを管理することは完全に非現実的です。 システムロケールを知る必要がありますか? プロセス間通信を実装しますか? 仮想メモリプロセスのサイズを確認しますか? 暗号化? 音声合成? これらはすべてネイティブAPIです。 ただし、WindowsでFindWindowまたはGetLocaleInfoを呼び出しても怖くないため、これはまったく怖いことではありません。 また、Delphiで何かが宣言されていない場合は、何でも宣言、追加、およびやり直すことができます。



API自体はいくつかのコンポーネント(BSD、Mach、Carbon、Cocoaなど)で構成されていますが、私たちの目的にとって、主な関心はCocoaです。 簡単に言えば、Cocoaは一連のクラスであり、Windows APIの使用に慣れている人にとってはかなり珍しいものです。 たとえば、UTCに対するコンピューターのタイムゾーンのオフセットを知る必要がある場合、WindowsではGetTimeZoneInformation関数にすぎません。 しかし、macOSでは、これはNSTimeZoneクラスです。 旅の始めに私たちのほとんどがかつてMSDNを吸ったように、 Apple API Referenceの余暇でたばこを吸うことに、あなたはそれに慣れてきます 。 しかし、脳が最初に本当に爆発するものから、それはDelphiとCocoaクラスの間の「橋」の構文からです。 これは非常に珍しいことです。



クラス関数は、OCWordという魔法の言葉で呼び出されます。



TNSTimeZone.OCClass.localTimeZone
      
      





通常はポインターを返しますが、すべてがそれほど単純ではありません。 これらのオブジェクトポインターは直接使用できません。 ポインターはObjective-Cでidと呼ばれるものであり、そのようなポインターをオブジェクトに変換するには、魔法のラップを作成する必要があります。



 TNSTimeZone.Wrap(TNSTimeZone.OCClass.localTimeZone)
      
      





そして、クラスインスタンス関数を既に呼び出して、最終的に必要なオフセットを取得できます。



 TimeZoneShift:= TNSTimeZone.Wrap(TNSTimeZone.OCClass.localTimeZone).secondsFromGMT;
      
      





別の例? お願い! サーバーの可用性を確認します。



 function BlockingGetTestURL: boolean; var URL: NSURL; URLRequest: NSURLRequest; aData: NSData; Response: Pointer; Policy: NSURLRequestCachePolicy; TimeOut: NSTimeInterval; const URL_TO_CHECK = 'http://open.mapquestapi.com'; begin URL := TNSURL.Wrap(TNSURL.OCClass.URLWithString(StrToNSStr(URL_TO_CHECK))); Policy:= NSURLRequestReloadIgnoringLocalCacheData; TimeOut:= 10; URLRequest := TNSURLRequest.Wrap(TNSURLRequest.OCClass.requestWithURL(URL, Policy, TimeOut )); aData := TNSURLConnection.OCClass.sendSynchronousRequest(URLRequest, @Response, nil); result:= (aData <> nil) and (aData.length > 0); end;
      
      





Objective-Cクラス関数がオブジェクトへのポインターを必要とする場合、再び@MyDelphiObjectを取得して渡すことはできません。GetObjectID関数を使用してこのポインターをIDに変換する儀式ダンスを実行する必要があります。



 function GetUserDefaultMeasureUnit : TSSMeasureUnitType; var p: pointer; ns: NSString; const AppKitFwk: string = '/System/Library/Frameworks/AppKit.framework/AppKit'; begin ns:= CocoaNSStringConst( AppKitFwk, 'NSLocaleUsesMetricSystem'); p := TNSLocale.Wrap(TNSLocale.OCClass.currentLocale).objectForKey((NS as ILocalObject).GetObjectID); if TNSNumber.Wrap(p).boolValue then Result := msMeters else result:= msFeet; end;
      
      





一般に、例を調べることで構文に慣れることができます。 Delphiから直接OS X APIを使用するという記事読むことをお勧めします。このトピックはよく公開されています。



API(Cocoaのみに限定されない)について直接話すと、かなり心地よい感じがします。 macOS APIのWindows APIには単純に何もありません。その逆も同様です。 macOSではWindowsよりも複雑なものもあれば、簡単なものもあります。 たとえば、AES暗号化を使用します。 Windowsでは、バイト配列を暗号化するには、5つの関数と数十行のコードを使用する必要がありますが、macOSでは、CCCrypt関数を使用してほぼ1行でこれを行うことができます。 そして、これはもはやCocoaの一部ではありません。



甘い、甘いPOSIX



POSIXもCocoaの一部ではありませんが、macOSをご利用いただきありがとうございます。 人生がずっと楽になります。 高レベルのクラスを介して実行できることの多くは、POSIXを介して低レベルで実行する方がはるかに簡単です。 たとえば、プロセス間通信を実装する方法は? 分散オブジェクトとNSProxyクラス? Nsconnection? メモリマップドファイルとPOSIX関数を使用して、数行のコードですべてを解決します。 shm_open、shm_unlink、mmapが必要です。 ちなみに、最初の2つはDelphiでは宣言されていませんが、これは問題ではありません。 説明を注意深く読み、宣言します:



 function shm_open(__name: PAnsiChar; __oflag: integer; __mode: mode_t): integer; cdecl; external libc name _PU + 'shm_open'; function shm_unlink(__name: PAnsiChar): integer; cdecl; external libc name _PU + 'shm_unlink';
      
      





そして、すべてが簡単です、私たちは呼び出します:



 fd := shm_open( PAnsiChar(UTF8Encode(ID)), O_RDWR or O_CREAT, S_IRUSR or S_IWUSR or S_IRGRP or S_IROTH ); ftruncate(fd, aSize); mmap(nil, aSize, PROT_READ or PROT_WRITE, MAP_SHARED, fd, 0);
      
      





それだけです。他のプロセスから名前でアクセスできるマッピングを作成しました。



まだPOSIXが必要なのはなぜですか? はい、たくさんあります。 たとえば、ここに:



 function GetPhysicalCoreCount: Cardinal; var CoreCount: Cardinal; Size: Integer; begin Size:= SizeOf(Cardinal); if sysctlbyname('hw.physicalcpu',@CoreCount, @Size, nil, 0) = 0 then result:= CoreCount else result:= System.CPUCount; end;
      
      





ソケットの使用、COMポートの使用など-これらすべてのために、Windowsとほぼ同じ構文で、シンプルで使い慣れたPOSIXが適しています。 特に、WindowsでGPSレシーバーを使用するために使用したCOMポートを使用するには、Delphiクラスを移植する必要がありました。 そこにあるコードは約1,500行です。 難しいですか? いいえ、そうでもありません。 この種の1日の作業と約50のIFDEF:



 function TGPSReceiver.ClearInputBuffer: Boolean; begin Result := False; if Assigned(ComThread) and ((ComThread as TComThread).ComDevice <> GPS_INVALID_HANDLE_VALUE) then begin try {$IFDEF MSWINDOWS} Result := PurgeComm((ComThread as TComThread).ComDevice, PURGE_RXCLEAR); {$ENDIF} {$IFDEF MACOS} result:= tcflush((ComThread as TComThread).ComDevice, TCIFLUSH) = 0; {$ENDIF} except Result := False; end; end; end;
      
      





それらは移植され、テストされ、1日の終わりまでに、楽しく点滅する衛星画像でGPSを操作するためのモジュールを受け取りました。



ネイティブコントロール



FMXコントロールの標準セットに慣れていない場合、これは重要ではありません。 ネイティブビジュアルクラスの使用を禁止したり、特定のルールを遵守してFMXコントロールと組み合わせたりすることも禁止されていません。 実際のところ、DelphiアプリケーションでFMXフレームワークを使用することを禁止する人はいません(ただし、これは既に少し極端です)。



パフォーマンスのためにネイティブクラスを使用する必要があります。 たとえば、大きなビットマップのズームとスクロールでFMXコンポーネントで実行されるビューポートが著しく遅くなるという事実に直面したため、NSImageViewを内部に持つネイティブのNSScrollViewに置き換えました。 ネイティブクラスのイベントにアクセスするには、サブクラス化するか、デリゲートを使用する必要があります。 これはDelphiで非常に簡単にエンコードされるため、イベントにアクセスできます。 NSImageViewクラスのmagnifyWithEventイベントが必要ですか? 問題ありません。 インターフェイスを継承します。



 NSImageViewEx = interface(NSImageView) ['{3E4F87DA-0577-4F21-A1CF-8BCA774FA903}'] procedure magnifyWithEvent(event: NSEvent); cdecl; end;
      
      





実装クラスを作成します。



 TExtendedNSImageView = class(TOCLocal) … public procedure magnifyWithEvent(event: NSEvent); cdecl; … end;
      
      





そして、実装クラスのメソッドが呼び出されたときに、私たちが望むことを何でもします:



 procedure TExtendedNSImageView.magnifyWithEvent(event: NSEvent); begin // Do whatever you want end;
      
      





これが機能するには、クラスを作成するときにさらにコードが必要です。 例はインターネットで簡単に見つけることができます。 イベントをインターセプトする方法はサブクラス化だけではありません。メソッドスウィズリングを使用することもできます。以下に例を示します。



それは、ネイティブコントロールとFMXコントロールを組み合わせた、私たちの生き方です。



macOS上のTamoGraph








(これまで)macOSでDelphiができないこと



蜂蜜の大きな樽には、しばしば、それほど美しくない物質がある程度含まれています。 変更の欠陥について話しましょう。



これまでに未解決の問題がありますが、非常に重要です。 これはmacOS用の64ビットコンパイラであり、ロードマップには含まれていますが、まだ実行されていません。 もちろん、これはIdera / Embarcaderoにとって恥ずべきことです。私たちの意見では、製品のMacブランチを無視して、あまり重要ではないことに熱心です。 楽しみにしています。



ソリューション- コードブロック 、DelphiでサポートされていないC ++およびObjective-Cの言語機能。 より正確には、Delphiには独自のアナログコードブロックがありますが、macOS APIから期待されるコードブロックとは互換性がありません。 実際、多くのクラスには、完了ハンドラーとしてコードブロックを使用する関数があります。 最も単純な例は、NSSavePanelおよびNSOpenPanelクラスのbeginWithCompletionHandlerです。 送信されたコードブロックは、ダイアログが閉じたときに実行されます。



 - (IBAction)openExistingDocument:(id)sender { NSOpenPanel* panel = [NSOpenPanel openPanel]; // This method displays the panel and returns immediately. // The completion handler is called when the user selects an // item or cancels the panel. [panel beginWithCompletionHandler:^(NSInteger result){ if (result == NSFileHandlingPanelOKButton) { NSURL* theDoc = [[panel URLs] objectAtIndex:0]; // Open the document. } }]; }
      
      





Delphiでは、このような「耳を使ったトリック」は明らかに非常に問題があります(少なくとも、私たちはできませんでした)。 言い換えれば、通常の方法では、ダイアログを閉じることについて知ることができません。 しかし、通常の方法は退屈ですらあります! 誰が私たちを狂ったように行かせないのですか? そのような問題を解決するために、いくつかの倒錯したアプローチがありますが、この場合、例えば、以下はうまくいきます。 まず、NSSavePanelクラスのすべての機能(ドキュメント化されたものとドキュメント化されていないもの)のリストを取得できます。 これは次のように行われます。



 function ListMethodsForClass(const aClassName: string): string; var aClass: Pointer; OutCount, i: integer; Arr: PPointerArray; p: PAnsiChar; begin result:= 'Instance methods for class ' + aClassName + ':' + #13#10; aClass := objc_getClass(PAnsiChar(ansistring(aClassName))); if aClass <> nil then begin Arr:= class_copyMethodList(aClass, OutCount); if Arr <> nil then begin for i := 0 to OutCount - 1 do begin p:= sel_getName(method_getName(Arr^[i])); result:= result + string(p) + #13#10; end; Posix.Stdlib.free(Arr); end; result:= result + 'Class methods:' + #13#10; Arr:= class_copyMethodList(object_getClass(aClass), OutCount); if Arr <> nil then begin for i := 0 to OutCount - 1 do begin p:= sel_getName(method_getName(Arr^[i])); result:= result + string(p) + #13#10; end; Posix.Stdlib.free(Arr); end; end; end;
      
      





リストを取得して、おいしいものを探しています...ええ、「_ didEndSheet:returnCode:contextInfo:」が見つかりました。 必要なものと非常に似ています。 ダイアログが閉じられたときにこのセレクターが呼び出されるかどうか、理論を確認する必要があります。 NSSavePanelのサブクラスを作成するか、このセレクターに大まかに恥知らずにフックを設定して、メソッドの実装を置き換えることができます(メソッドのシズリング):



 const END_SHEET_SELECTOR : ansistring = '_didEndSheet:returnCode:contextInfo:'; SAVE_PANEL_CLASS : ansistring = 'NSSavePanel'; var endSheetOld: procedure (self: pointer; _cmd: pointer; sheet: pointer; returncode: NSinteger; contextinfo: pointer); cdecl; procedure endSheetNew (self: pointer; _cmd: pointer; sheet: pointer; returncode: NSinteger; contextinfo: pointer); cdecl; begin endSheetOld(self, _cmd, sheet, returncode, contextinfo); FDialogClosed:= ReturnCode; end; procedure DoDialogHooks(); var FM1, aClass: pointer; begin aClass := objc_getClass(PAnsiChar(SAVE_PANEL_CLASS)); if aClass <> nil then begin FM1 := class_getInstanceMethod(aClass, sel_getUid(PAnsiChar(END_SHEET_SELECTOR))); if FM1 <> nil then begin @endSheetOld := method_getImplementation(FM1); method_setImplementation(FM1, @endSheetNew); end else raise Exception.Create('Failed to hook NSSavePanel'); end; end;
      
      





確認します-そして、見よ、キャンセルまたはOKでダイアログを閉じる瞬間に、フックされた関数に入り、それに応じて、ダイアログと閉じた結果自体が閉じていることがわかります。



鉱山



おそらく、誰も鉱山に爆破されずに製品を作成することはできませんでしたが、特にもう一度強調しますが、他の人のコードをもっと見て、本やAPIリファレンスを読むと、爆発の数を最小限に抑えることができます。 いいえ、実際には、 developer.apple.comでApp Napを読むのではなく、それを読んでから、アプリケーションのすべてのタイマーが突然10倍少なくなるのはなぜかと長い間考えた方がよいでしょう。 また、POSIX関数の文字列パラメーターは、ANSIまたはUTF-16ではなく、UTF-8エンコードで送信する必要があることを事前に知っておくとよいでしょう。 そして、テスト、テスト、テスト...さらに、「あなた自身のために、そしてその人のために」。 はい、Delphiにも鉱山があります。Idera/ Embarcaderoは、製品のMac部分をテストすることを本当に好みません。 テストが正常に構成されていれば、Macapi.Foundation.NSMakeRectはそれらに該当しません。



まとめ



macOS向けの製品の作成方法を検討している人にとって、Delphi + Cocoaの最初の知り合いが参考になることを願っています。 バンドルは非常に機能しており、本格的なソフトウェアを作成できます。 Idera / Embarcaderoへの私の願い-macOSを忘れないでください。 モバイル開発は非常にファッショナブルであることを理解していますが、過去20年間のWindowsの例からわかるように、デスクトップソフトウェア開発は非常にまともな市場です。macOS向けの優れた製品にはほとんどすべてのものがあります。 むしろ、64ビットコンパイラを展開し、 quality.embarcadero.comで伝えられた内容を修正します。



All Articles