2015年11月28日 星期六

如何建立自己的 Xcode .a 檔案

在使用 Delphi 建立自己的 app 時, 有時候會需要使用到第三方廠商的 source code, 這些 source 如果是用 Xcode objective-c 撰寫, 用來提供 iOS 的某些功能, 直接使用這些驗證過的程式碼, 會比自己照著原始碼重寫一次來的快、來的可靠。


所以,最快的方法就是用 Xcode 把這些原始碼 (通常會是 .m 跟 .h 檔案) 建立一個 .a 檔案, .a 檔案是靜態函式庫, 等同於在 C 語言裡面的 .lib 檔, 連結後就不能動態改變執行檔的內容了。

以下是建立 .a 檔案的簡單步驟說明:

1.     使用 XCode建立一個新的專案

      

2.     選擇 Framework & Library -> Cocoa Touch Static Library, 然後選Next












3.     填寫 library 的基本資料, 任填就行了.
 
4.     選擇一個 folder 來存放專案檔案, 我選擇在 iOS SDK8 目錄下, Xcode會自動建立專案目錄

5.     finder找到要加入的.m .h 檔案, 拖拉進 Xcode 專案中


6.     選擇編譯的專案組態, 必須對Device Simulator分別都進行 Build.



7.     Device Simulator 都編譯好了之後, 找到製作好的 .a 檔案, 快速找尋方法: Products 點選 lib, 然後用滑鼠右鍵點選它, 選擇 Show In Finder, 就會出現一個新的 Finder 視窗, 顯示該檔案的位置。





8.     上面的 Finder 是顯示 Debug-iphones Debug-iphonesimulator兩個目錄裡面的libmyPackage.a 檔案.
9.     iphoneos 目錄裡面的, 是給實際裝置使用的 .a 檔案, iphonesimulator目錄裡面的, 是給模擬器使用的 .a 檔案, 我們可以透過以下的 command line 來把兩個檔案合一, 檔案會變大, 但就不用選擇不同的檔案了, 這個指令的用法是 lipo –create 第一個.a 第二個.a… -output 合併檔案

Mac OSX 裡面, Terminal (終端機)有個很方便的設計, 就是可以從 finder 把檔案直接拖拉到終端機畫面, 可以直接把該檔案的完整路徑拖拉進去:




然後 key in –output, 再拖拉 folder 進去:

10. 輸入檔名, 剛剛指令已經完成輸入到 –output 目錄 , 我們把檔案名輸入一下, 本例中是 libmyPackage.a
11. 按下 Enter, lipo 就會執行, 把兩個 .a 檔案合併成同一個, 完成。

補充: 預設建立的 .a 檔案是 debug 模式的, 裡面會有 debug information, 如果要建立 Release 的版本, 則需要修改一下 Build Configuration:


點選 Edit Scheme, 會出現設定畫面:
下拉 Build Configuration, 選擇 Release 即可.
重複6-12步驟, 這次要選擇 Release-iphoneos Release-iphonesimulator當中的 .a檔案, 建立出來的 .a , 就會是 Release 版本的了.

注意事項: 如果第三方的 source code 裡面有使用到 Delphi IDE 預設匯入的 iOS framework 以外的 framework, 也要記得把他們透過 Delphi IDE 的 Option->Tool->SDK Manager 匯入, 才能正確運作喔.

2015年10月22日 星期四

Indy - TCP 之一 - 以 TIdTCPServer 自製檔案伺服器 (1)

緣起

自 2001年9月, 我的第四本書- Delphi/Kylix Indy 網際網路程式設計出版之後,忙工作,忙教學,不知不覺也過了十幾年。 (好可怕啊~~~ 時光艿苒就是這種感覺嗎?)

在這期間我也曾經發過幾次神經,想要把該書改版一下,當時撰寫的時候,那時空背景是 Delphi 7, Kylix 3, Indy 8.0.25 轉 Indy 9 的時候,現在 Delphi XE7 都舉行了預覽發表會,Kylix 已經緲緲不知所蹤,Indy 都到 10.5 以後的版本了。

但曾和幾家台灣地區較為知名的出版社接洽出版,得到的回音不外乎:『目前還有人在寫 Delphi 喔?』『現在出寫程式的書沒有人要買了,如果你寫 PhotoShop 怎麼修照片的書,我們就出』,而我對於 PhotoShop 的使用,只停留在剪貼、放大縮小、模糊、去背的基本使用,所以還是不獻醜了。

