顯示具有 Indy 標籤的文章。 顯示所有文章
顯示具有 Indy 標籤的文章。 顯示所有文章

2017年6月9日 星期五

使用 TRESTClient 與 TRESTRequest 作為 HTTP Client 之一 (POST 字串參數)

在 Delphi XE 推出以前的年代,Delphi的發展方向是筆直朝向資料庫連結Windows 應用程式這個目標不斷前進的,從Delphi 1開始,到Delphi 7,Delphi奠定了VB Killer的外號,主要依靠的就是與資料庫的連接功能超越其他開發工具,而且超越的距離不只一個世代。

在 .NET開始發展,Delphi 8, Delphi .NET 不斷延遲的時候,與資料庫連接功能的方便性,仍然讓許多ERP廠商、軟硬體廠商持續愛用 Delphi.

直到 Web 開發與 App 開發超越了 Windows 應用程式的需求,VC, VB, Delphi 也開始隨著這波潮流,漸漸不再像 1990年代那麼廣受愛戴了。

在1990到 2010年之間,Delphi的網路連線功能,主要是藉由第三方元件來提供的,其中知名度最高,全球使用人數也最多的,應該就是 Indy 這套元件了。

這套元件在 2000 年前,叫做 WinShose,從第八版之後才改名為Indy,全球投入這套元件開發的開發人員,前後超過40人,從最基礎的 TCP/IP 功能到各種協定的Client與Server 端元件,筆者從中得益非常多,也開發了當中的DNS Server元件,對通訊協定的深入了解,Indy團隊可說是我不可或缺的師長。

隨著Delphi 工具走入了多平台開發的領域,Indy的侷限性也在這兩三年凸顯了出來,主要是在各個作業系統上面對於SSL與加密功能的支援無法緊密結合到作業系統內建的功能所致。

由於這個局限性,Delphi XE6開始,REST Client系列元件漸漸開始成為 Delphi 團隊的重點開發項目之一,所以我們從 Delphi XE6, Delphi XE7 之後的版本,可以發現到,使用 TRESTClient, TRESTRequest, TRESTResponse 系列組合的應用程式越來越多了,原廠也不斷鼓勵大家使用這套元件來提供 REST API 的連線功能。

REST API 的基礎是 HTTP 協定,大多以 HTTP 的 POST 方法把 JSON 編碼形式的參數傳遞到 Server,而 Server 再以 JSON 形式的參數回傳。

有時作法也會稍有變化,例如以 POST 方法把 Web-Form 編碼形式的參數傳遞給 Server,Server 再以 JSON 形式把資料回傳。

形式不一而足,但相同的是 HTTP 協定,最常用的也是以 POST 方法把參數傳給 Server 端。

今天要跟大家分享的主題,則是如何『使用 TRESTClient 與 TRESTRequest 作為 HTTP Client』。

前面已經提到過,在沒有 TRESTClient 整組元件以前,我們通常用的是 Indy 系列的元件來提供網路傳輸的功能,而現在有了 TRESTClient 整組元件,我們在行動平台上面就可以不需要另外配置函式庫,也能夠直接使用 https 與 server 連線了,在勒索病毒氾濫的今天,使用 https 會讓使用者比較安心。

POST作業說明

在 HTTP 的 POST 作業當中,參數跟 GET 作業一樣,Client端需要以 name=value&name2=value2 這種形式進行字串連接,再傳送到 Server 端去。Get 跟 POST的差異,在於 Get 方法是把所有參數當做 URL 的一部分,發送 HTTP GET 指令的時候,參數連同 URL  一起傳送。

而 POST 作業則是發送完 POST 指令後,把所有的參數與資料隨之傳送。依照 HTTP 型定的規範,GET 作業的 URL 是無法加密的,而且長度也有限制。因此,當需要傳遞的資料比較多,或者有機敏性,透過 HTTPS 傳送,就是最直接,也最方便,更是目前最通用的資料保護方法。

透過 POST 傳遞的參數,除了字串以外,還常常包含了檔案傳遞。我們很常看到在網頁上面以按鈕提供使用者選擇要上傳的檔案,也常看見提供以拖拉的方式把檔案上傳到遠端系統,尤其網頁郵件系統最常見到這種作法。

過去以 TIdHTTP 元件的 POST 方法發送參數時,呼叫方式如下:
var
    httpClient : TIdHTTP;
    url, params, httpResultStr : string;
begin
    url := 'http://mytestURL.com/test.php';
    params := 'name=我的名字&test=測試';

    httpClient := TIdHTTP.Create(self);
    try
         httpResultStr := httpClient.Post(url, params);
         showMessage(httpResultStr);
    finally
        httpClient.Free;
    end;
end;
  
