2017年12月11日 星期一

在 Delphi 當中使用 TMapView 顯示地圖之一:程式設定與加上地點標示

在 Delphi Seattle 10 之前,還沒有提供封裝好的地圖元件,因此在那之前的案子,筆者也大多用 TWebBrowser 搭配 JavaScript 土法煉鋼來提供手機上的地圖功能。

在 Delphi Seattle 之後,TMapView總算出現了,但一直沒有實際的機會可以拿來用,最近有個機會,需要在 App 當中提供標示地點、路線的功能,因此正好把多年以前在 iOS 上面製作過的桃園福利地圖App當中的導航跟標示地點的功能拿來複習一下。

在 Delphi 上面要製作具備有 Google Map功能的地圖,請先服用官方說明:http://docwiki.embarcadero.com/CodeExamples/Tokyo/en/FMX.Map_Sample

如果您懶得看,我整理一下懶人包(如何設定你的專案,讓它具備Android平台上能顯示地圖的功能),只有幾點而已:

第一大項:建立 Google Map API 的 Key:

1. 連線到 Google API Console: https://code.google.com/apis/console/?noredirect
2. 在專案列表中,建立一個新的專案
3. 把 App 的 id 跟名字輸入進去
4. 完成,畫面上會出現 Google API Key,這是一長串的字。

第二大項:設定專案的權限:

1. 先把專案切換到 Android 平台

2. 點選 Project Manager 上的專案,滑鼠右鍵點擊,選擇 Project > Options > Uses Permissions
3. 確定以下三個權限有勾選起來:
  • Access coarse location
  • Access fine location
  • Access network state 

4. 點選 Project Manager 上的專案,滑鼠右鍵點擊,選擇 Project > Options > Entitlement List 確定 Map Service 有勾選起來


5. 點選 Project Manager 上的專案,滑鼠右鍵點擊,選擇 Project > Options > Version Info 把 Google API Key 貼到apikey 欄位上,也確認 id 跟在 Google API Console 輸入的一致


這樣就設定完成了。

設定 TMapView

接著就可以從元件盤上面拉一個 TMapView 到 Form 上面了,MapView 要放多大都可以,但放太小的話使用者操作起來會不方便,所以盡量能留個半邊螢幕給它比較恰當。

使用了 TMapView,第一個需要操作的行為,一定是把地圖的中心點拉到我們想要顯示的地方,並且把地圖放大到『適當的大小』。

以上兩個需求,可以透過設定 TMapView 的兩個 Property 來達成,一個是設定大小用的 Zoom,另一個則是設定中心點的 Location。

先介紹 Zoom,它比較單純,是一個 Single 型別的數字,跟 Google Map 的網頁版一樣,數字越大,街道越大,數字越小,街道越小,我自己習慣的大小,是設定在 16.0,這個大小很適合作為導航之用。

只需一行指令:
Self.MapView1.Zoom := 16;

中心點就稍微麻煩一點,因為 Location 屬性的型別是 TMapCoordinate,所以我們得先建立出一個 TMapCoordinate 實體:

var
    mapCenter: TMapCoordinate;
begin
    .....
    Self.MapView1.Zoom := 16;

    mapCenter := TMapCoordinate.Create(23.459567, 120.323351);
    self.MapView1.Location := mapCenter;
    .....
end;

透過這段程式碼,就能夠把地圖的中心點拉到嘉義高鐵站,並且把大小調整為適合導航的大小。

加一個標示點在 TMapView 上

如果您使用過 Google Map網頁版進行開發,這裡就會很熟悉了,在Google Map網頁版上,要加一個標示點,是以 Marker 元件來加的,但只能以 Javascript 或後端工具在網頁上添加。

在 Delphi 程式中,則提供了對應的元件 TMapMarker,要建立 TMapMarker,則需透過 TMapMarkerDescriptor 這個 record,當中包含了 Marker 的座標、文字描述,如果我們想要自定圖示,也可以直接透過 TMapMarkerDescriptor 來指定:
var
    mapCenter: TMapCoordinate;
    MyMarker: TMapMarkerDescriptor;
begin
    .....
    Self.MapView1.Zoom := 16;

    mapCenter := TMapCoordinate.Create(23.459567, 120.323351);
    self.MapView1.Location := mapCenter;

    MyMarker := TMapMarkerDescriptor.Create(mapCenter, '嘉義高鐵站');
    MyMarker.Visible := true;
    MapView1.AddMarker(MyMarker);
    .....
end;
這樣就能在 MapView 上面,把地圖中心點拉到嘉義高鐵站、在上面插個標示了,如果想要自定圖示,只需指定一個 TBitMap 給 MyMarker.Icon 即可。




使用 TRESTClient 與 TRESTRequest 作為 HTTP Client 之二 (POST 檔案)

使用 HTML 進行檔案上傳,已經是很平常的應用了,在手機App裡面,也常常會用到這個作業,例如拍照上傳,或是從相簿選取照片上傳,都是很常見的。

在 HTML 的 Form 裡面,要讓使用者選擇檔案上傳,通常會這樣寫:
<input type="file" name="fileId1" id="fileId1"/>
當我們在 HTML 裡面這麼寫,網頁瀏覽器會自動在畫面上顯示一個按鈕,點選之後,會出現檔案選擇的對話框讓使用者選擇檔案。

但是,在手機作業系統中,為了提供檔案系統的Sandbox 功能,並無法從網頁直接提供相簿照片選擇或拍照上傳的功能,所以大多數類似的功能,還是得透過 App 提供才行。