從 2001年到現在2014年,13個年頭過去了,對於Indy相關的應用不少,但真正用到Indy強大功能的不多。這幾年從沒間斷在程式寫作上的浸淫,Delphi 除了 2009-2011之間,我把重心稍微轉到 Objective-C 的階段之外,也從來沒放下過,但看到對Indy真的運用的很棒,居然只有 Embarcadero.

別說 Embarcadero 是原廠,所以他們就自然會對Indy 熟悉,這是牽強的說法,因為Indy從 WinShoes 開始,都是OpenSource 的軟體元件,只要你肯花點時間,一定能很快熟悉它,甚至寫些元件發佈給 Indy 用,君不見,TIdDNSServer 就是筆者所寫?

扯遠了,拉回來。

Embarcadero 這幾年在 Delphi XE3 之後新增的 Server 端元件,不僅是傳統的 DataSnap server 支援了 TCP/IP與 REST 兩種模式,用的就是 Indy 元件來寫的,大家可以花點時間進去 C:\Program Files (x86)\Embarcadero\Studio\14.0\source 這個目錄看看, 裡頭真的花了很多心思,用 Indy 用的非常精闢。

舜何人? 予何人? 有為者亦若是!

筆者可能算是比較老派的,所以老愛掉書包,我們也別落人後,今天就來個範例,讓大家寫寫 Indy 小程式,做做傳遞檔案的功能吧。

可能您會說:我用 FTP Server 就好了,我用 Apache 加 PHP 用 Upload File 來做就好了等等。

我的回應是:都很好,能達成功能,不傷身體就好,但多學一種方法也不錯,最近不是有個廣告,比別人多會一招,勝算就多一成?

就來多學這一招半式吧。

使用時機

什麼時候會用到這東西? Indy 會飛嗎?

別再亂鬧了,哪天你的專案需要傳檔案,你就等著哭吧!

如果哪天專案要把資料回傳到 Server, 而這些資料又是檔案,架設 FTP 是OK的,到時候你還是得寫個 FTP Client 來上傳, 而且設定也還是得花時間寫的。

不多說,來看一個簡單的 TCP Server 是怎麼運作的吧,FTP Server 跟 HTTP Server 都不例外喔。

TCP Server 的運作,就是從啟動開始,先建立一些等待的 Queue, 也就是上圖裡面的等待隊序,像 TIdTCPServer 預設的 ListenQueue 數量就是 15 個, 當然可以隨我們的需要來修改這個數字。

在等待的過程當中,一旦有使用者連線進來了,就會由其中一個線程來處理這個使用者的需求,等這個使用者的需求處理完畢,該連線就會由 Client 或 Server 端來切斷。

命令有哪些?

這個問題很好,既然現在我們正在製作一個新的 Server, 也就沒有傳統命令的包袱,我們可以自己來決定,以下我們假設要讓特定的 Client 端傳送檔案到 Server 來,所以可以來定義幾個簡單的命令,你也可以把這些個命令當成通訊協定,因為本來這就是一個自定的通訊協定。

我們定義有以下幾個簡單的命令:
filename
filereplace
fileresume
sendfile

命令的順序要正確,不然就從 server 端強制斷線,我們定義順序如下:
Client                                                          Server
 "sendfile Open-Sesume" -------------------->

             <----------------------------- "200, OK, Please send file name, size, data"

"filename 要存放在 server 端的檔案路徑"--->

             <----------------------------- "200, ok, ready for get file: 檔案路徑"

"filesize 檔案大小" ------------------------------>

"檔案內容" ---------------------------------------->

             <----------------------------- "200, ok, file accepted"

Server 端只存放 filesize 後面所帶的數字描述的資料量,多的全放棄,接著,Server 斷線。

講到這裡,概念說完了,就是 Client 跟 Server 之間把字串送來送去,如此而已,接著,我們要來看怎麼用 Indy 達成這個要求了,請接著看Part2.

2015年5月11日 星期一

Delphi XE7, XE8 在 Windows XP 上面佈建時要注意的事項

緣起