這樣就可以把 params 字串的眾參數傳到 server 去了。理論上是這樣沒錯,但事情並沒有這麼簡單,在 HTTP 協定當中要傳參數給 Server,如果這些字串包含了特殊字元,則必須要先經過編碼,而編碼,是我們一生都需要與之對抗的繁複程序。

在 HTTP GET 方法當中,所有的參數除了要以 name=value 對每一個參數做描述,以及需要用 & 來連接各組參數,所有的 value 都需要以 url encode 來擺脫 URL 保留字元的糾纏。name 是否需要編碼呢?筆者建議,name 就乖乖的用英文吧,可以省下很多問題,以及處理這些可避免的問題所需要的時間!

那麼同樣的功能,以 TRESTClient 跟 TRESTRequest 要怎麼達成呢? 也很容易,作法如下:
1. 在 form 裡面放上 TRESTClient 跟 TRESTRequest 元件各一。
2. 把要傳遞的參數加到 TRESTRequest 實體的 params 屬性裡面去,這個屬性的型別是 TArray,所以可以存放多組參數。
3. 設定 TRESTClient 要傳送參數的URL,注意,URL 是設定在TRESTClient 哦!
4. 設定 TRESTRequest 要使用的傳輸方法,要設定為POST(因為我們正在介紹的是POST方法,請按照您的需求調整)
5. 呼叫 TRESTRequest 實體的 execute 方法,就可以把資料送去 server 了。

寫成 Delphi 的程式碼,會像以下這樣:
self.RESTClient1.BaseURL :=
          'http://我的網址/acceptNewCard.php';
self.RESTRequest1.Params.Clear;
self.RESTRequest1.Method := rmPOST;

self.RESTRequest1.AddParameter('test', self.EditCardNo.Text);
self.RESTRequest1.AddParameter('name', self.EditName.Text);

是不是很容易呢?的確很容易,裡頭的問題我們等下再深入探討,先來看 server 端要怎麼接收這些個參數,我們用 PHP 當範例,需要用 C#,JSP的讀者朋友們請自行轉譯喔⋯⋯

PHP Server 端接收 POST 參數的方法
從 1994 年開始,筆者就陸續撰文說明 HTTP POST 方法如何接參數,包含了CGI 用C,perl等語言實作,也包含 ISAPI 以 Delphi 實作,近幾年比較流行的是 PHP,JSP,C#,但 PHP 程式碼讀起來比較簡潔易懂,所以我就選擇 PHP 來做範例了。

在 PHP 裡面,透過GET 跟 POST 方法傳遞的參數,會被分別存放在 $_GET 跟 $_POST 這兩個陣列變數裡面,如果要偷懶,不想區分 GET 或 POST 方法,也可以從 $_REQUEST 這個變數試著讀取,當中有些安全性考量,最好勤勞一點,把它們區分開來。

以剛剛的例子來看,我們傳了一個名為 name,以及一個名為 test 的字串,用的是 POST 方法,所以我們得用以下兩個變數來存取這兩個字串:
  • $_POST['name']  這個變數可以取得 Client 端發送出來的 name  
  • $_POST['test']  這個變數可以取得 Client 端發送出來的 test
所以在 server 端,我們可以這樣寫,來抓到這兩個資料:
$name = $_POST["name"];
$test = $_POST["test"];

這樣寫會不會出問題呢? 答案是不會!如果使用者不輸入中文的話!

中文資料的編碼處理 

Delphi的開發人員絕大多數都是英美語系的人,我推測因此對於亞洲語系的文字顯示與傳輸比較沒有辦法完整的測試,但對於我們以中文為母語的人來說,從電腦誕生的那個年代,中文的顯示在每個作業系統、每種通訊協定的設計都比英文協定來的困難。

以上面的例子來看,如果我們直接拿這個例子來測試,筆者寫的範例程式,執行傳輸資料時,Server 所抓到的文字並不是正確的中文字,如下圖所示:

可以看得出來,傳到 server 的時候,server 是讀不到資訊的。這是怎麼回事呢?筆者屬於不認輸的好奇寶寶,使出了渾身解數,終於解決了這個問題。

寫過 Web 程式的讀者們一定可以立刻推測出來,這絕對是文字編碼出問題了,然而,是什麼地方出問題?可能出問題的點我列出來跟大家分享:
  • HTTP Client 的 charset 設定錯了
  • HTTP Request 裡面的文字編碼出問題
檢查的方向也是從這兩個關鍵點出發,第一點的檢測很容易,從Object Inspector檢查一下 RESTRequest1的設定:

AcceptCharset 確定是 UTF-8,沒錯,所以設定不是問題。

接著,就要從 Client 端發出去的資料下手了。有讀者或許會問『你怎麼不懷疑Server端程式寫錯了?』這個問題很好,之所以排除了這個問題,是因為同一個 Server 端的 PHP 程式,我用了 Postman 做過比對測試,回傳的結果是正確的,因此判定是 Client 端程式的問題。