在這個範例中,為了讓使用者能直覺使用檔案選擇的功能,所以也同時介紹了如何從手機相簿選擇與拍照上傳的功能。

如何讓Delphi程式提供相簿選照片的功能

在Delphi環境中,要提供使用者從相簿選相片,或是啟動相機拍照,都要透過 TAction 來完成,只是從相簿選照片跟啟動拍照功能,是以兩個不同的 TAction 來完成的。雖說是不同的 TAction 處理不同的程序,但取得照片的時候,回傳給程式的結果都是用TBitMap 來儲存相片。

換句話說,取得了 TBitMap 之後,我們想要對TBitMap做任何影像處理、格式處理,都可以由我們的程式碼來操作。

從 Delphi XE6 之後,這些直接使用行動裝置功能的程式碼,都已經被載入到TAction裡面了,我們只需要在使用上了解如何把TAction跟按鈕事件綁在一起即可。

首先,我們需要在 Form 上面放上一個 TActionList 元件,然後雙擊這個元件,就可以顯示出 ActionList 的編輯畫面:
在這畫面中,我們要新增Media Library 的兩個 Standard Action,就是上圖的TakePhotoFromLibraryAction (從相簿取得照片),以及TakePhotoFromCameraAction (從相機拍攝照片)。


建立兩個 Action 之後,我們就需要為這兩個 Action 設定 onDidFinish 跟 onDidCancel 的事件,前者是用來處理確定拍攝/挑選照片的事件,後者則是用來處理取消拍攝/挑選照片的事件。

在這個範例中,onDidFinish 的時候,會把回傳來的圖片資料直接放在畫面上的 TImage 元件顯示,所以它的程式碼很單純:

procedure TForm2.TakePhotoFromCameraAction1DidFinishTaking(Image: TBitmap);
begin
   self.Image1.Bitmap.Assign(Image);
end;

procedure TForm2.TakePhotoFromLibraryAction1DidFinishTaking(Image: TBitmap);
begin
   self.Image1.Bitmap.Assign(Image);
end;

就是直接把 Image Assign 給 Image1.Bitmap 屬性即可。

最後,點選按鈕上傳,完整的程式碼如下:

procedure TForm2.btnUploadClick(Sender: TObject);
var
   newItem: TRESTRequestParameter;
   allPass: boolean;
   imgPath, houseNum, floorIdx: string;
   nameStr: String;
   obj: TJSONObject;
begin
   allPass := True;

   if self.EditName.Text = '' then begin
      allPass := False;
      ShowMessage('請填寫住戶名字');
   end
   else if (Length(self.EditCardNo.Text) < 5) or
       (Length(self.EditCardNo1.Text) < 5) then begin
      allPass := False;
      ShowMessage('請填寫悠遊卡卡號10碼');
   end
   else if self.Image1.Bitmap.IsEmpty then begin
      allPass := False;
      ShowMessage('請提供悠遊卡感應卡號照片');
   end;

   if allPass then begin
      self.RectWaiting.Visible := True;
      self.AniIndicator1.Enabled := True;

      imgPath := System.IOUtils.TPath.Combine
          (System.IOUtils.TPath.GetDocumentsPath, 'tmp123.png');
      self.Image1.Bitmap.SaveToFile(imgPath);

      self.RESTClient1.BaseURL :=
          'http://testURL/acceptNewCard.php';

      self.RESTRequest1.Params.Clear;
      RESTRequest1.Method := rmPOST;

      nameStr := self.EditName.Text;

      self.RESTRequest1.AddParameter('cardNum', self.EditCardNo.Text + ':' +
          self.EditCardNo1.Text);
      nameStr := TIdURI.ParamsEncode(nameStr, IndyTextEncoding_UTF8);
      self.RESTRequest1.AddParameter('name', nameStr,
          TRESTRequestParameterKind.pkGETorPOST,
          [TRESTRequestParameterOption.poDoNotEncode]);
      // self.RESTRequest1.AddParameter('name', nameStr, TRESTRequestParameterKind.pkGETorPOST, [TRESTRequestParameterOption.poDoNotEncode]);

      case self.ComboBoxHouseNum.ItemIndex of
         0: begin
               houseNum := '1';
            end;
         1: begin
               houseNum := '374';
            end;
      end;

      houseNum := TIdURI.ParamsEncode(houseNum, IndyTextEncoding_UTF8);
      self.RESTRequest1.AddParameter('houseNum', houseNum);

      floorIdx := IntToStr(self.ComboBoxFloor.ItemIndex + 1);
      self.RESTRequest1.AddParameter('floorIdx', floorIdx);
      self.RESTRequest1.AddFile('picFilename', imgPath);
      self.RESTRequest1.Execute;

      obj := TJSONObject.ParseJSONValue(self.RESTRequest1.Response.JSONText)
          as TJSONObject;
      ShowMessage(obj.GetValue<String>('result'));

      self.RectWaiting.Visible := False;
      self.AniIndicator1.Enabled := False;
   end;
end;

上面的程式碼裡面,處理檔案的重點只有一行:
self.RESTRequest1.AddFile('picFilename', imgPath);

在更前面一點的程式碼中,imgPath 是由TImage.Bitmap即時存下來的照片,這個範例只適用於 Delphi 10.2 (Tokyo) 以後的版本,因為在 Berlin 當中,對於檔案的 MIME Type 還沒有完全支援,所以在 Berlin 或 Seattle 當中要使用這個方法上傳檔案的朋友,請升級吧,我也沒辦法直接提供 TRESTClient 在新版的程式碼給大家。