之前有個專案, 目標平台是 Windows XP, 這個專案的 Scope 是要把原本記載在 XP 上面的 Access MDB 檔案資料, 透過 Someway 回傳到 Web server 上, 好讓管理人員能夠 Centralize 管理各個不同節點的資料.

好死不死, 我的 Windows 開發系統早從 2010 年起就已經全面 VM 化, 且從那時候就一直保留在 Windows 7 上面, 因為真的又快又穩定.

直到 2014 年, 我的開發系統已經是 Windows 7 + Delphi XE7, 所以對於 Windows XP 已經有種日薄西山的感覺.

在開發這個系統的過程當中, 並沒有特別注意什麼, 就很順利的用 FireDAC 連接遠端的 MySQL server,  Client 端用 idHTTPServer 作為 WebServer, 中央的 Web Server 在用戶從網頁進行資料同步索取的時候, 發一個 jquery 向 idHTTPServer 指定要索取的資料的日期、並且給 session string, 以及資料的 offset number.

IdHTTPServer 在接到 Request 的時候, 透過 TIdCommander, 建立一個 Thread, 把 server 端傳來的資料 select 出來, 然後一筆一筆寫成單一一個 Insert 指令, 用 FireDAC 連接 MySQL 資料庫, 直接執行 Insert 指令, 把資料全寫進去.

在 Windows 7, Windows 8 上面, 執行的超快樂的, 去年的實習生對於這種架構, 完全就是看傻了眼, 壓根沒想過能這樣搞, 但跑的超順的, 他也 Happy 的把 PHP 程式搞定了.


直到去幫業主做 Deployment 的時候, 這下是我傻眼了, 四個營業點, 用的系統都是 Windows XP, 其中三個更是 Windows XP HOME.......

看倌們應該有些已經跟我一樣, 心裡咒罵連連了吧.
Windows XP 也就算了, Windows XP "HOME", 這就是說, 系統一開始就沒打算讓外面的機器或網路能夠連進去 (當然網路芳鄰可能是唯一例外吧, 是叫我們這些寫 TCP/IP Protocol 去跳樓嗎?)

和業主進行反映之後, 業主先跟設備商詢問是否有升級為 Windows Vista/Windows 7/Windows 8的可能性?

設備商的反應是: 沒辦法, 因為他們的 VB6 程式, 在控制實體設備的時候, 用的是 Windows Message 來傳遞同步資訊跟指令的, 他們不會, 也沒有打算升級到 Windows XP 之後的技術.

這一點, 筆者在 2008 年有吃到苦頭, 在 2 個月當中, 把原本用來做 IPC (Inter-Process Communication) 的 Windows Message 改成 Named Pipe, 這是在 Vista 把 Windows 的 Session 改為真實多使用者 Session 之後的重大變革.

在 Windows XP 當中, 雖然可以有多使用者的假象, 但實際上, 所有登入、登出的使用者都還是共用同一個 Session, 也就是 Session 0, 因此驅動程式、病毒、駭客, 都很熟悉透過 Windows Message 來傳遞訊息.

回憶 2007 年的時候, Vista 剛推出,Nvidia, AMD 哀鴻遍野, 因為這兩家視訊卡大廠的驅動程式跟工具程式也是完全透過 Windows Message 作為傳遞介面跟工具程式之間訊息的管道, 直到 2008 年中, 整整過了半年多, 新版能支援 Vista 的驅動程式跟工具程式才 release...

WIC 元件


這是多說的了, 回頭說, 在 Delphi XE7 的 FireMonkey, 要 Deploy 到 Windows XP 的時候, 可能會遇到直接 Crash 的狀況, 我自己也在此卡關許久, 後來發現, 原來是需要自己在Windows XP 上面, 安裝 WIC 元件, 這個元件的取得路徑如下:

http://www.microsoft.com/zh-CN/download/confirmation.aspx?id=32

只有簡體中文版的網頁, 但下載回來, 安裝的都是 enu, 也就是英文版, 反正沒有什麼複雜的操作要進行, 不斷的按下一步就行了.

這個情形會發生在一執行的時候, 而且沒有什麼訊息提示, 就跟一般我們遇到 Memory Access Violation 的時候差不多, 只會在 XP 上面說要回報給 Microsoft, 但 Microsoft 已經停止了對 Windows 的支援, 所以回傳也沒什麼幫助.