接著筆者從 TRESTRequest.AddParameter 的各種多載形式來嘗試,AddParameter 這個方法有以下幾種多載的形式:
  • procedure AddParameter(const AName, AValue: string); overload;
  • procedure AddParameter(const AName: string; AJsonObject: TJSONObject; AFreeJson: boolean = true); overload;
  • procedure AddParameter(const AName, AValue: string; const AKind: TRESTRequestParameterKind); overload;
三種形式我都測試過,從 AddParameter 的執行中 trace 進去看各個可能性,由於 TRESTRequest 的參數中,Get 跟 Post 的加入方法是混用的,在程式碼裡面編碼又會有點不同。

在 REST.Client.pas 裡面,我曾經懷疑過編碼錯誤,所以也在執行階段對各個變數都進行觀察,最後,找到了原因與解法,至於過程,就不多說了,花了我兩天咧。

原因:編碼錯誤

用HTTP傳遞中文的時候,務必用UTF-8編碼,但一定要記得,中文字在作業系統中,都是UCS32編碼,這個現象在Windows裡面如此,在Android裡面如此,在iOS跟Mac我不確定,但處理方法是一樣的。

直接以 AddParameter('name', '中文測試'); 把參數加進 TRESTRequest 的時候,REST.Client.pas 的程式碼是把 '中文測試' 這個字串直接抓 Ord 的資料來做編碼的,然而,這個作法,是錯的!!!!!!!!

在 HTTP 傳遞 UTF-8 資料的時候,我們要傳遞的是 UTF-8 文字的二進位資料,但直接把 '中文測試' 這個字串直接拿來轉成二進位? 當時編碼並不是 UTF-8 啊,當然怎麼編碼送到 server 都是錯的!!!!

解法:AddParameter之前先做 UTF-8 轉換

這個解法,筆者第一天就已經想到,只是很想像以前改 Indy 程式一樣,直接改好 REST.Client.pas 之後,回饋給原廠使用,所以花了不少時間找方法,最後發現這個方法不用動到 REST.Client.pas,又能正確處理,就直接這麼跟大家分享了,寫成 Delphi 程式碼如下:
var
     nameStr : String;
begin
   ...
   nameStr := TIdURI.ParamsEncode(nameStr, IndyTextEncoding_UTF8);
   self.RESTRequest1.AddParameter('name', nameStr,     
             TRESTRequestParameterKind.pkGETorPOST,    
             [TRESTRequestParameterOption.poDoNotEncode]);
   ...
end;
在把字串透過 AddParameter 加入參數陣列之前,我先把字串做個 UTF-8 轉換,在這裡用的是 TIdURI 的類別方法 ParamsEncode,這個方法只有兩個參數,第一個參數是字串內容,第二個參數則是要文字編碼的種類,在這裡我選擇了 UTF8,寫法就是上面範例程式的第一行。

接著,在呼叫 AddParameter 的時候,我使用了多載形式當中的第三種,要求 AddParameter 處理資料的時候不要再動我的編碼,因為我已經處理好了。

這麼修改過之後,在各個作業系統當中,執行結果都是正確的,上面兩個圖以 Windows 作業系統為例,現在我們拿 Android 截圖來做為例子:


讀者可以看到右邊的截圖裡面,server 回傳的資料已經是正確的中文字了。

最後,我把 PHP 程式碼也附上來給大家參考:
<?php
date_default_timezone_set("Asia/Taipei");
header('Content-Type: charset=utf-8');

include("public/DBClassPDO.php");
$objDBPDO = new DBClassPDO();

$cardNum = $_POST["cardNum"];
$floorIdx = $_POST["floorIdx"];
$name = $_POST["name"];
$houseNum = $_POST["houseNum"];

$params = array();
$params['name'] = $name;
$params['cardno'] = $cardNum;
$params['houseNum'] = $houseNum;
$params['floorIdx'] = $floorIdx;
$params['picFilename'] = $fileSaveName;
$params['created'] = 0;

$result["resultCode"] = "0";
$result["result"] = "成功";
$result["sqlcmd"] = $name;

$jsonStr = json_encode($result);
echo $jsonStr;
?>
這是最基本的傳遞字串,下次再給大家示範怎麼傳檔案,透過 TRESTRequest來做也是很簡單的,TRESTRequest 跟 TRESTClient 的確是取代 TIdHTTP 的好工具。

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年1月6日 星期二

如何用 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啦。
本篇文章範例程式專案

2014年8月29日 星期五

FireMonkey v.s. VCL 之一 - 動態產生視覺元件的差異

在撰寫程式的過程中,對於畫面的製作,會隨著程式經驗與類型的積累,產生幾種演進:
所有畫面都事先在DesignTime,也就是設計階段,把畫面都先處理好
能在設計階段製作的,先在設計階段處理,不行的部分,在RunTime即時產生
所有畫面都在RunTime 產生

演進的狀況不外乎上述三種,但未必每個程式人員都有機會遭遇到全部三種狀況,也很難說那一種做法是功力最高的,因為所有的程式製作方法,都應該視當時的條件來決定製作的方式,這些條件包含了:
開發時程 - 有時候很多專案會被要求在極短的時間之內完成,沒時間,就沒機會使用全RunTime的做法完成。

對FootPrint (整個編譯結果的檔案大小)的要求- 如果遇到只能在非常局限的磁碟空間中安裝,完全RunTime 的做法或許是達成要求的唯一方法。

便於團隊對程式碼的長久維護- 此時就要考慮整個團隊的程式設計習慣,挑選適合的做法。

且不管最後選了哪一種,在RunTime建立畫面的需求都是可能會存在的,所以我們在此對完成這個需求的方法做些介紹。,也就是動態產生元件的方法,並就VCL跟 FireMonkey 兩種架構的差異進行比較。

我們以最簡單的,在 Form 上面建立 Button 的程序進行比較

VCL版:
procedure TForm1.btnAddButtonClick(Sender: TObject);
var
    btn: TButton;
    countByRow, idx: integer;
begin
    btn := TButton.Create(self);
    btn.Parent := self.Panel1;

    self.ButtonList.Add(btn);
    idx := self.ButtonList.IndexOf(btn);
    btn.Caption := 'btn' + IntToStr(idx);

    countByRow := (self.Panel1.Width div btn.Width);
    btn.Left := idx mod (countByRow) * btn.Width + 10;
    btn.Top := idx div (countByRow) * btn.Height + 10;
end;
上面這段程式可以在 VCL 建立一個 Button, 但在 FireMonkey 當中, 建立的方法就不一樣了:

FireMonkey版:
procedure TForm2.btnAddNewButtonClick(Sender: TObject);
var
    btn: TButton;
    countByRow, idx: integer;
begin
    btn := TButton.Create(self);
    self.scrollBox.AddObject(btn);

    self.ButtonList.Add(btn);
    idx := self.ButtonList.IndexOf(btn);
    btn.Text := 'btn' + IntToStr(idx);

    countByRow := Trunc(self.scrollBox.Width / btn.Width);
    btn.Position.X := idx mod (countByRow) * btn.Width + 10;
    btn.Position.Y := idx div (countByRow) * btn.Height + 10;
end;
在 VCL 當中,動態建立了一個新的按鈕以後,我們只需指定該按鈕的 Parent, 就可以把建立的按鈕放到 Parent 這個元件上面,當然,也需要先指定新按鈕的位置 (Left, Top) 與大小 (Width, Height).



而 FireMonkey 要做到相同的效果,則是看我們要把建立出來的按鈕放在哪個元件上面,就呼叫該元件的AddObject 方法,例如我們要把新建立的按鈕顯示在表單上面,就直接呼叫 scrollBox.AddObject(newButton);



帶一句題外話:從上面的兩個範例 procedure 裡面, 可以看出 FireMonkey 的座標與元件大小都可以有小數點, 但 VCL 不行。

要動態移除該元件的時候,也非常不同,VCL只需要呼叫該元件的 Free 方法即可:
procedure TForm1.btnRemoveBtnClick(Sender: TObject);
var
    btn: TButton;
begin
    if self.ButtonList.Count > 0 then begin
        btn := self.ButtonList.Last;

        if Assigned(btn) then begin
            self.ButtonList.Remove(btn);
            btn.Free;
        end;
    end else begin
        ShowMessage('No Button');
    end;
end;
FireMonkey 則不一樣:
procedure TForm2.btnRemoveClick(Sender: TObject);
var
    btn: TButton;
begin
    if self.ButtonList.Count > 0 then begin
        btn := self.ButtonList.Last;

        if Assigned(btn) then begin
            self.ButtonList.Remove(btn);
            self.scrollBox.RemoveObject(btn);
            btn.DisposeOf;
        end;
    end else begin
        ShowMessage('No Button');
    end;
end;
在 VCL 裡面,呼叫了 Free 方法以後,obj 所佔用的記憶體就會在 Windows 系統裡面被釋放出來,但在 FireMonkey 當中,由於要適應絕大多數系統上的特性,所以 Free 方法只會把該元件的 Reference Count 減一,在 Android 跟 iOS, Mac OS 裡面,一個元件要被系統釋放掉的話,必須要 Reference Count 等於 0, 才會真正把元件刪除掉。