我是後來想了很久, 覺得不對, 在透過 PAServer 從 Windows 8 VM 去連 Windows XP VM做遠端偵錯的時候, 才從 Delphi Debug message 裡面找到了這個錯誤訊息, 不然真的不知道為什麼會 Crash.....

我懷疑過的點挺多的, 例如 MDAC 版本不對, MySQL dll 版本不對, 什麼都試過了, 但是在業主的營業點沒得測試, 所以這次用完全乾淨的 Windows XP Pro 來做這個驗證, 得到這個經驗, 或許遇到這情形的先進不多, 但他山之石, 可以攻錯, 所以也把這個體驗留在這個園地, 讓大家有需要的時候可以互相分享一下.

2015年1月6日 星期二

Get HTML/JSON from TWebBrowser FireMonkey (Delphi XE7)

Starting


FireMonkey starts from Delphi XE2, this new framework provides many powerful components and runtime library, and the most important point is, most of the components can be used cross-platform.

XE2 -> iOS fundmental features ready.
XE3-XE5 -> Android features were appended.
XE6 -> Almost features were ready, but minor performance issue remained.
XE7 -> Everything is ready, we still need some completion for couple components.

With mobile apps development, we need to connect the web server from time to time. Indy components play as a proper role for Delphi and C++ Builder, but the SSL feature is not perfect yet.

Hence, we still need TWebBrowser for user to interact with website, if the feedback or redirect path contains HTTPS URL, e.g., Facebook login, Google+ login.

However, there is no interfaces provided by TWebBrowser for retrieving the content of the page.

A->B, B->C, C->D

It's said in some movies, "Genius can get A to D without intermediate path, other cannot", I am not genius, so I have to find out A->B, B->C, C->D to achieve A->D.

The following is my thought:
A->D, A is the TWebBrowser, D is retrieveing content.

TWebBrowser cannot get content, but it can run Javascript without return value.
Javascript can get the content, but there is no way to send the content back to Delphi.
TWebBrowser can get the URL, which the browser wish to navigate.
Javascript can redirect the web browser to another specific URL.

So, the workaround is:
1. waiting for the target URL we want to get the content, e.g. the Google+ login result page.

http://www.moveinpocket.com/demo/GoogleLogin_API/index2.php (login page)
https://www.googleapis.com/oauth2/v1/userinfo?access_token= (login success page, the user information will be sent back with JSON format.)

2. if the login success page is done, the onDidFinishLoad event handler will handle the JSON Page:
if (Pos('https://www.googleapis.com/oauth2/v1/userinfo?access_token=',
        self.WebBrowser1.URL) = 1) then begin

            js := 'var markup = document.documentElement.innerText;' + #13 + #10
                + 'var newURL = "http://1.1.1.1/" + markup;' + #13 + #10 +
                'window.location = newURL;';
            self.WebBrowser1.EvaluateJavaScript(js);
    end;

3. Javascript redirect the browser to http://1.1.1.1/ (you can modify it to another http url), and append the JSON data (innerText in above codes) to the URL.

4. Let onShouldStartLoadURL of Browser to get "http://1.1.1.1/" url, then we got the content in Delphi!
if (Pos('http://1.1.1.1/', URL) = 1) then begin
        self.WebBrowser1.Height := 1;
        jsonStr := URL;
        Fetch(jsonStr, 'http://1.1.1.1/');

        jsonStr := TIdURI.URLDecode(jsonStr, IndyTextEncoding_UTF8);

        ShowMessage('got it');
        self.Memo1.Lines.Text := jsonStr;
        self.TabControl1.ActiveTab := self.TabItemResult;
    end;
I had search some work around with the following key words in Google, Stackoverflow, and it's pity that there is no any solution yet:
Delphi, FireMonkey, WebBrowser, get HTML, get JSON, get Content.

So, this work around might be the only way to get content from TWebBrowser, if you need the sample project, please download from here.

Best Regard, and Enjoy your Delphi.
Dennies Chang.

如何用 FireMonkey 的 TWebBrowser 取回 JSON 資料

緣起

從 Delphi XE2 開始, FireMonkey就包含了跨平台的TWebBrowser元件(Windows版本跟行動版本是分開的, 我記得Windows版本的 TWebBrowser 元件, 是 VCL 版本的 ActiveX),但從 XE2 開始到 XE6, 每一版的 TWebBrowser 都少了點東西.