所以 RemoveObject 被呼叫以後,obj 元件會在畫面上看不見了,但它還沒有真的被刪除,在 FireMonkey 當中,我們可以呼叫 obj 元件的 DisposeOf 方法,這個方法在Windows 平台上,會有等同於呼叫了 Free 方法的作用。

但在 iOS, Andorid, Mac OSX 上面, 則會強制把 Reference Count 遞減為 0, 真正強制的把佔用的記憶體釋放出來。

結語
許多已經很習慣於傳統 Delphi 或 傳統 Windows 應用程式設計的 Programmer, 會很習慣於把所有的畫面都放在同一個 Form 裡面。

在傳統的 Windows 或桌機型的應用程式環境當中,這是沒有問題的,但在行動裝置或者嵌入式裝置的應用程式當中,這是行不通的,因為在行動裝置當中,記憶體極其有限,有時只要使用超過了 40MB 的記憶體,應用程式就會被強制停止,也就是俗稱的閃退。

大家或許會覺得,40MB 很少,或者自己的應用程式不會用這麼多,但請大家記得一個數字,一張 2048x1536 的圖片,就可能佔用了約 10-15MB 的記憶體。

為什麼我提到 2048x1536 這個數字?

因為它正是 iPad Retina 螢幕的實際解析度,包含 iPad 3, 4, 5 (iPad Air), 以及 iPad Mini 2, 都是這個解析度,我們一旦有這個解析度大小的應用程式,只要兩張全螢幕的圖片,就已經處在危險邊緣。

如果還像以往的寫法,把所有的圖片、畫面都放在同一個 Form 裡面的話,只要換個兩頁,應用程式就會閃退,這種應用程式連上架的機會都沒有,所以,動態產生、動態刪除元件或畫面的需求將會越來越大,大家也一定要實際學會怎麼處理這種情形,不然前路絕對是坎坷不平的。

動態產生範例 (VCL & FireMonkey)

2014年7月12日 星期六

FireMonkey - 行動裝置上虛擬鍵盤的出現與隱藏

在 Win32 的應用程式開發中, VCL 架構當中, 我們常常會在畫面上有多個 TEdit 或 TMemo 的時候, 設定這些元件的 TabOrder, 用以決定每個文字編輯元件的順序, 待使用者輸入文字後,按下Tab鍵,便可切換到下一個文字輸入元件。

在行動裝置上卻沒有所謂以Tab鍵進行文字輸入元件切換的功能,要不是在虛擬鍵盤或手寫輸入控制元件上緣有上一個、下一個的按鍵進行切換,就會直接在鍵盤或手寫輸入框裏有換行、完成的按鍵,待使用者按下之後切換到下一個輸入的文字框去。

不僅如此,在行動裝置上輸入文字時,還會因為虛擬鍵盤的出現,需要把畫面整個向上挪移,才不會有文字框被虛擬鍵盤擋住,使用者輸入了什麼都看不見的窘況。

在 FireMonkey 的 TEdit,TMemo,就是扮演文字輸入框的角色,然而遍尋這兩個元件的所有Event,卻也找不著要如何達成前述切換文字輸入框的相關事件,更別提要像obj-c提供個像是 textFieldBeginEdit 這樣的Delegate method來偵測虛擬鍵盤的出現了。

雖然在TEdit裡面有onChange,onKeyUp,onKeyDown,onTyping這些事件,但這些事件卻無法接收到虛擬鍵盤的Return被觸發的事件,當然Windows系統上的Return可以測得,但行動裝置的不行,所以很令人煩躁。

然而,在FireMonkey 中,還是能夠達到前述功能的,只是要多花一些功夫,稍微多幾個步驟就是了:

KillFocusByReturn
這個屬性(property)是 TEdit 的新屬性,在設定接受到虛擬鍵盤的 Return 值時,是否要停止編輯,並讓目前的TEdit 失去Focus,這個屬性預設值是false,我們要把它改設定成true。

這樣一來,按下虛擬鍵盤的Return時,虛擬鍵盤就會自動縮回去了,也就是該文字框完成了編輯。

 

















































































TEdit 與鍵盤相關的新屬性
                        
TEdit 的事件


onEnter 事件
在每一個TEdit的事件表裡面,都有 onEnter 跟 onExit 這兩個事件,可以偵測到行動裝置上,該欄位是否開始/結束編輯了」

我們得自己計算一下每個TEdit 進入編輯模式時,該TEdit會不會被擋到?以iPad 為例,iPad的虛擬鍵盤(橫向)高度約為250,所以我會在橫向畫面最底下的TEdit 的 onEnter 事件當中設定一個畫面座標向上挪移250的動作:

procedure THeaderFooterForm.Edit1Enter(Sender: TObject);
begin
   shiftY := -150;
end;

設定了-150 的Y軸位移量之後,接著要偵測虛擬鍵盤出現與消失的事件:onVirtualKeyboardShow, onVirtualKeyboardHidden 這兩個事件,別急,看完再去寫 code。

Form 的虛擬鍵盤顯示/隱藏事件

onVirtualKeyboardShow, onVirtualKeyboardHidden 這兩個事件並不是歸屬給單一 TEdit 或TMemo 的哦,它們是歸屬於 TForm 的事件。

所以,我們接下來要先點選 TForm 元件,找到 onVirtualKeyboardShow, onVirtualKeyboardHidden 這兩個事件,雙擊它們,來製作出處理這兩個事件的事件處理常式。

如果Form 的畫面是由一個 align 設定為全螢幕(alClient)的TTabControl 元件所構成的,則在 onVirtualKeyboardShow 這個事件裡,我們要先把 TTabControl 元件的 align 屬性改設定為 alNone,否則修改其Position.Y 就不會有任何效果。

出現虛擬鍵盤
輸入完畢以後, 恢復原位














接著,把TTabControl Position.Y 設定為 ShiftY,就會看到畫面向上移動,露出正在輸入當中的 TEdit 或 TMemo 了。

最後,onVirtualKeyboardHidden 的時候,只要把 Position.Y 設定為 0,畫面就會恢復正常了。


心得分享:每一個文字輸入框的 ShiftY 都是獨立的,不用去管其他文字框的位移量是多少,彼此並沒有交互作用。


範例程式: 下載點

2014年6月30日 星期一

FireMonkey 桌面應用程式最小化

問題 - 視窗縮小時不會整個應用程式縮小

在製作 Delphi 或 VB 程式時, 常常會使用多個 Form, 在每個 Form 的畫面當中, 會賦予不同的功能, 但每個 Form 都會被當成獨立的畫面, 也就是 MainForm.

我們對 MainForm 的預期, 就是在按下右上方的縮小鍵時, 整個應用程式縮小到工作列上, 按了工作列上的按鈕時, 整個程式畫面還原回來原來的狀態.

但從過去 VCL 的時期到現在的 FireMonkey, 都並沒有這麼容易, 多個 Form 的時候, 只要不是按下 MainForm 的縮小按鈕, 視窗都會縮在桌面左下角。

視窗沒有縮到工作列上, 而是成為縮小視窗
以往這種 Form 的特性, 是為了製作 MDI Application, 也就是一個大的 Form 當中有多個文件會被開啟, 就像 Word, Powerpoint 那樣, 可以開啟多個文件, 讓每個文件有獨立的小視窗用來檢視文件。

以往在 VCL 架構的解決方法

在 VCL 架構的應用程式中, 解決這個問題的方法, 是在任何一個 Form 裡面建立一個 onMessage 的事件處理常式, 並在 FormCreate 事件中把它指派給 Application.OnMessage 事件:

procedure TForm1.HandleMessage(var Msg: TMsg; var Handled: Boolean); 
begin
   if (Msg.Message=WM_SYSCOMMAND) and (Msg.wParam=SC_MINIMIZE) then
     Application.Minimize;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
   Application.onMessage := self.HandleMessage;
end;

當我們發現 SC_MiniMize 事件發生, 就直接執行 Application.Minimize, 整個應用程式就會縮到工作列上了, VCL 上面如此, FireMonkey 卻不然...... 時至今日, 網路上的各個論壇還在尋找著跟 VCL 架構當中的 Application.onMessage 對應的事件...... 然而, 我也找不到, 但是在 FireMonkey 當中, 有很多玩法跟 VCL 不相同, 花了一陣子搜尋 XE5 裡面的 RTL 原始碼, 找到了這個玩法, 有效, 簡易, 跟大家分享。

範例-製作兩個 Form 互相切換

先建立一個 FireMonkey 應用程式, 包含兩個 Form, Form1 跟 Form2, Form1 是 預設的MainForm, Form2 則是一般的 Form.

Form1, 放一個 button 切換到 Form2
Form2, 放一個 button 切換到 Form1













Form1 上面的 Button1Click 內容如下
procedure TForm1.Button1Click(Sender: TObject);
begin
   form2.Show;

   self.Hide;
end;

Form2 上面的 Button1Click 內容如下
procedure TForm2.Button1Click(Sender: TObject);
begin
   form1.Show;

   self.Hide;
end;

這是最常見的寫法, 然而切換到 Form2 的時候, 按下視窗上的縮小按鈕, 就會跟本文最上方的圖片所顯示的一樣:

這時候, 只要在 Form1 的 Button1Click 加上一行:
Application.MainForm := form2;

就可以把 Application.MainForm 從 form1 切換成 form2, 此時再按 form2 的縮小按鈕, 就不會再出現縮小的視窗了耶, 超神奇, 兩個 click 事件處理常式如下
修改後, Form1 上面的 Button1Click 內容如下:
procedure TForm1.Button1Click(Sender: TObject);
begin
   form2.Show;
   Application.MainForm := form2;
   self.Hide;