XE2到XE4都沒有Android版本的WebBrowser, 到 XE5 總算行動平台的 WebBrowser 都具備了, 但在網頁當中的 Form 輸入文字, 卻有選擇了文字無法傳送到文字框的缺失, 這個問題在 XE6 被解決掉了,XE6 的 WebBrowser 又有了效能不夠好的問題。

到了 XE7,終於大多數問題都被解決掉了,也加入了強制執行 Javascript 的功能,可謂十全八美 (按:iOS執行 Javascript之後,可以把執行後的字串當成回傳值讓 UIWebBrowserDelegate 處理,Delphi XE7 目前還不行)。

另外,也還無法直接 access WebBrowser 裡面的內容,在以往我們使用 VCL 版本的 TWebBrowser 時,我們至少還可以透過 ActiveX 介面取得 innerHTML 或者 innerText,然而目前在行動裝置上,FireMonkey 並沒有可以讓我們可以取得內容的途徑。

由A到B 由B到C 由C到D的轉折

俗話說「山不轉路轉」,TWebBrowser沒有可以 access 內容的途徑,但是Javascript有。Javascript 不能把字串直接傳給 Delphi 程式碼,但是可以把字串當成Javascript 的變數。TWebBrowser 不能直接取得內容,但可以取得網址。

所以,筆者兜兜轉轉,拼湊出了這麼一套方法:
1. 先確定進入了我們需要的內容網址,筆者以自己寫的 Google+ 登入網頁作為範例。
http://www.moveinpocket.com/demo/GoogleLogin_API/index2.php (登入)
https://www.googleapis.com/oauth2/v1/userinfo?access_token= (登入成功頁, 會以 JSON 格式回傳使用者的資料)
2. 檢查是否進入了登入成功頁,如果進入了,就讓 WebBrowser 執行Javascript,把 innerText 抓出來當字串變數。
if (Pos('https://www.googleapis.com/oauth2/v1/userinfo?access_token=',
        self.WebBrowser1.URL) = 1) then begin

            js := 'var markup = document.documentElement.innerText;' + #13 + #10
                + 'var newURL = "http://1.1.1.1/" + markup;' + #13 + #10 +
                'window.location = newURL;';
            self.WebBrowser1.EvaluateJavaScript(js);
    end;
3. 用 Javascript 的 redirect 方法:windows.location,轉到一個不存在的網址,把innerText 當成該網址的參數,不用名字,直接接在網址後面就行了。
4. 透過 TWebBrowser 的 ShouldStartLoadURL, 就可以抓到這串字了。
if (Pos('http://1.1.1.1/', URL) = 1) then begin
        self.WebBrowser1.Height := 1;
        jsonStr := URL;
        Fetch(jsonStr, 'http://1.1.1.1/');

        jsonStr := TIdURI.URLDecode(jsonStr, IndyTextEncoding_UTF8);

        ShowMessage('got it');
        self.Memo1.Lines.Text := jsonStr;
        self.TabControl1.ActiveTab := self.TabItemResult;
    end;

也太費功夫了吧⋯⋯ 不過沒辦法,這方法還真的能抓到TWebBrowser的內容喔。

使用時機

大家可能會問我:為什麼不用IdHttp,或者 TRESTClient來抓資料,這麼費神幹嘛,你自己不是Indy的愛用者嗎?

是的,筆者愛用Indy是事實,也還寫了Indy的書沒有錯,但有時候還是會有需要用TWebBrowser的時候,像是如果需要用 Google+ 當作登入頁,試問除了TWebBrowser,還有什麼方式可以做到?

這時候千萬別回我「用IdHTTP」喔,在行動裝置上的IdHTTP,對https網址是無法載入的,不信可以試試看。所以認命點,這是你在全球唯一能找到的解決方法了,問問我怎麼這麼有自信?因為我才剛在寫這篇文章之前做過功課,從 stackoverflow,到 edn,到Marco cantu的 Blog,expert-exchange,KTOP我都找過了,到2015年一月,目前也只有這個解法了,請相信!

以下,附上範例程式碼,太長的內容我不保證一定成功喔,但JSON資料還OK啦。
本篇文章範例程式專案