end;

修改後, Form2 上面的 Button1Click 內容如下
procedure TForm2.Button1Click(Sender: TObject);
begin
   form1.Show;
   Application.MainForm := form1;
   self.Hide;
end;

結語

FireMonkey 上面許多用法都很神奇, 由於所有元件都有 Container 的特性, 所以可以直接收納其他元件, Application.MainForm 也不是唯讀屬性, 所以可以在切換 form 的時候順帶把 Application.MainForm 做重新指派, 雖然很難想到這個用法, 不過它確確實實的有效, 程式碼也簡潔, 所以推薦給大家使用.

範例程式連結

2014年6月26日 星期四

Delphi 浴火重生, 火猴的逆襲已經來臨

楔子

自 FireMonkey 推出以來, 漸漸燃起了從 2000 年以來這十年間, Delphi 程式人員對於 Delphi 的信心。從 2000 年 Anders Hejlsberg 琵琶別抱, 為微軟量身訂做了.NET 平台與 C# 程式語言之後, Delphi 的聲勢江河日下, 再加上 Borland 的策略失準, 以行銷為導向, 失卻了 2000年以前 Delphi 所自豪的穩定性與效能, 曾被稱為 VB Killer (VB 殺手) 的 Delphi, 在 2000-2010這個十年之間, 完全失去了主要程式開發工具的地位。

然而, 在歐洲各地的 Programmer 們並沒有受到 Delphi 崩壞的影響, 在俄羅斯、義大利、法國,Delphi 在程式開發的這個圈圈裡, 仍舊有許許多多的人在努力著, 從 2001 (年份我可能記得不清楚了) 的 Delphi 8, 到之後的 Delphi 2005, 2006, 2007, 2009, 2010, XE, XE2, XE3, XE4, XE5, 到 2014 年的 XE6, 各式各樣功能的第三方 VCL 元件不斷在全世界被創造、散佈,使用在各種場合與應用裡.

Tnt 元件

在 Delphi 2009 發佈之前, 所有 Delphi 的元件與 IDE 並沒有支援 Unicode 的字元, 也因此從 Delphi 5 開始, 就有兩三種內建的工具讓程式開發人員可以製作出 Resource 檔案, 用來儲存 Form 當中的各種多國語言的內容, 然而, Unicode 才是王道, 沒有 Unicode, 多國語系真的是個遙不可及的夢想。

Tnt系列元件, 是提供尚未支援 Unicode 的版本的 Delphi (Delphi 2009 以前的所有版本)對應的 VCL, 從 TLabel, TEdit, 到 BDE 的絕大多數元件, 都有對應的 Unicode 版本元件, 對應上來就是 TtntLabel, TtntEdit 等等。

在提供多國語系的應用程式上, 或者處理跨國企業的資料庫時, 都對程式人員提供了非常多的幫助, 感謝 Tnt 元件的開發者.


Indy 網路元件組

從 1999 年的 WinShoes, 到 2000 年開始的 Indy 7, Indy 8, 一直到目前搭載在 Delphi XE5 的 Indy 10.60, Indy 陪伴著 Delphi 開發人員走過了將近 20 個年頭, 從 Client, Server, Protocol, 各式各樣的功能, 讓開發人員處理網路相關的程式功能更不費力.

目前 Delphi XE5 當中的很多官方元件, 例如 DBX 系列元件的底層, 也都是以 Indy 作為其基礎, 因此如果我們遇到 DBX 系列元件發生了什麼問題, 也就可以從 Delphi 內附的 RTL 或 VCL 程式原始碼來抓出到底是那兒出了問題.


跨平台的努力-FireMonkey

從 Delphi 2009 把 Unicode 功能完完整整, 裡裡外外的融入了 Delphi 的 IDE, 編譯器以後, 我們現在已經可以在程式裡面用中文變數, 甚至各國文字作為變數, 要怎麼處理多國語系, 都不是問題.

但 2009 年的時空, 已經不是 Windows 桌面應用程式的天下, 就連網頁程式也已經是小學生就能提供的東西, 因此, Delphi 的下一版, 除了把 64 bit 編譯器做出來, 讓效能能夠更好, 接著就是朝向跨平台的目標前進.

在 1993-1995年間, JAVA 的概念與程式標準開始被 SUN 提出, 每個新的產品都有其行銷的口號, JAVA 當時的口號就是: Once Compile for All Platforms. (編譯一次, 所有平台都可用)

這個口號非常美好, 但大家可以在後來的桌面應用程式跟網頁程式當中得知, 這個口號又是另一個夢幻泡影. (如露亦如電, 應作如是觀 - 引述自金剛經)

JAVA 的 Once Compile for All Platforms, 是奠基在『所有硬體設備上面都會提供有 JAVA 虛擬機器 (本文後將簡稱為 JAVA VM)』的這個前提假設上, 然而, 並非每個硬體設備都有 JAVA VM, 且當時設計這個概念的人也沒想到 JAVA VM 的效能簡直是一場惡夢.

Delphi XE2 開始, 把這個概念作了一些些修正, 叫做 One Code for Multiple platforms (同樣一份程式碼, 能夠在多種平台上執行), 這個口號聽起來沒有很炫, 但卻實際多了.

從 Delphi XE2 開始, 沿用了 20 多年的 VCL 有了 32/64 bit 兩種版本, 另一組新的視覺元件庫: FireMonkey 也被推出, 透過 FireMonkey 元件組所撰寫的程式 (我們先討論一般桌面程式), 就可以直接編譯成 Win32, Win64, 以及 OSX (Mac 作業系統).

雖然要編譯三次, 在操作上多一些些步驟, 但編譯出來的程式, 在各個平台上都是原生程式碼, 不再透過像 JAVA 那樣高階的虛擬機器, 而能夠讓編譯出來的程式碼直接就是機器碼, 這效能的提升, 比透過直譯器達成的程式, 或者虛擬機器達成的程式, 都高出了不知道多少倍.

且 FireMonkey 所提供的介面, 在設計階段都是 Windows 的外觀, 但實際執行的時候, 在Windows 跟 Mac OSX 上面, 都完完全全是原生平台的程式外觀, 不用程式人員再花額外的功夫去處理佈景或圖片的問題了.

至此, 跨平台的功能頗有小成, 但請回想一下, 2009 年, 已經是 iOS 跟 Android 打的死去活來, Windows Phone 只能撿一些小屑屑, 市佔率幾乎達不到兩位數, 開發人員完全不會去想提供 Windows Phone 平台的應用程式.....

因此接下來的 XE3, XE4, XE5, XE6, Delphi 就花了更多的功夫在這兩個完全引領主流的作業系統上面, 到 XE5 算是有所成, 第一次能夠讓 FireMonkey Mobile 的同一份程式碼在 iOS 跟 Android 系統上面執行.


未來將何去何從?

這個題目太大, 即使問微軟說: 你們的 Visual Studio 接下來要提供什麼功能? 相信微軟的開發人員也會一下就楞住, 我相信 Delphi 跟 C++ Builder 也一樣.

目前 Visual Studio 2012 版, 提供了 C#, VB, VC 三種程式語言, 可以透過 .NET 開發網頁程式、Windows 應用程式, 以及透過一些第三方工具, 讓一部分程式能夠編譯成 iOS 能執行的 app (是透過像 JAVA 那樣的虛擬機器, 類似 Mono).

有了網頁, 桌面應用程式, 手機應用程式, 程式人員對 Delphi 的未來還有什麼期待?

未來是不斷在人們手中開創的, 2005 年的時空下, Windows Mobile 幾乎主宰整個智慧型手機市場, 靠的是 Compact .NET framework, 然而, 誰會知道 2007 年 iPod Touch 即將推出, 2008 年 iPhone 2G 與 App Store的推出會讓 XCode (Objective C) 在兩年內席捲全球? 2009 年 Android 系統跟 Eclipse 讓 JAVA 借屍還魂?

誰又知道 2014 年六月, Apple 會推出語法近似 JAVA 與 C#, 包含有完整物件導向概念, 原生相容於 XCode 所有 Framework 的新程式語言 Swift? (按筆者: 所有程式語言, 請從其概念來理解, 則只需要 Reference 文件就可以很快學會, 大約 1-2周吧)

未來, 永遠是軟體人員心中永遠的期許, 也是傷痛, 這兩個字代表著無限的學習, 永不停止.


對 FireMonkey 的不熟悉

FireMonkey 是一組全新的元件, 大多數一直使用 Delphi 的程式人員, 都有著陌生的感覺, 在台灣, 除了 Embarcadero 原廠的文件與代理商捷康努力的推廣, 請李維大師持續的在這塊領域上耕耘之外, 能夠說自己已經熟悉 FireMonkey 的人恐怕不多.

筆者從 Delphi XE5 使用至今, 剛剛有一些些熟悉, 未來也會把這些相關經驗在網路上分享出來, 期望透過交流與跟網路上的開發人員交互學習, 讓 Delphi 與 FireMonkey 能夠發揮出很好的功能, 讓 Delphi 的程式技巧能夠再次引領業界與學界的風氣, 在未來的某一天, 如果台灣能夠開始對軟體開發稍微正眼看待的時候, 讓 Delphi 與 C++ Builder 能夠成為程式人員前幾名的選擇.

Dennies
2014 年六月.