Search results for '파이어몽키'

  1. 2015.10.02 -- [따라하기] 건강데이터 수집 및 기록 시스템 #1 - BLE 기반 스마트 체중계에서 실시간 데이터 받기 (11)
  2. 2015.10.01 -- iOS9의 새로운 기능인 "App transport Security" 예외 허용을 위한 Info.plist xml 수정방법
  3. 2015.07.28 -- [따라하기] 위험지역 경보 시스템 #1 - 비콘을 이용해 위험지역 진입 경보앱 만들기 (20)
  4. 2015.07.27 -- [XE8] 모바일 앱에서 클래식 블루투스를 이용해 데이터 전송하기 (2)
  5. 2015.07.02 -- 모바일 앱 실행 시 검은화면이 표시되는 경우 조치방법
  6. 2015.06.26 -- [하이브리드 앱 개발] 웹페이지에서 델파이 함수 호출하기
  7. 2015.06.25 -- TWebbrowser의 "tel:, sms:" 링크가 동작하지 않는 이슈 해결방법
  8. 2015.06.09 -- 개발환경 설정 - 안드로이드 개발환경
  9. 2015.06.04 -- 앱의 상태바(StatusBar) 제어하기(색상변경, 감추기, 투명하게)
  10. 2015.06.04 -- [XE8] 스피드버튼의 이미지를 크게 표시할 수 있습니다.
  11. 2015.05.18 -- [JVESoft] 광고와 결재 컴포넌트(AdMob, Paypal 등)
  12. 2015.05.18 -- [iOS] 배포파일 아이클라우드 백업 옵션설정하기
  13. 2015.04.14 -- 파이어몽키에서 외부 라이브러리 연동하기(jar, so, a) (3)
  14. 2015.03.04 -- [FMX] ListBox 전체체크 기능 구현하기
  15. 2015.02.24 -- [XE7] iOS 시뮬레이터 배포 시 'Please specify exact device preset UUID.' 오류 대응
  16. 2015.02.23 -- 파이어몽키 용 그리드 컴포넌트 안내 - FirePower
  17. 2015.02.12 -- [XE7] 안드로이드 블루투스 활성화 조회와 설정하기
  18. 2015.02.11 -- [XE7] 안드로이드 WiFi 상태조회와 설정하기
  19. 2015.02.09 -- 태블릿용 멀티컬럼 리스트뷰 ItemAppearace 만들기 (6)
  20. 2015.01.28 -- 트루타입 폰트(FontAwesome) 파일을 이용해 아이콘 표시하기(안드로이드, iOS)

[따라하기] 건강데이터 수집 및 기록 시스템 #1 - BLE 기반 스마트 체중계에서 실시간 데이터 받기

2015.10.02 13:06


위 동영상(건강데이터 수집 및 기록 시스템 데모) 중 스마트 체중계의 체중정보를 모바일에서 실시간으로 받아오는 기능을 따라하기로 진행합니다.


스마트 체중계에서 체중정보 수신앱 만들기

❑ 앱소개

앱소개

이 앱은 스마트체중계와 블루투스 LE로 연결 해 체중정보를  실시간으로 받아와 화면에 표시하는 앱입니다.

따라하기의 목적

스마트 체중계 연결을 통해 블루투스 LE 인터페이스를 제공하는 다양한 기기(또는 센서)에 연결하고 데이터를 수신하는 방법을 이해합니다.


따라하기에서는 다음 내용을 다룹니다.

  • RAD Studio로 멀티-디바이스(안드로이드, iOS, 윈도우, OS X) 앱 프로젝트 생성 및 화면 디자인
  • TBluetoothLE 컴포넌트로 스마트 체중계와 연결 및 데이터 수신(구독)
  • 실 기기에 배포 및 테스트(안드로이드 폰, 아이폰)

따라하기

0, 사전 준비사항

1, 빈 멀티-디바이스 애플리케이션 프로젝트 생성

2, 메인 UI 제작

3, 블루투스LE 컴포넌트 추가

4, 주변의 디바이스 탐색

5, 탐색된 디바이스 목록에서 디바이스 선택 후 서비스 탐색

6, 탐색된 서비스 목록에서 서비스를 선택 후 특성 구독

7, 특성 데이터 수신 후 체중 데이터 가져오기

8, 블루투스 권한설정 

9, 디바이스에 배포 및 테스트

0, 사전 준비사항

개발도구 준비하기

델파이(또는 RAD Studio)를 이용해 실습 진행합니다.(이 실습은 델파이 10 시애틀 버전으로 작성되었습니다. 델파이 XE8 이상에서 따라할 수 있습니다.) 

만약, 델파이가 설치되지 않았다면 아래의 페이지에서 델파이를 다운로드 받아 설치 후 진행하시기 바랍니다. 

스마트 체중계 준비하기

따라하기에서는 Wahoo Balance Bluetooth Smart Scale 제품을 이용해 실습을 진행합니다.

https://www.underarmour.com/en-us/wahoo-balance-bluetooth-smart-scale/pid1276806-100-OSFA


Wahoo Balance Bluetooth Smart Scale은 다음과 같은 디바이스 이름과 UUID를 갖습니다.

디바이스 이름 : Wahoo Scale v1.3

서비스 UUID : 00001901-0000-1000-8000-00805F9B34FB

특성 UUID : 00002B01-0000-1000-8000-00805F9B34FB

위 정보는 구현에 필요한 정보입니다.(시중의 다른 스마트 체중계는 위 값들이 다릅니다.)

BLE 구조에 대한 소개

블루투스 LE는 프로파일(Profile), 서비스(Service), 특성(Characteristic) 기반으로 아래 그림과 같이 프로파일은 하나 이상의 서비스를 갖고, 서비스는 하나이상의 특성을 갖는 계층구조로 구성되어 있습니다.



(출처: GENERIC ATTRIBUTE PROFILE (GATT) - https://developer.bluetooth.org/TechnologyOverview/Pages/GATT.aspx)


블루투스 LE 인터페이스를 제공하는 장비와 연결하고 데이터를 받기 위해 다음 과정이 필요합니다.(실제 구현도 다음 단계대로 진행합니다.)

1, 주변의 블루투스 LE 프로파일을 탐색합니다.

2, 탐색된 프로파일에서 서비스를 탐색합니다.

3, 탐색된 서비스에서 특성을 찾아 구독(subscribe)합니다.

4, 구독한 특성에서 발생되는 특성값에서 필요한 데이터를 추출합니다.

1, 빈 멀티-디바이스 애플리케이션 프로젝트 생성

  1. File > New > Multi-Device Application - Delphi 메뉴를 선택 하고, Blank Application을 선택해 프로젝트를 생성합니다.
  2. File > Save all 메뉴를 선택 해 유닛이름을 "MainForm.pas"로 프로젝트 이름을 "SmartScale.dproj"로 저장합니다.

2, 메인 UI를 제작

모바일 앱 스타일로 화면 구성하기 위해 폼디자이너 스타일을 Android로 변경합니다.


다음화면과 표를 이용해 체중정보 수신 앱 메인 UI를 개발합니다.


상위 오브젝트 

오브젝트 

속성 

값(또는 설명) 

 Form1

 ToolBar1

 

 

 Layout1

 Align

 Top
 Height 81
 Memo1

 Align

 Client

 ToolBar1

 Label1

 Align

 Client

 Text

 스마트 체중계 데모

 TextSettings.HorzAlign

 Center

 Switch1 Align Right
 Layout1 Text1

 Align

 Center

 Text

 0.0
 TextSettings.Font.Size

 30

 TextSettings.HorzAlign Trailing
 Width 114
 Text1 Text2 Position.X 120
 Text Kg

 TextSettings.HorzAlign

 Leading

3, 블루투스 LE 컴포넌트 추가

블루투스 LE 컴포넌트 추가

Tool Palette에서 TBluetoothLE 컴포넌트 선택 해 폼에 추가합니다.


필요한 변수, 메소드, 상수 추가

구현에 필요한 변수(디바이스, 서비스, 특성), 메소드 선언(시작, 중지), 상수(장비명과 서비스, 특성 UUID)를 아래와 같이 추가합니다.

  private
    { Private declarations }
    FBLEDevice: TBluetoothLEDevice;
    FBLEGattService: TBluetoothGattService;
    FBLEGattChar: TBluetoothGattCharacteristic;

    procedure StartScale; // 체중계 연결 동작 시작(블루투스LE 활성화, 주변 디바이스 탐색)
    procedure StopScale; // 체중계 연결 동작 중지(블루투스LE 비활성화, 특성 구독 해제)
  public
    { Public declarations }
  end;

const
  ScaleDeviceName = 'Wahoo';

  WEIGHT_SERVICE: TBluetoothUUID          = '{00001901-0000-1000-8000-00805F9B34FB}';
  WEIGHT_CHARACTERISTIC: TBluetoothUUID   = '{00002B01-0000-1000-8000-00805F9B34FB}';

메소드 구현부 생성

StartScale, StopScale 메소드 구현부를 자동 생성하기 위해 procedure StartScale;에 마우스 커서를 옮기고 키보드에서 Ctrl + Shift + C를 동시에 누릅니다.


4, 주변의 디바이스 탐색

주변 디바이스 탐색

Switch1의 OnSwitch 이벤트 핸드러 생성(Switch1 컴포넌트 선택 > Object Inspector 창에서 Event 탭 선택 > OnSwitch 우측의 콤보박스 더블클릭) 후 아래 코드를 입력합니다.

procedure TForm1.Switch1Switch(Sender: TObject);
begin
  if Switch1.IsChecked then
    StartScale
  else
    StopScale;
end;

StartScale 메소드에서 블루투스 LE를 활성화하고, 주변 디바이스를 탐색하는 코드를 다음과 같이 입력합니다.
(StopScale은 제일 마지막에 다시 구현합니다.)

procedure TForm1.StartScale;
begin
  BluetoothLE1.Enabled := True;
  BluetoothLE1.DiscoverDevices(1000); // 1초(1000 ms)동안 주변 디바이스 탐색, 완료 시 OnEndDiscoveryDevices 이벤트 발생
end;

5, 탐색된 디바이스 목록에서 디바이스 선택 후 서비스 탐색

StartScale 메소드의 DiscoverDevices 메소드 호출하면 (주변의 디바이스 탐색 후) OnEndDiscoverDevices 이벤트가 발생합니다. 

OnEndDiscoverDevices 이벤트는 파라메터로 탐색된 디바이스 목록(ADeviceList)을 제공합니다.

이 디바이스 목록에서 'Wahoo'(위에서 ScaleDeviceName 상수로 선언)로 시작하는 디바이스를 선택합니다.

디바이스를 선택했다면, 디바이스에서 제공하는 서비스 탐색을 시도합니다.

procedure TForm1.BluetoothLE1EndDiscoverDevices(const Sender: TObject;
  const ADeviceList: TBluetoothLEDeviceList);
var
  Device: TBluetoothLEDevice;
begin
  if ADeviceList.Count = 0 then
  begin
    Memo1.Lines.Add('발견된 디바이스가 없습니다.');
    Switch1.IsChecked := False; // 디바이스 미발견 시 스위치 끄기
    Exit;
  end;

  FBLEDevice := nil;
  Memo1.Lines.Add('Device List');
  for Device in ADeviceList do
  begin
    Memo1.Lines.Add(' - ' + Device.DeviceName); // 로그-디바이스 이름

    // 디바이스 이름으로 디바이스 선택
    if Device.DeviceName.StartsWith(ScaleDeviceName) then
    begin
      FBLEDevice := Device;
      Memo1.Lines.Add('디바이스를 찾았습니다.');
    end;
  end;

  if FBLEDevice = nil then
    Exit;

  // [TIP] 서비스 발견이 실패하는 경우가 종종 있음
    // 실패한 경우 한번더 발견요청
  if not FBLEDevice.DiscoverServices then
    FBLEDevice.DiscoverServices; // 서비스 탐색 완료 시 OnEndDiscoverServices 이벤트 발생
end;

6, 탐색된 서비스 목록에서 서비스를 선택 후 특성 구독

서비스 탐색(DiscoverServices)이 완료되면 OnEndDiscoverServices 이벤트가 발생합니다.

OnEndDiscoverServices 이벤트는 파라메터로 탐색된 서비스 목록(AServiceList)을 제공합니다.

이 서비스 목록에서 서비스 UUID와 같은 UUID를 갖는 서비스를 선택합니다.

선택한 서비스에서 특성 UUID로 특성을 찾습니다.

특성에서 체중정보를 포함한 특성데이터를 수신할 수 있도록 구독(SubscribeToCharacteristic) 합니다.

procedure TForm1.BluetoothLE1EndDiscoverServices(const Sender: TObject;
  const AServiceList: TBluetoothGattServiceList);
var
  Service: TBluetoothGattService;
begin
  Memo1.Lines.Add('Service List');
  for Service in AServiceList do
  begin
    Memo1.Lines.Add(' - ' + Service.UUID.ToString); // 로그-디바이스 이름
    if Service.UUID = WEIGHT_SERVICE then
    begin
      FBLEGattService := Service;
      Memo1.Lines.Add('서비스를 찾았습니다.');
    end;
  end;

  if FBLEGattService = nil then
  begin
    Memo1.Lines.Add('서비스를 찾지 못했습니다.');
    Exit;
  end;

  // [TIP][예외] 특성목록을 생성하기 위해 아래 코드 호출
    // GetCharacteristic 바로 호출 시 특성을 가져오지 못해
    // GetCharacteristics을 먼저 호출
  BluetoothLE1.GetCharacteristics(FBLEGattService);

  FBLEGattChar := BluetoothLE1.GetCharacteristic(FBLEGattService, WEIGHT_CHARACTERISTIC);
  if FBLEGattChar = nil then
  begin
    Memo1.Lines.Add('서비스 특성을 찾지 못했습니다.');
    Exit;
  end;

  if BluetoothLE1.SubscribeToCharacteristic(FBLEDevice, FBLEGattChar) then // 구독하면 OnCharacteristicRead 이벤트를 통해 특성 값을 받음
    Memo1.Lines.Add('특성에 구독했습니다.'); 
end;

7, 특성 데이터 수신 후 체중 데이터 가져오기

특성에 구독(SubscribeToCharacteristic)하면 TBluetooth 컴포넌트의 OnCharacteristicRead 이벤트로 특성 값을 받을 수 있습니다. 스마트 체중계의 경우 체중계의 체중이 변경될 때마다 특성 값을 받을 수 있습니다.

Wahoo Balance Blueeoth Smart Scale은 다음과 같은 코드로 특성값에서 체중 정보를 가져옵니다.

(특성 값을 분석하는 방법은 서비스마다 다릅니다.)

procedure TForm1.BluetoothLE1CharacteristicRead(const Sender: TObject;
  const ACharacteristic: TBluetoothGattCharacteristic;
  AGattStatus: TBluetoothGattStatus);
var
  Weight: Single;
begin
  Weight := (ACharacteristic.GetValueAsInteger shr 8) / 10;
    // 특성 값에서 앞의 3byte 이용, 마지막 숫자는 소숫점이므로 10으로 나누기

  Text1.Text := Format('%3.1f', [Weight]);
end;

마지막으로 앞에서 선언한 StopScale에 특성 구독을 해지하고, 블루투스 LE를 비활성화하는 코드를 추가합니다.

procedure TForm1.StopScale;
begin
  if Assigned(FBLEDevice) and Assigned(FBLEGattChar) then
    BluetoothLE1.UnSubscribeToCharacteristic(FBLEDevice, FBLEGattChar);
  BluetoothLE1.Enabled := False;
end;

8, 블루투스 권한설정

(이 과정은 안드로이드 플랫폼을 사용하지 않는 경우 생략할 수 있습니다.)

비콘은 블루투스 LE(Low Energy) 기반으로 통신합니다.  블루투스 사용권한을 설정합니다.

Project > Options 메뉴를 선택 후 Uses Permissions 화면으로 이동합니다.


Bluetooth와 Bluetooth admin 두개의 권한을 사용하도록 설정합니다.


9, 디바이스에 배포 및 테스트

처음 디바이스에 배포하는 경우 먼저 모바일 개발환경 설정하고 진행하기 바랍니다.


개발을 완료했습니다. 이제 준비된 개발환경으로 플랫폼 선택 후 Run > Run Without Debugging 메뉴를 이용해 실행하고 테스트 합니다.



테스트 방법:

  1. 디바이스의 블루투스 기능을 킵니다.
  2. 앱을 실행 후 스위치를 커서 주변의 블루투스 LE를 탐색해 스마트 체중계와 연결합니다.
    (메모 컨트롤에 "디바이스를 찾았습니다." > "서비스를 찾았습니다." > "특성에 구독했습니다." 메시지가 표시되면 정상 연결된 것입니다.)
  3. 체중계에 올라가면 실시간으로 앱에 체중정보가 표시되는 것을 확인합니다.

험프리.김현수 파이어몽키

  1. Blog Icon
    이승욱

    안녕하세요.
    지난 9월에 세미나를 교수님과 다녀온 후에 학과에서 간단하게 시연해보려고 계속 작업중에 있습니다. 비콘을 이용한 경보 앱은 구현을 하고 이번에는 체중계 앱을 작업하고 있는데 문제가 있어 조언을 얻고자 글을 남기게 되었습니다.
    기존에 포스팅 하신데로 모두 설정하였고 UUID도 맞게 입력이 되었는데 컴파일 하고 연결하려고 하면 디바이스가 1개 검색이 되지만 weight_service를 불러오지 못한다고 합니다.
    현재 작업하는 디바이스는 탭이고 개별로 블루투스로 연결해볼까 해서 연결하면 Wahoo Scale v1.3 에서 연결을 거절했다는 메시지가 뜨네요...
    답변해주시면 감사하겠습니다.

  2. 디바이스 발견 시 본문의 화면과 같이 Wahoo Scale v1.3으로 표시되나요?
    쳬중계를 연결할때 미리 체중계에 올라가 체중계를 키고 시도해 보세요.

    그리고, 혹시 모르니 다른 안드로이드 기기로도 테스트해보시기 바랍니다.
    (그럴리 없지만 체중계의 이상을 확인하려면 아이폰에서 Wahoo Wellness 앱을 설치해 연결을 확인해보세요. 무료입니다.)

    그래도 안되면 다음 링크에 데모 샘플코드가 있으니 해당 소스코드로 테스트해보시기 바랍니다.
    https://github.com/devgear/RADStudio_IoTDemos

    마지막으로 시간되시면 제가 사물인터넷 1일 과정 교육을 진행하고 있습니다.
    해당 교육에서 스마트 체중계 연결 실습도 진행되니 오프라인으로 확인하고 싶으시면 교육신청해주세요.^^
    http://devgear.co.kr/support/education/radstudio_iot.html

  3. Blog Icon
    김헌성

    안녕하세요~

    블루투스 저울로 스마트폰 앱을 만드는것을 공부하고 있습니다.

    혹시 체중계별로 UUID가 다르다고 하셨는데요..

    Wahoo Balance Bluetooth Smart Scale 이제품의 UUID는 어떻게 아신건가요??

    공개를 해주나요??아니면 다른방법으로 아는 방법이 있나요??

    저는 블루투스 정밀저울로 테스트를 해야해서요..ㅠ

    제가 테스트하려는 제품은 Smart Connect Bluetooth Kitchen Scale 입니다.

    답변주시면 감사하겠습니다. ㅎ

  4. Blog Icon
    박상우

    안녕하십니까. 비콘과 앱 개발을 처음 해보는 학생입니다.
    비콘에 대한 정확한 개념도 없이 막 시작하는 터라 궁금한 점이 한두가지가 아닌데, 초면에 무례함을 무릎쓰고 몇가지 질문 드려도 될까요?

    우선 첫 번째로 비콘을 이용하여 자동 출석체크 시스템을 만드려는데, 강의장이라고 인식되는 범위 내에 단말이 들어오면 단말에 앱 알람이 뜨며 해당 강의실의 출석 버튼이 오픈되고, 출석 버튼을 누르면 출석이 인정되게 하는 앱을 구현하고 싶습니다. 가능할지 궁금합니다.

    첫번째단계까지 구현하고 난 다음에는 출석 후 단말이 30분 이상 해당 지역에 없으면 출석인정이 안되도록 하고 싶은데, 비콘에 따라 로그를 남겨 그 기록을 바탕으로 빅데이터 처리를 할 수 있다고 해서 이 기능을 이용하고 싶은데 가능할지 궁금합니다.

    세 번째로는 비콘의 종류는 상관이 없는것인지, 비콘에 따라 구현이 다르게 되는지에 대한 궁금증입니다.

    바쁘신 와중에 질문드려 죄송합니다. 그리고 읽어주셔서 감사합니다.

  5. 1, 원하는 기능 구현 가능합니다. 미들웨어 형태의 서버가 필요하겠네요. 모바일 앱에서 비콘 신호를 받아 출석버튼을 활성화 하고 버튼 누르면 서버를 통해 출석처리하도록 요청하도록 하세요.

    2, 모바일 앱을 계속 켜놓고, (예로)1분마다 서버로 비콘과의 거리등을 전송하면 서버에서 30분이상 해당 지역에 있었는지 판단할 수 있습니다.

    3, 비콘이 아이비콘, 알트비콘 주종의 표준을 따랐다면 비콘의 종류와 관계없이 대부분의 비콘을 연동할 수 있습니다.

    비콘 연동하는 내용은 http://blog.hjf.pe.kr/384 을 참고하세요.

  6. Blog Icon
    박상우

    빠른 답변 대단히 감사드립니다. 혹시 eclipse에서도 구현할 수 있는 기능인가요? 제가 pascal어를 공부해본 적도, rad studio를 사용해본 적도 없어서 잘 할 수 있을지가 걱정이 됩니다..답변 다시한번 대단히 감사합니다. 큰 도움이 되었습니다.

  7. 개념과 구성만 잘 잡고 개발하신다면 툴(언어)가 문제가 되진 않을 것입니다.
    자바에서도 비슷한 기능을 제공하니까 살펴보시기 바랍니다.

    기회되면 델파이도 한번 관심 갖어주세요^^
    (델파이 교육과정 : http://devgear.co.kr/edu
    대학생분들은 5명까지 무료로 참석할 수 있습니다.)

  8. Blog Icon

    비밀댓글입니다

  9. 제가 테스트한 시점에 인바디 정보를 다루지 않아 정확한 내용을 모르겠습니다.

    만약, 체중계가 인바디 정보를 제공한다면,
    충분히 가져올 수 있습니다.

    위 샘플에서는 원하는 체중정보를
    프로필 > 서비스 > 특성 > 속성 > 값으로 원하는 값을 찾아서 사용한 것이기 때문에
    위 매커니즘으로 인바디 정보를 제공한다면 가져올 수 있을 것으로 보입니다.

  10. Blog Icon
    이원석

    감사합니다. 공부하는데 많은 도움이 되었습니다.
    다름이 아니오라, 여러개의 블루투스에서 동시에 값을 받아올수 있을까요?
    이를테면 체중계를 여러개 연결하여, 1번 체중계, 2번 체중계 데이터를 동시에 받는
    형태입니다.
    염치없이 질문 드려봅니다.
    감사합니다.

  11. 컴포넌트 구조 상 컴포넌트 여러개를 두고 연결하면 가능할 것으로 생각합니다.

    하지만, 직접해보시고 가능여부를 판단하는 것이 좋습니다.

iOS9의 새로운 기능인 "App transport Security" 예외 허용을 위한 Info.plist xml 수정방법

2015.10.01 13:11

이 글은 엠바카데로의 David I 블로그 글을 인용 및 참고해 작성한 글입니다. 자세한 내용은 원글을 통해서 확인하시기 바랍니다.

http://community.embarcadero.com/blogs/entry/how-to-use-custom-info-plist-xml-to-support-ios-9-s-new-app-transport-security-feature

iOS9의 App Transport Security 예외 허용하기

iOS9에서 애플은 내부적으로 HTTP 프로토콜 요청하는 응용프로그램을 제한하는 새로운 "App Transport Security" 기능을 추가했습니다. 


App Transport Security

“App Transport Security is a feature that improves the security of connections between an app and web services. The feature consists of default connection requirements that conform to best practices for secure connections. Apps can override this default behavior and turn off transport security. Transport security is available in iOS 9.0 or later, and in OS X v10.11 and later.”


간단하게 요약하면 iOS9 부터는 HTTP(암호화 되지 않은 통신)에 대해서 OS 단에서 제한을 한다는 내용입니다.


애플에서는 Info.plist를 설정해 앱에서 HTTP를 사용할 수 있도록 허용합니다. 

이 글에서는 RAD Studio에서 Info.plist 옵션을 설정하는 방법과 RAD Studio 10 시애틀 샘플소스코드 프로젝트에 대한 링크를 제공합니다.


❑ Info.plist 편집 방법

Info.plist 파일은 프로젝트를 빌드 할때마다 다시 생성됩니다. 그렇기 때문에 Info.plist 파일을 직접 편집하기 위해서는 예를들어 프로젝트의 디렉토리와 같은 위치에 Info.plist 파일을 복사 후 복사한 파일을 편집해야 합니다.

(Info.plist 파일은 iOS Device - 32 bit / 64 bit로 타겟 선택 후 프로젝트를 빌드하면 프로젝트 저장 경로의 하위에 생성됩니다.

예> \iOSDevice32\Debug\Project1.Info.plist)


복사 후 편집한 Info.plist 파일은 Project > Deployement 기능을 통해 별도로 배포해야합니다.

(이 과정은 이 글의 후반부에 다시 안내합니다.)


❑ App Transport Security 옵션 설정

옵션1. "App Transport Security" 비활성화


<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key><true/> </dict>


Info.plist 파일의 제일 마지막으로 이동 후 </dict> 태그 바로 위에 위의 코드를 추가합니다.

이 방법은 TWebBrowser, Indy HTTP 등의 컴포넌트를 사용하는 경우 앱에서 사용하는 모든 HTTP 통신에 대해 App Transport Security 기능을 해제하는 설정입니다.


옵션2. 'App Transport Security" 예외 도메인 등록


<key>NSAppTransportSecurity</key> <dict> <key>NSExceptionDomains</key> <dict> <key>appanalytics.embarcadero.com</key> <dict> <key>NSExceptionAllowsInsecureHTTPLoads</key><true/> </dict> </dict> </dict> 

Info.plist 파일의 제일 마지막으로 이동 후 </dict> 태그 바로 위에 위의 코드를 추가합니다.


이 방법은 특정 도메인(appanalytics.embarcadero.com)으로 접속하는 연결에 대해 보안이 적용되지 않은 HTTP 통신을 허용하도록 설정하는 방법입니다.



❑ Info.plist 배포 설정

앞에서 수정한 Info.plist 파일을 기본 Info.plist를 대신하도록 배포설정해야 합니다. 이 과정은 iOS Device 32 bit와 64 bit 모두에서 진행해야 합니다.(32 비트와 64비트 버전이 약간 다르다는 것을 주의)

배포 관리자(Project > Deployement)로 이동 후 편집한 Info.plist 파일을 추가(Add file)합니다.


아래 그림과 같이 기본 Info.plist 파일을 선택 해제합니다.(배포 대상에서 제외)

AppAnalytics_Info.plist_Project_Deployment

위와같이 배포 설정후 앱을 배포하면 변경된 설정의 Info.plist가 배포됩니다.

관련 링크



험프리.김현수 파이어몽키

[따라하기] 위험지역 경보 시스템 #1 - 비콘을 이용해 위험지역 진입 경보앱 만들기

2015.07.28 18:03

업데이트 내역


2019-05 : 안드로이드 권한 모델 변경(런타임 시 권한 요청)이 적용되도록 업데이트



위 동영상(위험 지역 경보 시스템 데모) 중 위험 지역 진입 시 경보를 발생하는 앱을 따라하기를 통해 만들어 봅니다.

위험지역 진입 경보앱 만들기

❑ 앱 소개

앱 소개

이 앱은 위험지역에 진입 시 화면과 경고음으로 위험지역에 진입을 경고해 주는 (프로토 타입)앱입니다.


따라하기의 목적

따라하기를 통해 비콘을 이용한 위치기반 앱 개발을 시작합니다.


이 따라하기에서는 다음 내용을 다룹니다.

  • 비콘 컴포넌트를 이용해 비콘과의 거리를 활용합니다.
  • UI 컨트롤의 투명도에 애니메이션 효과를 적용해 화면경고 기능을 구현합니다.
  • 사운드 파일(*.mp3)을 앱과 배포해 경보음으로 사용합니다.

따라하기

0, 사전 준비사항

1, 빈 멀티-디바이스 애플리케이션 프로젝트를 생성하고 저장합니다.

2, 메인 UI를 만듭니다.

3, 위험 경보 UI를 추가합니다.

4, 위험경고 화면 효과, 경고음 기능을 추가합니다.

5, 비콘 컴포넌트 추가 후 이벤트를 만듭니다.

6, 비콘과 거리를 기반으로 위험지역 경보 이벤트를 발생합니다.

7, 블루투스 권한설정

8, 디바이스에 배포하고 테스트합니다.

0, 사전 준비사항

개발 도구 준비하기

델파이(또는 RAD Studio)를 실행합니다.(이 실습은 델파이 XE8 버전으로 작성되었습니다.)

만약, 델파이가 설치되지 않았다면 아래의 페이지에서 델파이를 다운로드 받아 설치 후 진행하시기 바랍니다.

따라하기에서 사용할 리소스 다운로드

DangerZoneAlertRes.zip


위 파일을 다운로드 후 적당한 위치에 압축해제하시기 바랍니다.

비콘 준비하기

비콘을 준비합니다.(시중에서 다양한 비콘을 판매하고 있습니다. 저는 Beacon B1을 이용해 진행했습니다.)
시중에서 판매되는 (표준을 지키는)대부분의 비콘과 연동됩니다.

비콘 정보 확인/설정하기

비콘의 UUID와 Major Id, Minor Id를 알아야 합니다. 제조사 사이트 또는 비콘 스캐너 앱을 통해 파악할 수 있습니다.
보통 같은 제품의 비콘은 동일한 UUID, Major Id, Minor Id로 설정되어있습니다. 비콘 별로 구별하기 위해 Major Id, Minor Id를 변경해야 하는데, 제조사의 안내에 따라 Major Id, Minor Id를 다르게 설정하기 바랍니다.(대부분 제조사에서 전용 앱을 제공합니다.)

1, 빈 멀티-디바이스 애플리케이션 프로젝트 생성

  1. File > New > Multi-Device Application - Delphi 메뉴를 선택 하고, Blank Application을 선택해 프로젝트를 생성합니다.
  2. File > Save all 메뉴를 선택 해 유닛이름을 "MainForm.pas"로 프로젝트 이름을 "DangerZoneAlert"으로 저장합니다.

2, 메인 UI를 만듭니다.

모바일 앱 스타일로 화면 구성하기 위해 폼디자이너 스타일을 Android로 변경합니다.


다음화면과 표를 이용해 위험지역 경보 앱 메인 UI를 개발합니다.
(이 따라하기는 비콘 활용 기능에 집중하기 위해 최소한의 UI로 구성했습니다.)


상위 오브젝트

오브젝트

속성

값(또는 설명)

 Form1

 ToolBar1

 

 

 ToolBar1

 Label1

 Align

 Client

 Text

 위험지역 경보 앱

 TextSettings.HorzAlign

 Center

 Switch1

 Align

 Right

 Form1

 Image1

 MultiResBitmap

 "factory.png 선택"

 StatusBar1

 Height

 32

 StatusBar1

 Label2

 Align

 Client

 Text

 위험지역과의 거리: 

3, 위험 경고 UI를 추가합니다.

위험 지역 진입 시 화면으로 경고를 주기위한 UI를 추가합니다. 붉은색 배경에 안내문구를 표시하고, 화면을 깜빡이게 합니다.

화면을 깜빡이는 기능은 다음 단계에서 진행합니다.


다음화면과 표를 이용해 위험 경보 UI를 추가합니다.


상위 오브젝트 

 오브젝트

 속성

 값(또는 설명)

 Form1

 Rectangle1

 Align

 Client

 Fill.Color

 Red

 Opacity

 0.8

 Rectangle1

 Layout1

 Align

 Center

 Width/Height

 228/186(적당히)

 Layout1

 Label3

 Align Text
 Text

 위 그림 참고(위험지역에 ...)

 lblDistance Align Bottom
 Name lblDistance

 TextSettings.Font.Size

 24
 TextSettings.HorzAlign Center

4, 위험경고 화면 효과, 경고음 기능을 추가합니다.

위험지역 진입을 알리기위해 경고음을 발생하고, 경고화면을 표시 후 화면을 깝빡여주는 기능을 구현합니다.


경고음을 실행하기위해 TMediaPlayer 컴포넌트를 경고화면을 깜빡이기 위해 TFloatAnimation 컴포넌트를 추가합니다.


Rectangle1의 FloatAnimation은 Opacity 속성 추가 메뉴(Create New TFloatAnimation)를 선택해 생성합니다.


상위 오브젝트

오브젝트

속성

값(또는 설명)

 Form1

 MediaPlayer1

 

 

 Rectangle1

 FloatAnimation1

 Autoreverse

 True

 Duration

 1

 Loop

 True

 PropertyName Opacity

 StartValue/StopValue

 0.8/0.2

경고음 사운드 파일 배포등록하기

구현에 앞서 경고음으로 사용할 사운드파일을 배포관리자에 등록힙니다.

  1. 메인메뉴에서 Project > Deployment 메뉴를 선택해 Deployment 윈도우를 표시합니다.
  2. Add files 버튼을 누르고 "alert.mp3"파일을 선택해 추가합니다.
  3. 배포 플랫폼 선택 콤보박스를 Android platform과 iOS Device platform으로 선택하고, 아래를 참고해 Remote Path를 수정합니다.

    • Android platform - assets\internal\
    • iOS Device platform - StartUp\Documents\

경고 기능 구현하기

private 영역에 아래 코드를 참고해 4개의 메소드를 선언합니다.

  private
    { Private declarations }
    procedure StartWarning; // 위험지역 진입 경고 시작
    procedure StopWarning;  // 위험지역 진입 경고 중지

    procedure StartSiren;   // 사이렌 시작
    procedure StopSiren;    // 사이렌 중지
  public

Ctrl + Shift + C 단축키(Complete Class at Cursor)를 이용해 메소드의 구현부를 자동완성합니다.

아래 코드를 참고해 구현부를 완성합니다.

procedure TForm1.StartSiren;
begin
  MediaPlayer1.FileName := TPath.Combine(TPath.GetDocumentsPath, 'alert.mp3');
  MediaPlayer1.Play;
end;

procedure TForm1.StopSiren;
begin
  MediaPlayer1.Stop;
end;

procedure TForm1.StartWarning;
begin
  Rectangle1.Visible := True;
  FloatAnimation1.Enabled := True;

  StartSiren;
end;

procedure TForm1.StopWarning;
begin
  Rectangle1.Visible := False;
  FloatAnimation1.Enabled := False;

  StopSiren;
end;

사운드파일 경로 설정을 위해 사용한 TPath가 선언된 System.IOUtils 유닛을 유즈절에 추가합니다.

5, 비콘 컴포넌트 추가 후 이벤트를 만듭니다.

비콘 컴포넌트(TBeacon)는 안드로이드, iOS, OSX를 지원합니다. 타겟 플랫폼을 안드로이드로 변경 후 폼위에 비콘 컴포넌트를 추가합니다.


모니터링 대상 비콘 등록

비콘을 모니터링하기 위해 비콘 정보를 등록합니다.

  1. Beacon1 컴포넌트의 MonitorizedRegions 속성을 선택 후 확장 버튼을 클릭합니다.
  2. 편집창에서 Add new 버튼을 눌러 항목을 추가합니다.
  3. 대상 비콘의 UUID, Major, Minor 값을 입력합니다.

(만약, Major, Minor 값을 -1로 설정하면 UUID만 이용해 대상을 탐색합니다.)


비콘 인스턴스 할당하기

비콘 변수를 선언하고, BeaconEnter, BeaconExit 이벤트에서 비콘 인스턴스를 할당해줍니다.


private 영역에 변수를 선언합니다.

  private
    { Private declarations }
    FBeacon: IBeacon;

 비콘 컴포넌트의 OnBeaconEnter, OnBeaconExit 이벤트 핸들러를 만들고 비콘 인스턴스를 할당합니다.

(이벤트 핸들러는 이벤트의 우측 영역을 더블클릭하면 생성됩니다.)


procedure TForm1.Beacon1BeaconEnter(const Sender: TObject;
  const ABeacon: IBeacon; const CurrentBeaconList: TBeaconList);
begin
  FBeacon := ABeacon;
end;

procedure TForm1.Beacon1BeaconExit(const Sender: TObject;
  const ABeacon: IBeacon; const CurrentBeaconList: TBeaconList);
begin
  FBeacon := nil;
end;

비콘 컴포넌트 동작을 활성화하기

안드로이드 권한 모델 변경으로 10.3 이상의 버전은 런타임 시 권한 요청하도록 변경이 필요합니다.

10.2 이전 버전
스위치(Switch1)를 키면 비콘이 동작하도록 Switch1의 OnSwitch 이벤트 핸들러 생성 후 아래 코드를 입력합니다.
procedure TForm1.Switch1Switch(Sender: TObject);
begin
  Beacon1.Enabled := Switch1.IsChecked;
end;

10.3 이상 버전(런타임 시 권한 요청)
구현부 uses 절에 필요한 유닛을 추가합니다.

implementation

uses
  System.Permissions,
{$IFDEF ANDROID}
  Androidapi.JNI.Os,
  Androidapi.JNI.JavaTypes,
  Androidapi.Helpers,
{$ENDIF}
  FMX.DialogService;

{$R *.fmx}

스위치(Switch1)를 키면 권한을 요청하고, 비콘이 동작하도록 Switch1의 OnSwitch 이벤트 핸들러 생성 후 아래 코드를 입력합니다.

procedure TForm1.Switch1Switch(Sender: TObject);
begin
{$IFDEF ANDROID}
  if Switch1.IsChecked then
  begin
    PermissionsService.RequestPermissions(
      [JStringToString(TJManifest_permission.JavaClass.BLUETOOTH),
      JStringToString(TJManifest_permission.JavaClass.BLUETOOTH_ADMIN)],
      procedure(const APermissions: TArray<string>; const AGrantResults: TArray<TPermissionStatus>)
        begin
          if (Length(AGrantResults) = 2)
          and (AGrantResults[0] = TPermissionStatus.Granted)
          and (AGrantResults[1] = TPermissionStatus.Granted) then
            { activate or deactivate the location sensor }
            Beacon1.Enabled := True
          else
          begin
            Switch1.IsChecked := False;
            TDialogService.ShowMessage('블루투스 권한이 없습니다.');
          end;
        end);
  end
  else
    Beacon1.Enabled := False;
{$ELSE}
  Beacon1.Enabled := Switch1.IsChecked;
{$ENDIF}
end;

6, 비콘과 거리를 기반으로 위험지역 경보 이벤트를 발생합니다.

이제 모든 준비는 완료되었고, 비콘과의 거리를 계산해 위험지역 진입 경보 이벤트를 발생하면 됩니다.

위험지역 진입에 대한 경보는 아래와 같은 기준으로 시작하고 중단합니다.

위험지역 진입 경보 기준

  1. 비콘과의 거리가 1.0미터 이하인 경우 위험지역으로 간주한다.
  2. 경보가 아닌 상태에서 위험지역에 3초이상 연속으로 머무른 경우 경보를 시작한다.
  3. 경보가 발생중인 상태에서 위험지역을 3초이상 연속으로 벗어난 경우 경보를 중단한다.

비콘과 거리를 주기적으로 확인하기 위해 타이머 컴포넌트(Timer1)를 폼에 추가합니다.


아래 변수를 private 영역에 선언합니다.

    FIsWarning: Boolean;          // 경고 중인가?
    FDangerAreaStaySecs,          // 위험지역에 머무른 시간(초)
    FDangerAreaOutSecs: Integer;  // 위험지역에서 벗어난 시간(초)


Timer1의 OnTimer 이벤트 핸드러를 생성하고, 아래 코드를 추가합니다.

procedure TForm1.Timer1Timer(Sender: TObject);
var
  InDangerArea: Boolean;  // 위험지역에 있는가?(비콘과의 거리가 1m 이내인가?)
begin
  if Assigned(FBeacon) then
  begin
    InDangerArea := FBeacon.Distance <= 1;

    // 경고 중 아님
      // 위험지역에 3초간 머무른 경우 경고 시작
    if not FIsWarning then
    begin
      if InDangerArea then
        Inc(FDangerAreaStaySecs)
      else
        FDangerAreaStaySecs := 0
      ;

      if FDangerAreaStaySecs >= 3 then
      begin
        FIsWarning := True;
        StartWarning;
        FDangerAreaStaySecs := 0
      end;
    end
    // 경고 중
      // 위험지역을 3초 이상 벗어난 경우 경고 중단
    else if FIsWarning then
    begin
      if not InDangerArea then
        Inc(FDangerAreaOutSecs)
      else
        FDangerAreaOutSecs := 0;

      if FDangerAreaOutSecs >= 3 then
      begin
        FIsWarning := False;
        StopWarning;
        FDangerAreaOutSecs := 0;
       end;
    end;

    // 위험지역과의 거리를 표시
    lblDistance.Text := FBeacon.Distance.ToString;
    Label3.Text := '위험지역과의 거리: ' + FBeacon.Distance.ToString + ' m';
  end;
end;

7, 블루투스 권한설정

(이 과정은 안드로이드 플랫폼을 사용하지 않는 경우 생략할 수 있습니다.)

비콘은 블루투스 LE(Low Energy) 기반으로 통신합니다.  블루투스 사용권한을 설정합니다.

Project > Options 메뉴를 선택 후 Uses Permissions 화면으로 이동합니다.


Bluetooth와 Bluetooth admin 두개의 권한을 사용하도록 설정합니다.

8, 디바이스에 배포하고 테스트합니다.

처음 디바이스에 배포하는 경우 먼저 모바일 개발환경 설정하고 진행하기 바랍니다.


개발을 완료했습니다. 이제 준비된 개발환경으로 플랫폼을 선택 후 Run > Run Without Debugging 메뉴를 이용해 실행하고 테스트 합니다.



테스트 방법 : 

  • 앱을 실행 후 비콘과 1미터 이내로 접근 후 3초이상 머무르면 경고가 발생
  • 경고 발생 중 1미터 초과 거리로 이동 후 3초이상 머무르면 경고 중단


참고자료



험프리.김현수 파이어몽키 Beacon, IOT, 비콘, 사물인터넷

  1. Blog Icon

    비밀댓글입니다

  2. 이번 따라하기에는 로그로 기록하고, 관리자에게 보고하는 내용이 포함되지 않았습니다.

    영상에서 사용된 앱에서는 주기적으로 클라우드에 위험지역과의 거리를 기록하고, 위험지역에 머무른 경우 관리자에게 푸쉬메시지(GCM)을 전송했습니다.

    이 내용에 관심이 있으시다면 제가 진행하는 세미나에서 자세한 이야기를 들어보세요.(8월 11일은 이미 신청완료 됐고, 9월 3일에 앵콜세미나가 진행됩니다.)
    http://tech.devgear.co.kr/board_in1/413343

  3. Blog Icon

    비밀댓글입니다

  4. 비콘은 XE8에서 추가된 컴포넌트입니다.

    업데이트 또는 트라이얼 버전으로 진행해주세요^^

  5. Blog Icon
    김태규

    그럼 따로 비콘 업체에서 제공해주는 api 가져다가 만들어야하나요?

    비콘 컴포넌트 없이도 제작은 가능하죠??

  6. 네 비콘 표준을 기반으로 직접 구현할 수 있습니다.(iBeacon, AltBeacon을 필요한 플랫폼 별로 구현해야 합니다.) 하지만 쉽지 않습니다.ㅠㅜ

    트라이얼로 테스트 해보시고 최신버전으로 업그레이드하셔서 컴포넌트 이용하는 것을 추천드립니다.

    그리고 모바일 개발 하시려면 잦은 모바일 OS업데이트로 델파이 버전도 빠르게 올라가기 때문에 업데이트 서브스크립션을 구매하시는 것이 아주 좋습니다.
    http://www.devgear.co.kr/products/rad-studio/update-subscription/

  7. Blog Icon
    정재용

    비콘과 미디어플레이어 경고 알람 기능이 포함된 소스는 구할 수 없을까요?

    비콘만 구현하면 잘 돌아가고,

    미디어만 구현해도 잘 돌아가는데,

    간단하게 섞을라 치면 어플이 안드로이드핸드폰에서 실행하자마자 꺼지네요.

    휴대폰 리소스 문제일까요?

    비교해보고싶어서 그런데 사용예제를 git으로 얻을 수 있을까 여쭤봅니다.

  8. 이 따라하기가 비콘과 미디어플레이어를 이용하고 있습니다.
    혹시 따라하기로 잘 안되시면 아래 깃헙링크에서 다운로드받아 테스트해보세요.
    (단, 배포: Project > Deployment 부분에서 파일 배포 부분을 잘 검토해 보세요.)
    https://github.com/devgear/RADStudioMobileIoTEdu/tree/master/20150828/BeaconDemo3

  9. Blog Icon
    박문수

    안녕하세요 이번에 비콘을 구매하여서 위험지역 경보 앱을 따라 제작하며

    추천해주신 비콘중에 Gimbal 제품을 구매하여 테스트 하는데 이 제품은 UUID가 따로 없는데

    혹시 이 제품으로 연동하는 방법을 알 수 있을까요?

  10. 제조사 홈페이지에서 찾아보시거나,
    모바일에서 Beacon Scan 기능을 제공하는 앱을 다운받아 비콘을 스캔해 보시기 바랍니다.
    안드로이드인 경우 Beacon Scanner, 아이폰인 경우 Locate 앱을 이용하면 됩니다.

  11. Blog Icon
    박문수

    답변 감사드립니다.
    이야기 해주신 어플은 스캔이 안되어서 다른 어플(BeaconFinder)로 하니깐 나와서 UUID를 입력 후 어플을 돌리면 비콘이 안잡히고 있습니다.
    그래서 다시 UUID 확인을 하면 특정위치 한자리가 변경이 되고 있습니다.
    혹시 이게 왜 이런지 아시는거 있으신가요??

  12. 이 현상은 비콘 스캐너 앱을 의심해봐야 할것 같네요.
    다양한 앱으로 시도해보시기 바랍니다.

    그리고 비콘 제조사 홈페이지에서 UUID를 확인해보시구요.
    제조사에서 제공하는 전용 앱이 있다면 해당 앱으로도 탐색해보세요.

  13. Blog Icon
    이원진

    안녕하세요? 좋은 자료 감사합니다.
    제가 하고자 하는것은 비콘을 자동으로 인식 하고자 하는데..
    보편적으로 매장내에서 비콘을 활용하고자 할경우에 uuid를 입력하는 절차가 있으면 곤란하고요
    uuid를 직접 입력하지 않고 uuid정보를 가져올수 있는 방법은 없을까요?

  14. Blog Icon
    MG

    다음 강의도 올려주세요~

  15. Blog Icon
    박한수

    안녕하세요 이 글을 읽고 비콘이 멀어질때 알람이 울리는 식으로 응용을 해보았습니다. 그리고 비콘을 하나에서 세개로 늘려서 해보았는데

    각 비콘에 동일하게 스위치를 따로 달아서 진행하였고 추가되는 비콘들은

    procedure TForm1.Beacon2BeaconEnter(const Sender: TObject;
    const ABeacon: IBeacon; const CurrentBeaconList: TBeaconList);
    begin
    GBeacon :=ABeacon;
    end;

    이처럼 GBeacon이나 HBeacon으로 선언하여 이벤트를 만들었습니다. 알람부분에서도 타이머를 추가하여 변수 (FIsWarning, StopWarning,FDangerAreaOutSecs 등)도 GBeacon을 위한 새로운 변수(GIsWarning, GDangerAreaOutSecs 등)로 따로 선언을 해 두었습니다.

    이렇게 하니 하나의 스위치를 켜서 할때는 각각 모두 잘 되는데 스위치를 여러개를 켜면 하나의 비콘만 인식을 하고 다른것은 인식을 못하였습니다. 혹시 어디가 잘못되었는지 알 수 있을까요?

  16. Blog Icon
    EH

    4번 항목에
    'lblDistance'라는 오브젝트가 없어서 그러는데요.
    그걸 추가하려면 어떻게 해야하나요?

  17. lblDistnace는 TLabel의 이름(Name)을 lblDistance라고 변경한 것입니다.

  18. Blog Icon
    정진희

    안녕하세요 안드로이드 스튜디오 환경 또는 이클립스에서 어플을 제작하려고 합니다.
    비콘은 아두이노를 이용하여 제작하였고, 이제 제작한 비콘에서 거리 정보를 핸드폰으로 받아오는게 지금 해결해야하는 단계입니다.
    근데 어플을 만들어보는게 처음이라...
    위에 글을 읽어보니까 개발도구로 델파이를 사용하셨고 비콘 컴포넌트라는 것을 사용하셨다고 나와있는데
    혹시 저기 나와있는 비콘 컴포넌트는 델파이에서만 사용할 수 있는건가요?
    아니면 안드로이드 스튜디오 또는 이클립스에서 동일하게 사용할 수 있는건가요?
    감사합니다!

  19. 위 글에서 설명하는 비콘 컴포넌트는 델파이에서 사용해야 합니다.

    델파이 앱개발도 한번 검토해 보세요^^

    데브기어 사물인터넷 1일 교육과정에서 비콘 연동 등을 포함한 실습위주 교육을 진행하고 있습니다.
    하루만에 완성된 앱을 돌려보고 가져갈 수 있습니다.
    http://devgear.co.kr/edu

  20. Blog Icon
    박은정

    따라하기로 어플을 만들어보았는데요
    정확하게 다 한거같은데 어플에서 비콘을 인식을 못하는거 같아요 이런경우에는 어떻게 해야하죠?

[XE8] 모바일 앱에서 클래식 블루투스를 이용해 데이터 전송하기

2015.07.27 18:16

지난 7월 24일 사물인터넷 앱 개발 with RAD Studio 교육을 진행했습니다.

교육이 끝나고 한 교육생분이  "클래식 블루투스로 데이터 전송이 잘되지 않아요."라는 질문을 받고 테스트한 내용 공유합니다.


클래식 블루투스 기능 테스트는 RAD Studio 기본 샘플 중 "Classic Bluetooth Basic app"으로 진행했습니다.

Classic Bluetooth Basic app

RAD Studio XE8 사용자는 다음 경로에서 샘플을 확인할 수 있습니다.

C:\Users\Public\Documents\Embarcadero\Studio\16.0\Samples\Object Pascal\Mobile Samples\Device Sensors and Services\Bluetooth\Classic Bluetooth Basic app


이 샘플은 다음 기능을 포함하고 있습니다.

  • 하나의 앱에서 서버(서비스 생성)와 클라이언트 역할을 모두 제공합니다.
  • 장비를 탐색하고, 페어링하고, 서비스를 탐색합니다.
  • 연결된 서비스로 데이터를 전송하고, 서비스는 데이터를 수신합니다.

❑ 다른 서비스에 접속 시 주의할 점과 처리방법

이 샘플은 문자열 전송 서비스를 목적으로 구현되어 있습니다.(클라이언트 소켁 생성 시 문자열 전송 서비스의 GUID(상수)를 이용해서 클라이언트 소켓을 생성합니다.)

다른 서비스와 접속하도록 하려면 선택한 서비스의 GUID를 이용해 클라이언트 소켓을 생성하도록 수정해야 합니다..


위 내용을 적용하기 위해 아래와 같이 샘플 코드를 변경했습니다.


1, 선택한 서비스의 GUID를 보관하기 위한 변수 선언

서비스 콤보박스(ComboBoxService)에서 선택한 서비스의 GUID를 보관하기 위해 private 영역에 변수를 선언

FServiceGUID: string;


2, 서비스 콤보박스 변경 시 변수에 서비스 GUID 할당

ComboBoxService의 OnChange 이벤트에 선택한 서비스의 GUID를 변수에 할당하도록 구현

procedure TForm1.ComboBoxServicesChange(Sender: TObject);
var
  itemText: string;
begin
  if ComboBoxServices.ItemIndex >= 0 then
  begin
    itemText := ComboBoxServices.Items[ComboBoxServices.ItemIndex];
    FServiceGUID := Copy(itemText,
                          Pos('--> ', itemText) + Length('--> '),
                          Length(itemText));
  end;
end;


3, 선택한 서비스의 GUID로 클라이언트 소켓 생성하도록 수정

상수(ServiceGUI)로 선언된 문자열 전송 서비스 GUID 대신 선택한 서비스의 GUID로 클라이언트 소켓을 생성하도록 아래와 같이 수정

procedure TForm1.SendData;
var
  ToSend: TBytes;
  LDevice: TBluetoothDevice;
begin
  if (FSocket = nil) or (ItemIndex <> ComboboxPaired.ItemIndex) then
  begin
    if ComboboxPaired.ItemIndex > -1 then
    begin
      LDevice := FPairedDevices[ComboboxPaired.ItemIndex] as TBluetoothDevice;
      // [데브기어] 상수로 정의된 서비스 GUID 대신 ComboServices의 GUID 값으로 변경
//      DisplayR.Lines.Add(GetServiceName(ServiceGUI));
      DisplayR.Lines.Add(GetServiceName(FServiceGUID));
      DisplayR.GoToTextEnd;
      // [데브기어] 상수로 정의된 서비스 GUID 대신 ComboServices의 GUID 값으로 변경
//      FSocket := LDevice.CreateClientSocket(StringToGUID(ServiceGUI), False);
      FSocket := LDevice.CreateClientSocket(StringToGUID(FServiceGUID), False);
      if FSocket <> nil then
      begin
// 생략


테스트 환경과 결과

❑ 테스트 환경

테스트는 아래 그림과 같이 블루투스 모듈과 블루투스 모듈을 USB로 연결할 수 있는 USB-TTL 모듈을 이용해 블루투스 데이터를 ComPort로 읽어 오도록 구성했습니다.(관련 장비는 다모아 시스템(www.dma.kr)의 최낙구 대표님께서 제공해 주셨습니다.)


테스트에 사용한 모듈은 아래와 같습니다.

❑ 모바일 앱 - 블루투스 장비와 연결, 데이터 전송


블루투스 장비와 페어링

블루투스 장비와 연결하기 위해서는 페어링 과정이 필요합니다.

[Discover devices] 버튼으로 주변의 장비를 탐색하고, 탐색한 장비 선택 후 [Pair] 버튼으로 페어링 시도합니다.


이미 페어링 되어 있는 장비라면 위 과정을 생략할 수 있습니다.(위 페어링 과정은 OS에서 제공하는 페어링 기능을 이용할 수도 있습니다.)

블루투스 장비로 데이터 전송

페어링된 장비를 선택 후 [Services] 버튼을 누르면 장비에서 제공하는 서비스를 가져올 수 있고, 서비스를 선택 후 [Service Demo] 탭에서 [Send text to] 버튼으로 서비스로 문자열을 전송할 수 있습니다.

COM Port 소프트웨어

위 과정을 통해 모바일에서 전송한 데이터는 블루투스 모듈을 통해 ComPort로 데이터를 수신할 수 있습니다.




험프리.김현수 파이어몽키 XE8, 블루투스, 클래식 블루투스

  1. 베를린 기준 샘플 컴파일 시 Permission Denied.
    pm에서 블루투스 권한이 하나 안들어가 있어서 다시 부여하고..
    컴파일해서 페어링, 서비스 탐색까지 확인되나
    시리얼 전송 시 Java.IOException 나옵니다.
    해당 샘플에 문제가 있어 보입니다.

    다른 아두이노, 라즈베리, AVR 같은 임베디드 단말이나 마켓에 있는 다른 앱으로는 BT와 시리얼 통신이 잘 이루어집니다.

    테스트 환경은 안드로이드 4.4.2 6.0.1 두가지 입니다.

  2. 글쎄요. 샘플에 문제가 있다고 단정지으면 답이 없어질것 같습니다.(단, 어느 지점이 문제가 있다고 정확한 지적은 발전될 수 있습니다^^)

    저도 처음 시도할때 시행착오를 많이 겪었는데요. 결과적으로 보면 페어링과 연결(GUID)이 잘못된 부분을 찾아내서 해결하니 정상 연결 및 데이터 송수신을 확인할 수 있었습니다.

    문제가 있으시다고 보시면 디버그 모드로 실행하셔서 문제를 확인하고, 문제를 해결하거나 피해가는 형태로 개발해 보시면 반드시 해결할 수 있을 겁니다.

모바일 앱 실행 시 검은화면이 표시되는 경우 조치방법

2015.07.02 16:39

모바일 기기에 앱을 넣고 실행해보면 가끔 어찌된 영문인지 앱 실행 시 검은화면이 표시되고 더이상 구동되지 않는 경우가 발생합니다. 

검은화면은 앱 실행 시 오류가 발생한 경우 표시됩니다.


실행 시 발생하는 오류는 다양하지만 자주 발생하는 이슈는 아래와 같습니다.

  • FormCreate, FormShow 이벤트에 작성한 코드에서 오류 발생
  • 컴포넌트 Enable(또는 Active) 속성을 True로 설정 후 실행하면 실행 시 활성화 되고 활성화 시 오류 발생 가능
  • 필요한 라이브러리가 함께 배포되지 않은 경우
  • (안드로이드 경우)권한이 없는 기능을 사용한 앱이 구동 시 오류 발생

위 내용의 조치방법은 아래와 같습니다.

1, FormCreate(또는 FormShow) 이벤트에 작성한 코드에서 오류 발생

FormCreate 또는 FormShow 이벤트핸들러에 작성한 코드에서 오류가 발생하면 구동 시 검은 화면이 발생합니다.

오류가 발생하지 않도록 코드를 점검하고, 예외처리(try..except)를 추가하기 바랍니다.


또, 라이프사이클 이벤트를 이용해 앱 완전 구동 후 작업을 수행할 수 있습니다.

2, 컴포넌트 Enable(또는 Active)속성을 True로 설정 후 실행하면 실행 시 활성화 되고 활성화 시 오류 발생 가능

폼위에 올려놓은 컴포넌트도 내부적으로는 앱이 실행될때 코드가 실행됩니다. 특히 Enable과 Active 속성을 갖는 컴포넌트의 경우 앱 실행 시 활성화됩니다. 이때 컴포넌트 활성화에 필요한 정보가  설정되지 않은 경우 오류가 발생합니다.

예를 들어 DB 연결 시 DB의 경로가 플랫폼의 경로로 설정되지 않았다면, 오류가 발생할 수 있습니다.


이 경우, 앱이 구동된 이후 Enabled, Active 속성을 True로 변경하도록 처리하기 바랍니다.

3, 필요한 라이브러리가 함께 배포되지 않은 경우 오류가 발생할 수 있습니다.

데이터 연결 등 외부 라이브러리를 이용하는 경우, 외부 라이브러리 배포를 하지 않고 앱을 실행하면 구동 시 오류가 발생합니다.


이 경우, 배포관리자(Project > Deployment) 화면에서 필요한 라이브러리를 추가하기 바랍니다.

그리고, 플랫폼별 배포경로를 다음 링크에서 확인해 Remote Path를 설정하기 바랍니다.

팁으로 데이터 엑세스 관련된 라이브러리 파일은 Add Featured Files 기능을 이용해 추가할 수 있습니다.


4, (안드로이드 경우)권한이 없는 기능을 사용한 앱이 구동 시 오류가 발생할 수 있습니다.

블루투스 컴포넌트를 폼에 올리고 안드로이드에서 앱을 실행하려면 블루투스 권한을 설정해야 합니다.


사용 권한은 프로젝트 옵션(Project > Option > Uses Permissions)에서 설정할 수 있습니다. 


권한에 대한 자세한 정보는 엠바카데로 기술문서를 참고하세요.

5, 기타 오류가 발생할 수 있는 경우

멀티-디바이스 뷰를 추가 후 추가한 뷰에 설정 값을 잘 못 넣은 경우

위 그림과 같이 멀티-디바이스 디자이너는 여러가지 플랫폼과 해상도에 맞는 디바이스 뷰를 추가해 폼 디자인을 다르게 구성할 수 있습니다.


간혹, 디자인 뿐 아니라 연결 컴포넌트 등의 속성을 추가한 뷰(위 그림에서 Android 5" Phone)에 잘못된 설정을 하고, Master 뷰에서 원인을 찾을 경우 쉽게 못찾는 경우가 있습니다.


위 문제의 해결책은 되도록, 연결 등의 Non-visual 컴포넌트는 데이터모듈에 추가하도록 합니다. 부득이 폼에 추가한 경우 Master 뷰에서 속성을 변경 해 추가한 뷰에서 Master의 속성을 상속받아 실행되도록 합니다.

험프리.김현수 파이어몽키

[하이브리드 앱 개발] 웹페이지에서 델파이 함수 호출하기

2015.06.26 14:00

델파이는 성능좋은 네이티브 모바일 앱을 개발할 수 있는 개발 도구입니다.

모든 기능을 네이티브로 개발할 수 있지만, 이미 반응형 웹페이지를 갖고 있는 경우 일부 기능을 웹브라우를 통해 구현해 하이브리드 형태로 개발 할 수 있습니다.


이 경우 델파이 코드에서 웹페이지의 자바스크립트를 호출하는 방법은 웹브라우저에서 제공하는 메소드(EvaluateJavaScript)를 사용할 수 있지만, 반대로 웹브라우저에서 델파이 코드를 호출하는 방법은 잘 모르실 것 같아 샘플을 만들어 공개합니다.


이글에서는 다음 기능을 소개합니다.

  • 델파이 코드로 웹브라우저의 자바스크립트 코드 호출하기
  • 웹브라우저에서 델파이 메소드 호출하기(파라메터 포함)


기본 구성

❑ 웹페이지 구성

웹페이지(http://hjf.pe.kr/fmx/hybrid.php)의 자바스크립트 코드는 다음과 같습니다.

웹페이지에서 델파이 코드 호출은 아래와 같이 2개의 버튼으로 처리합니다..

	

❑ 델파이 앱 화면 구성


델파이에서 자바스크립트 호출하기

델파이에서 웹브라우저 안의 자바스크립트 호출은 EvaluateJavaScript 메소드를 이용합니다. 자바스크립트 함수 뿐 아니라 여러줄의 구문도 실행할 수 있습니다.

델파이 코드

델파이에서 자바스크립트의 callJSMethodFromDelphi 함수 실행

procedure TForm1.Button1Click(Sender: TObject);
begin
  WebBrowser1.EvaluateJavaScript('callJSMethodFromDelphi();');
end;

웹페이지의 자바스크립트 코드

자바스크립트는 다음과 같이 메시지 다이얼로그를 출력하도록 되어있습니다.

function callJSMethodFromDelphi(){
	alert("[JS] Call from delphi");
}

테스트 결과

EvaluateJavaScript 메소드 자세히 보기(웹브라우저 html 코드를 적용할 수 있는 LoadFromStrings 메소드도 활용도가 높습니다.)

웹페이지에서 델파이 메소드 호출하기

웹페이지에서 델파이 메소드를 호출하는 원리는 아래와 같습니다.

  1. 델파이는 웹브라우저 주소가 변경되면 이벤트를 받을 수 있다.
  2. 웹페이지에서 웹브라우저 주소를 변경할 수 있다.
  3. 웹페이지에서 주소를 http:// 형태의 페이지 주소가 아닌, 델파이 메소드를 호출하라는 사용자 포맷의 주소를 호출한다.
  4. 델파이에서 주소 변경 이벤트 발생 시 사용자포맷 주소인 경우, 주소를 분석 해 델파이 메소드를 호출한다.

❑ 델파이 파이어몽키용 웹브라우저 주소 변경 이벤트 종류

웹브라우저(TWebBrowser)의 주소변경 이벤트는 다음과 같습니다.

이벤트

 발생 조건

 OnDidFailLoadWithError

  페이지 로드 중 에러가 난 경우 발생

 OnDidFinishLoad

  페이지 로드가 완료한 경우 발생
 OnDisStartLoad  페이지 로드를 시작할때 발생
 OnShouldStartLoadWithRequest

  요청에 의해 페이지 로드를 시작할때 발생


이 데모에서는 페이지 로드 요청 시 발생하는 OnShouldStartLoadWithRequest 이벤트를 이용합니다.

(메소드 호출과정에 페이지가 로드되지 않기 때문에 OnDisStartLoad 이벤트는 발생하지 않습니다.)


❑ 웹페이지에서 델파이 메소드 호출하기

웹페이지에서 델파이 메소드 호출은 아래 자바스크립트를 호출해 동작합니다.

function callDelphiMethodFromJS(){
	window.location = "jscall://callDelphiMethodFromJS";
}

function callDelphiMethodFromJSWithParam(){
	window.location = "jscall://callDelphiMethodFromJSWithParam?Hello|1234";
}


위 자바스크립트 코드는 웹페이지 주소(window.location)를 변경하는 코드로, 사용자 포맷(jscall://)으로 주소를 변경합니다.


제가 정의한 포맷은 다음과 같습니다.(임의로 정한 규칙임으로 원하는 방식으로 정의해서 사용하기 바랍니다.)

 포맷 정의

용도 

 jscall:// 

 델파이 코드를 호출하는 주소라는 의미

 callDelphiMethodFromJSWithParam

 호출할 델파이 코드명

 ?Hello|1234

 "?" 뒤에는 파라메터를 추가합니다. 파라메터는 파이프("|")로 구분합니다.


다음으로 웹페이지에서 위 포맷으로 주소 변경 시, 델파이에서 감지해 메소드를 호출하도록 구현합니다.

❑ 델파이에서 사용자 포맷 주소 감지하고 분석하기

델파이 웹브라우저 OnShouldStartLoadWithRequest 이벤트에는 아래와 같이 구현했습니다.

procedure TForm1.WebBrowser1ShouldStartLoadWithRequest(ASender: TObject;
  const URL: string);
var
  MethodName: string;
  Params: TArray;
begin
  if ProcessMethodUrlParse(URL, MethodName, Params) then
  begin
    CallMethod(MethodName, Params);
  end;
end;


ProcessmethodUrlParse 메소드로 URL을 분석하고, 메소드 URL인 경우 메소드를 호출하는 CallMethod를 호출합니다.

메소드 URL 분석

// URL format
//  jscall://{method name}?{Param1}|{ParamN}
//  e.g> jscall://callDelphiMethodFromJSWithParam?Hello|1234
function TForm1.ProcessMethodUrlParse(AUrl: string;
  var MethodName: string; var Parameters: TArray<TValue>): Boolean;
const
  JSCALL_PREFIX = 'jscall://';
  JSCALL_PREFIX_LEN = Length(JSCALL_PREFIX);
var
  I: Integer;
  ParamStr: string;
  ParamArray: TArray;
begin
  Result := False;

  // iOS에서 특수기호(|)가 멀티바이트로 넘어옴
  AUrl := TNetEncoding.URL.Decode(AUrl);

  if AUrl.IndexOf(JSCALL_PREFIX) = -1 then
    Exit(False);

  if AUrl.IndexOf('?') > 0 then
  begin
    MethodName := AUrl.Substring(JSCALL_PREFIX_LEN, AUrl.IndexOf('?')-JSCALL_PREFIX_LEN);

    ParamStr := AUrl.Substring(AUrl.IndexOf('?')+1, Length(AUrl));
    ParamArray := ParamStr.Split(['|']);
    SetLength(Parameters, length(ParamArray));
    for I := 0 to Length(ParamArray)-1 do
      Parameters[I] := ParamArray[I];
  end
  else
    MethodName := AUrl.Substring(JSCALL_PREFIX_LEN, MaxInt);
  if MethodName.IndexOf('/') > 0 then
    MethodName := MethodName.Replace('/', '');

  Result := not MethodName.IsEmpty;
end;

❑ 분석한 메소드 호출

웹페이지에서 호출할 델파이 메소드는 아래와 같이 자바스크립트와 동일한 이름으로 작성했습니다.

procedure TForm1.callDelphiMethodFromJS;
begin
  ShowMessage('[DELPHI] Call from JS');
end;

procedure TForm1.callDelphiMethodFromJSWithParam(AStr1, AStr2: string);
begin
  ShowMessage(Format('[DELPHI] Call from JS(%s, %s)', [AStr1, AStr2]));
end;

텍스트 타입의 메소드이름으로 메소드를 호출하는 방법은 간단하게 조건문으로 호출하는 방법도 있고, RTTI를 이용해 메소드이름으로 메소드를 찾아 호출하는 방법으로도 구현할 수 있습니다.

조건문으로 메소드 호출

function TForm1.CallMethod(AMethodName: string; AParameters: TArray<TValue>): TValue;
begin
  if AMethodName = 'callDelphiMethodFromJS' then
    callDelphiMethodFromJS
  else if AMethodName = 'callDelphiMethodFromJSWithParam' then
    callDelphiMethodFromJSWithParam(AParameters[0].AsString, AParameters[1].AsString);
end;


RTTI를 이용해 메소드 호출

function TForm1.CallMethod(AMethodName: string; AParameters: TArray<TValue>): TValue;
var
  RttiCtx: TRttiContext;
  RttiTyp: TRttiType;
  RttiMtd: TRttiMethod;
begin
  RttiCtx := TRttiContext.Create;
  RttiTyp := RttiCtx.GetType(Self.ClassInfo);
  if Assigned(RttiTyp) then
  begin
    RttiMtd := RttiTyp.GetMethod(AMethodName);
    if Assigned(RttiMtd) then
      Result := RttiMtd.Invoke(Self, AParameters);
  end;
  RttiMtd.Free;
  RttiTyp.Free;
  RttiCtx.Free;
end;

테스트 결과



<주의>

안드로이드 앱의 웹브라우저에서 델파이 코드 호출 시 ERR_UNKNOWN_URL_SCHEME 오류가 발생하는 경우 다음 글을 참고해 조치하기 바랍니다.


 - http://blog.hjf.pe.kr/378


샘플 다운로드

다운로드 : 

HybridAppDemoSrc.zip



험프리.김현수 파이어몽키

TWebbrowser의 "tel:, sms:" 링크가 동작하지 않는 이슈 해결방법

2015.06.25 19:31

델파이로 하이브리드 앱 개발 시 안드로이드 환경에서 전화걸기 링크(tel:)를 클릭하면 웹페이지 오류(ERR_UNKNOWN_URL_SCHEME)가 표시됩니다.

원래는 전화걸기 기능이 실행되야 합니다.


이 글은 안드로이드 하이브리드 앱에서 웹브라우저(TWebBrowser)의 전화걸기, 문자전송 등의 링크 클릭 시 해당 기능이 동작하도록 처리하는 방법을 안내합니다.


먼저 웹페이지의 구성과 증상을 살펴봅니다.

웹페이지 구성과 증상

웹페이지(http://hjf.pe.kr/fmx/hybrid.php)에는 아래와 같이 전화걸기, 문자전송, 메일 전송 링크를 제공합니다.

	[Phone number]  / 
	[Send sms]  / 
	[Send mail]


위 페이지를 안드로이드 앱의 웹브라우저에 표시하고, 전화걸기 링크를 클릭하면 다음과 같이 웹 페이지 오류가 표시됩니다.

위 오류가 발생하는 원인은 링크를 클릭하면 관련 액티비티가 처리해야 하는데 웹페이지로 인식해 웹브라우저가 분석하는데 실패하기 때문입니다.

그럼 링크 클릭 시 페이지 이동 대신 기능 액티비티를 표시하도록 수정해보니다.

파이어몽키 안드로이드 웹브라우저 소스 수정

위 링크 이슈를 해결하기 위해서는 파이어몽키 안드로이드 웹브라우저의 소스(FMX.WebBrowser.Android.pas)를 수정 해야합니다.(아래 과정은 델파이 XE8 기준으로 설명합니다. 다른 버전은 소스의 경로와 소스코드 내용 중 일부가 다를 수 있습니다.)

1, 안드로이드 소스코드 복사

아래 경로에서 안드로이드 웹브라우저 소스(FMX.WebBrowser.Android.pas)파일 복사합니다.

  • C:\Program Files (x86)\Embarcadero\Studio\16.0\source\fmx\FMX.WebBrowser.Android.pas (XE8 기준)
위 파일을 델파이 프로젝트 파일(*.dproj)과 같은 경로에 붙여넜습니다.

프로젝트 경로에 파일을 복사한 이유는 델파이 컴파일러는 프로젝트 파일의 경로의 소스파일을 우선적으로 참고해 컴파일합니다. 위 특징을 이용해 프레임워크 소스코드를 프로젝트 파일과 같은 경로에 놓고 수정하면 수정된 내용이 적용되어 컴파일 됩니다.


2, 파이어몽키 소스코드 수정

위에서 복사한 소스코드를 델파이 IDE > File > Open 메뉴로 엽니다.
그리고, shouldOverrideUrlLoading 메소드 구현부를 수정하겠습니다.

shouldOverrideUrlLoading 메소드는 웹뷰의 URL 변경 시 발생되며, URL에 따라 호스트 애플리케이션이 제어할 수 있는 기회를 주는 이벤트입니다.(안드로이드 기술문서: shouldOverrideUrlLoading)

리턴 값이 True이면 호스트가 제어하기 때문에 화면을 이동하지 않고, False이면 화면을 이동합니다.

다음 코드를 참고해 shouldOverrideUrlLoading 메소드 구현부를 수정합니다.

function TAndroidWebBrowserService.TWebBrowserListener.shouldOverrideUrlLoading(
  P1: JWebView; P2: JString): Boolean;
var
  Url: string;
  Intent: JIntent;
begin
  Url := JStringToString(P2);

  FWBService.ShouldStartLoading(Url);
  Result := False;

  // 리턴값이 True이면 페이지를 랜더링하지 않음
    // http 또는 https 인 경우 False(랜더링 함)
    // 그 외 True(렌더링 안함)  로딩을 중지 함(P1.stopLoading)
  if (Url.StartsWith('http://') or Url.StartsWith('https://')) then
    Result := False
  else
  begin
    // tel:, sms:, mailto:, geo: 등은 해당 Activity 실행
    Intent := TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_VIEW,
      TJnet_Uri.JavaClass.parse(P2));
    try
      SharedActivity.startActivity(Intent);
      Result := True;
    except
    end;
  end;

  // 렌더링 안하므로 로딩 중지
  if Result then
    P1.stopLoading;
end;

3, 테스트

위와 같이 수정하고 빌드 후 전화번호 링크를 클릭하면 아래와 같이 관련된 기능이 동작합니다.


참고 글



험프리.김현수 파이어몽키

개발환경 설정 - 안드로이드 개발환경

2015.06.09 11:32

안드로이드 개발환경 설정

RAD Studio를 기본 설정으로 설치하면 안드로이드 앱을 개발할 수 있는 안드로이드 관련 소프트웨어(SDK, NDK, JDK 등)가 기본 설치됩니다.


1, 안드로이드 도구 설치확인하기

Android Tools 설치 확인

윈도우 시작 메뉴(시작버튼 > 앱 보기 > Embarcadero RAD Studio [버전])에서 Android Tools 항목을 확인합니다.


Android Tools 설치(설치하지 않은 경우)

만약, Android Tools 항목이 보이지 않는다면 Modify, Repair, Unistall 메뉴를 선택하고, 다음 화면에서 Android SDK와 Android NDK를 다시 설치하기 바랍니다.


2, 실제 디바이스 연결하기

개발PC와 디바이스 연결

개발 PC와 안드로이드 디바이스를 USB 케이블로 연결합니다. 개발 PC에는 안드로이드 디바이스를 인식하기 위한 USB 드라이버가 설치되어 있어야 합니다.
USB 드라이버 설치가 잘 되어 있다면 아래와 같이 탐색기에 디바이스 목록이 장치 관리자에 Android Phone 목록이 표시됩니다.


USB 드라이버 설치(USB 드라이버가 설치되어 있지 않은 경우)

만약, USB 드라이버가 설치되지 않은 경우 각 디바이스 제조사의 웹사이트를 통해 USB 드라이버 설치파일을 다운로드 받아 설치해야 합니다.


USB 디버깅 설정

개발 PC와 안드로이드 디바이스가 연결되어 있어도, 디바이스에서 USB 디버깅을 사용하도록 설정해야 앱을 배포하고 디버깅할  수 있습니다.

안드로이드 디바이스의 환경설정 > 개발자 옵션 > USB 디버깅 항목을 체크(선택) 합니다.


만약, 개발자 옵션 메뉴가 표시되지 않는다면 "환경설정 > 디바이스 정보 > 빌드 번호" 메뉴를 7회 터치하면 개발자 옵션 메뉴가 표시됩니다.(인드로이드 4.2 이상에서는 개발자 옵션이 기본적으로 감춰져 있습니다.)


USB 디버깅 허용

개발 PC와 장치 연결 시 "USB 디버깅을 허용할까요?" 메시지가 표시되면 허용해야합니다.

3, RAD Studio와 연결 확인하기

앞의 과정을 진행하면 개발PC와 디바이스 연결이 완료되었습니다. 이제 RAD Studio를 열고, File > New > Multi-Deivce Application 메뉴로 새로운 프로젝트를 만듭니다.

우측의 프로젝트 매니저에서 Target Platforms > Android > Target 트리메뉴를 확장하면 아래 그림과 같이 개발 PC와 연결된 디바이스 목록이 표시됩니다.


관련 글



험프리.김현수 파이어몽키

앱의 상태바(StatusBar) 제어하기(색상변경, 감추기, 투명하게)

2015.06.04 11:08

iOS 상태바 제어하기

iOS 앱의 상태바 색상을 변경하고, 감추고, 투명하게 할 수 있습니다.

1, 상태바 색상 변경하기

상태바 색상은 폼의 색상(Fill.Color)을 그대로 반영합니다.

  • 폼의 Fill.Color 변경
  • 폼의 Fill.Kind = Solid

2, 전체화면 전환(상태바 감추기)

폼의 테두리를 표시하지 않으면 상태바도 표시되지 않습니다.

  • 폼의 BorderStyle = None
참고로 코드로 BorderStyle을 변경 시 메모리 참조 에러가 간혹 발생합니다. 만약, 코드로 전체화면으로 전환이 필요한 경우 아래글의 Under the hood – FullScreen & TPlatformServices 섹션을 참고하시기 바랍니다.

3, 상태바 투명하게 하기

상태바를 투명하게 하기 위해서는 파이어몽키 소스코드(FMX.Platform.iOS.pas)를 수정해야합니다.

다음 글을 세번째 항목(三、透明状态栏(能见底图))을 참고하시기 바랍니다.


중국인 개발자(龟山阿卍)의 블로그입니다. 


Tip> 파이어몽키  소스코드를 수정하기

1, RAD Studio 소스코드 경로에서 파이어몽키 소스코드를 복사합니다.(설치경로로\source)

2, 프로젝트 경로에 파이어몽키 소스코드를 붙여넣기 합니다.

3, 프로젝트 매니저에서 위의 파이어몽키 소스코드를 추가합니다.

4, 프로젝트의 파이어몽키 소스코드를 수정 후 컴파일 시 프로젝트의 소스코드를 참고해 빌드됩니다.

안드로이드 상태바 제어하기

안드로이드는 XE7부터 몰입(Immersive)모드를 지원합니다. 몰입모드는 Android KitKat(4.4) 이상에서 동작합니다.

전체화면(상태바 감추기)

폼의 FullScreen 속성을 이용해 전체화면으로 전환/해제 할 수 있습니다.

  • 폼의 FullScreen = True
FullScreen 진입해 전체화면이 되면 상/하단을 쓸어 시스템 메뉴를 표시할 수 있습니다.

참고 글




험프리.김현수 파이어몽키

[XE8] 스피드버튼의 이미지를 크게 표시할 수 있습니다.

2015.06.04 10:32

파이어몽키에 이미지리스트가 추가(XE8~)되어 스피드버튼에 이미지 표시가 아주 편리해졌습니다.


하지만, 이미지의 기본크기가 너무 작다고 느끼신 경우 다음 글을 통해 스타일을 수정해 큼지막한 이미지를 표시해 보세요.



중국인 개발자(龟山阿卍)의 블로그입니다. 


험프리.김현수 파이어몽키 Imagelist, Speedbutton, XE8

[JVESoft] 광고와 결재 컴포넌트(AdMob, Paypal 등)

2015.05.18 09:29

JVESoft는 각종 광고 플랫폼과 결재 플랫폼에 연결할 수 있는 컴포넌트를 제작해 판매하고 있습니다.


RAD Studio에서는 TBannerAd 컴포넌트로 광고기능TInAppPuchase 컴포넌트로 결재 기능을 지원하고 있습니다.

각 컴포넌트의 기능과 편의성을 검토해 보시고 원하는 컴포넌트를 선택해서 광고와 결재기능을 여러분의 앱에 추가하시기 바랍니다.




험프리.김현수 파이어몽키

[iOS] 배포파일 아이클라우드 백업 옵션설정하기

2015.05.18 09:15

iOS앱 배포 시 함께 배포하는 파일에 대해 아이클라우드 백업 설정하는 코드입니다.

대상은 "StartUp\Documents"에 배포하는 파일이며, 아이클라우드 백업 설정하지 않으면 앱스토어 배포 시 리젝처리 될수 있다고 하니 아래 코드를 참고해서 아이클라우드 백업 설정처리하시기 바랍니다.

uses iOSapi.Foundation;
 
function SetBackupFlag( AFileName: string; ABackup: boolean): boolean;
var
  URL : NSUrl;
  Err : PPointer;
begin
  URL := TNSURL.Wrap(TNSURL.OCClass.fileURLWithPath(NSStr(AFileName)));
  Result := URL.setResourceValue(TNSNumber.OCClass.numberWithBool(not ABackup),
              NSStr ('NSURLIsExcludedFromBackupKey'), err) and (err = nil);
end;



참고 글



험프리.김현수 파이어몽키

파이어몽키에서 외부 라이브러리 연동하기(jar, so, a)

2015.04.14 09:58

안드로이드 라이브러리 연동하기

JAR 라이브러리 연동하기

안드로이드 커스텀 라이브러리 연동하는 방법은 엠바카데로 기술문서를 통해 확인할 수 있습니다.

위 페이지에서 제공하는 링크를 따라가며 상세 내용을 익히시기 바랍니다.

libXXX.so 라이브러리 연동하기

엠바카데로 팀 제팬 블로그(http://blogs.embarcadero.com/teamj/)의 글을 링크합니다.

아래 글에서 이클립스로 so 라이브러리를 만들고 델파이에서 dlopen 함수를 이용해 로드해 오는 내용을 참고할 수 있습니다.

(델파이 XE5에서 작성된 글이지만 사용방식은 최신버전에서도 동일합니다.)

iOS 라이브러리 연동하기

정적 라이브러리(*.a) 연동하기

iOS에서는 동적 라이브러리(*.dylib)를 사용하면 앱을 앱스토어에 등록 시 거절 사유가 되기 때문에 정적 라이브러리(*.a)만 사용 가능합니다.


iOS에서 정적 라이브러리 연동하는 방법은 최원식옹(http://blog.naver.com/simonsayz)님의 블로그 글을 링크합니다.

(델파이 XE4에서 작성된 글이지만 사용방식은 최신버전에서도 동일합니다.)

    델파이(파이어몽키)에서 Objective-C 라이브러리를 사용하기 위한 로더를 만드는 내용을 샘플로 소개합니다.

    델파이(파이어몽키)에서 Objective-C 라이브러리와 컨트롤 연동하는 방법을 샘플로 소개합니다.

      실무에서 사용하는 라이브러리(ZBar)를 델파이에서 연동하는 내용을 샘플을 통해 소개합니다.



      험프리.김현수 파이어몽키

      1. Blog Icon
        윤수아

        이제 막 XE7으로 안드로이드 개발을 시작하려고 하는데
        안드로이드 SDK, JAVA, 브릿지파일, 자바임포트, Java2OP
        이런 개념들이 뒤죽박죽 섞여서 개념 잡기가 힘이 드네요..

        그러다가 아래와 같은 컴포넌트 판매 페이지를 보게 되었는데...
        저 둘의 구분이 뭔지도 모르겠고.. JAVA2OP.exe 가 저 역활 아닌가.. 하면서 다시 혼란스럽네요..
        http://www.winsoft.sk/jbridge.htm
        http://www.winsoft.sk/jimport.htm

      2. Blog Icon
        김윤태

        위 주제에 반대로 하고 싶은데요. 파이어몽키에서 정적 또는 동적 라이브러리를 만들어서 외부 프로그램 (XCode, JAVA)에 연동하는 것은 어떻게 되는지 궁금합니다. 인터넷에서 찾아 봤는데 자료가 없더군요. 꼭 답변 부탁드립니다. ^^

      3. 델파이에서 아직 파이어몽키 라이브러리 개발을 지원하지 않습니다.

      [FMX] ListBox 전체체크 기능 구현하기

      2015.03.04 13:28

      리스트박스(TListBox)에 체크박스를 두고 버튼 클릭 시 전체체크하는 기능에 대한 문의가 있어 정리해 봤습니다.


      이 글에서는 아래 내용을 학습할 수 있습니다.

      • 리스트박스를 동적으로 생성하는 방법
      • 리스트박스 아이템에 체크박스를 표시하는 속성

      화면 구성

      컴포넌트 속성 설정

      주요 속성을 설정합니다.(Object Inspector에서 속성을 설정해도 됩니다.)

      procedure TForm2.FormCreate(Sender: TObject);
      begin
        Button1.StaysPressed := True; //버튼 눌림(IsPressed) 상태 유지
      
        ListBox1.DefaultItemStyles.ItemStyle := 'listboxitemrightdetail'; // 텍스트와 오른쪽에 상세정보가 표시되도록 기본속성 지정
        ListBox1.ShowCheckboxes := True; // 리스트박스 항목에 체크박스가 표시되도록 설정
      end;

      새로고침

      procedure TForm2.Button2Click(Sender: TObject);
      var
        I: Integer;
        Item: TListBoxItem;
      begin
        ListBox1.Items.Clear;
        ListBox1.BeginUpdate;
        try
          for I := 0 to 10 do
          begin
            Item := TListBoxItem.Create(ListBox1);
            Item.Parent := ListBox1;
            Item.Text := 'Item ' + I.ToString;
            Item.ItemData.Detail := 'Description';
          end;
        finally
          ListBox1.EndUpdate;
        end;
      end;

      전체선택

      procedure TForm2.Button1Click(Sender: TObject);
      var
        I, Idx: Integer;
      begin
        Idx := ListBox1.ItemIndex;
        ListBox1.BeginUpdate;
        try
          for I := 0 to ListBox1.Items.Count - 1 do
          begin
            ListBox1.ListItems[I].IsChecked := Button1.IsPressed;
          end;
        finally
          ListBox1.EndUpdate;
          ListBox1.ItemIndex := Idx;
        end;
      end;

      소스코드

      ProjectLBCheck.zip


      Tips>

      커스텀 스타일로 TListBoxItem 스타일을 만드는 경우 TCheckBox의 StyleName을 "check"로 지정하기 바랍니다.


      험프리.김현수 파이어몽키

      [XE7] iOS 시뮬레이터 배포 시 'Please specify exact device preset UUID.' 오류 대응

      2015.02.24 17:48

      iOS 시뮬레이터 실행 시 아래와 같은 오류 발생 시 대처사항입니다.



      결과적으로 원인은 RAD Studio XE7에서 iOS Simulator 8.1을 지원하지 않아서 입니다.

      (iOS Device에서는 8.1을 지원합니다.)

      Hotfix iPhone Simulator 7.1 지원

      RAD Studio XE7 출시 이후 나온 7.1에 대한 패치가 필요합니다. 아래 글의 2번째 Hotfix 설치하기 바랍니다.

      iPhone Simulator 7.1 설치 및 설정

      1. 맥에서 XCode(6.1) 실행 > XCode > Preferences... > Download 화면에서 iOS 7.1 Simulator 다운로드
      2. 델파이 Project Manager의 iOS Simulator에서 Property > SDK에서 iPhoneSimulator 7.1 선택(없으면 Add new... 선택)
      3. (PAServer가 연결된 상태에서)자동으로 iPhoneSimulator 7.1 관련 라이브러리 생성(만약 동작하지 않으면 PAServer 재시작)
      4. iOS Simulator로 다시 배포 및 테스트


      험프리.김현수 파이어몽키

      파이어몽키 용 그리드 컴포넌트 안내 - FirePower

      2015.02.23 13:24

      태블릿용 앱에서는 한 화면에 많은 정보(여러 컬럼과 레코드)를 표시해야하는 경우가 있는데요.

      이때, 기본 컴포넌트를 이용한다면 리스트뷰에 컬럼을 추가해 구현할 수 있지만, 아무래도 전통적인 그리드 컴포넌트가 필요할 수 있습니다.


      오늘은 이미 인포파워 그리드로 유명한 Woll2Woll사의 파이어몽키용 그리드 컴포넌트인 FirePower 컴포넌트를 소개합니다.


      제가 트라이얼로 돌려본 주요 기능은 아래와 같습니다.

      • 기본 그리드 컴포넌트(TGrid, TStringGrid) 대비 스크롤 속도가 상당히 빠릅니다.(이 부분은 기본 컴포넌트의 성능도 꾸준히 개선되고 있습니다.)
      • 다양한 스타일(배경색상, 글자색, 글자 스타일 등)을 적용할 수 있습니다.
      • 셀에 버튼, 이미지 등 속성을 지정할 수 있습니다.
      • 컬럼별 색상을 지정할 수 있습니다.
      • 컬럼별 정렬이 가능합니다.

      그 외의 기능들은 제품소개 또는 트라이얼로 테스트 할 수 있습니다.

      엠바카데로 테크니컬 파트너 페이지에도 등록되어 있습니다.(최신버전을 지원하는 다른 컴포넌트도 알아보시기 바랍니다.)


      참고 링크



      관련 글


      험프리.김현수 파이어몽키 XE7, 그리드

      [XE7] 안드로이드 블루투스 활성화 조회와 설정하기

      2015.02.12 09:39

      일본인 개발자인 山本隆(야마모토 타카시)의 글을 참고해 안드로이드에서 블루투스 활성여부 조회와 설정하는 내용을 공유합니다.

      블루투스 활성여부 조회

      uses
        Androidapi.JNI.Bluetooth;
      
      procedure TForm2.Button1Click(Sender: TObject);
      var
        Adapter: JBluetoothAdapter;
      begin
        Adapter := TJBluetoothAdapter.JavaClass.getDefaultAdapter;
        if Adapter.isEnabled then
          ShowMessage('Bluetooth가 활성화 되어있습니다.')
        else
          ShowMessage('Bluetooth가 활성화 되지 않았습니다.');
      end;

      블루투스 활성화 설정

      uses
        Androidapi.JNI.Bluetooth;
      
      procedure TForm2.Button2Click(Sender: TObject);
      var
        Adapter: JBluetoothAdapter;
      begin
        Adapter := TJBluetoothAdapter.JavaClass.getDefaultAdapter;
        if Adapter.enable then
          ShowMessage('Bluetooth를 활성화합니다.')
        else
          ShowMessage('사용할 수 없습니다.');
      end;
      
      procedure TForm1.Button3Click(Sender: TObject);
      var
        Adapter: JBluetoothAdapter;
      begin
        Adapter := TJBluetoothAdapter.JavaClass.getDefaultAdapter;
        if Adapter.disable then
          ShowMessage('Bluetooth를 비활성화합니다.')
        else
          ShowMessage('사용할 수 없습니다.');
      end;

      블루투스 권한이 있는지 확인

      uses
        Androidapi.Helpers,
        Androidapi.JNI.GraphicsContentViewText;
      
      procedure TForm2.Button3Click(Sender: TObject);
        function HasPermission(const Permission: string): Boolean;
        begin
          Result := SharedActivityContext.checkCallingOrSelfPermission(StringToJString(Permission)) = TJPackageManager.JavaClass.PERMISSION_GRANTED
        end;
      
      begin
        if HasPermission('android.permission.BLUETOOTH') then
          ShowMessage('Bluetooth 통신 권한이 있습니다.')
        else
          ShowMessage('Bluetooth 통신 권한이 없습니다');
      
        if HasPermission('android.permission.BLUETOOTH_ADMIN') then
          ShowMessage('Bluetooth 설정 수정 권한이 있습니다.')
        else
          ShowMessage('Bluetooth 설정 수정 권한이 없습니다.');
      end;

      • 블루투스 관련 권한(Project Options > Uses Permissions)이 필요합니다.
        • Bluetooth
        • Bluetooth admin

      샘플프로그램 다운로드

      ProjectSetBluetooth.zip

      참고 글


      험프리.김현수 파이어몽키 XE7, 블루투스, 안드로이드

      [XE7] 안드로이드 WiFi 상태조회와 설정하기

      2015.02.11 10:44

      일본인 개발자인 山本隆(야마모토 타카시)의 글을 참고해 안드로이드 앱에서 Wifi 상태를 읽고 키고끄는 기능을 구현해 보고 공유합니다.

      ❑ 안드로이드 WifiManager 델파이 브릿지 파일 생성

      Java2OP 툴을 이용해 WifiManager 델파이 브릿지 파일을 만듭니다.

      Java2OP.exe -classes android.net.wifi.WifiManager -unit Androidapi.JNI.WifiManager


      위 명령어를 실행하면 아래와 같이 Androidapi.JNI.WifiManager.pas 유닛파일이 생성됩니다. 



      해당 유닛파일을 프로젝트 경로 또는 본인이 관리하는 라이브러리 경로에 복사합니다.(저는 D:\Projects\common\rtl\XE7\android 경로에 복사 후 Option > Library Path에 경로를 등록했습니다.)


      만약, 아래와 같은 "Missing JDK. Please install JDK 1.7+ or make sure that it is in your PATH" 메시지가 표시된다면


       커맨드라인에서 아래의 명령어를 이용해 환경변수에 추가 후 다시 시도하기 바랍니다.(도움말)
      SET PATH=%PATH%;C:\Program Files\Java\jdk1.7.0_25\bin


      ❑ Androidapi.JNI.WifiManager 유닛을 이용해 WiFI 제어기능 구현

      새로운 프로젝트를 생성하고, 위에서 생성한 Androidapi.JNI.WifiManager를 추가합니다.

      uses 절에 구현에 필요한 유닛을 추가합니다.

      uses
        Androidapi.Helpers,
        Androidapi.JNI.JavaTypes,
        Androidapi.JNI.GraphicsContentViewText,
        Androidapi.JNIBridge,
        Androidapi.JNI.WifiManager;

      WiFi 설정(키고 끄기)하기

      procedure SetWifiEnabled(AEnable: Boolean);
      var
        Obj: JObject;
        WifiManager: JWifiManager;
      begin
        Obj := SharedActivityContext.getSystemService(TJContext.JavaClass.WIFI_SERVICE);
        if Obj = nil then
          Exit;
      
        WifiManager := TJWifiManager.Wrap((Obj as ILocalObject).GetObjectID);
        WifiManager.setWifiEnabled(AEnable);
      end;

      WiFi 상태 조회

      function IsWifiEnabled: Boolean;
      var
        Obj: JObject;
        WifiManager: JWifiManager;
      begin
        Obj := SharedActivityContext.getSystemService(TJContext.JavaClass.WIFI_SERVICE);
        if Obj = nil then
          Exit;
      
        WifiManager := TJWifiManager.Wrap((Obj as ILocalObject).GetObjectID);
        Result := WifiManager.isWifiEnabled;
      end;

      ❑ 사용자 권한 설정

      위 Wifi 제어와 상태조회 기능을 사용할 경우 사용자 권한을 추가해야 합니다.

      Project Option > Uses permissions

      • Access wifi state : Wifi 상태 조회

      • Change wifi state : Wifi 상태 설정

      ❑ 최종 화면과 소스코드


      참고 글



      험프리.김현수 파이어몽키

      태블릿용 멀티컬럼 리스트뷰 ItemAppearace 만들기

      2015.02.09 11:28

      모바일(폰과 패드)용 목록을 만드는 컴포넌트는 대표적으로 ListView와 ListBox가 있습니다.

      두 목록 컴포넌트의 차이점은 이름으로 알수 있듯이 목적에 차이가 있습니다.


      ListView는 

      View 즉 보여주는 것을 목적으로 하기 때문에 목록을 빠르게 이동할 수 있지만 목록아이템을 꾸미는데 제한적입니다.

      반면, 

      ListBox는 

      Box 즉 목록 아이템에 다른 아이템을 담아 자유롭게 목록을 구성할 수 있는 컴포넌트입니다. 목록을 원하는데로 꾸밀 수 있지만 많은 컴포넌트를 담는다면 스크롤이 상대적으로 느려질 수 있습니다.


      두 목록 컴포넌트의 목적을 잘 이해하고 사용하시기 바랍니다.


      ListView는 목록 아이템을 꾸미는데 제한적이라고 했는데요. 그 이유는 TListViewItem은 (TFMXObject를 상속받지 않았기 때문에)다른 컴포넌트를 올릴 수 없도록 설계되었습니다. 아이템 외관을 꾸미기 위해서는 ListView의 ItemAppearance 속성을 이용할 수 있습니다. 이 속성은 기본으로 7개의 항목만 제공됩니다.

      기본 제공되는 ItemAppearance는 모바일 폰(작은화면)을 기준으로 제공합니다. 태블릿용 앱에서는 화면이 다소 허전해 질 수 있습니다.

      이렇게 기본 제공되는 외관을 변경(항목 추가, 위치 이동)하기 위해서는 ItemAppearance 패키지 프로젝트를 직접 만들어 설치(Install) 후 사용할 수 있습니다.

      ItemAppearance 패키지 제작

      기본 샘플 참고

      델파이 기본 샘플에서 ListView 샘플(Samples\Object Pascal\Mobile Samples\User Interface\ListView)을 참고할 수 있습니다.

      ListView 샘플에는 상세정보를 여러건 보여주는 MultiDetailItem, 별점을 표시하는 RatingItem 등의 목록을 구성할 수 있는 패키지 프로젝트(*.dpk)가 있고, 그 패키지를 사용하는 샘플 프로젝트가 있습니다.

      먼저 패키지 프로젝트를 열고 프로젝트 매니저에서 설치(오른쪽 마우스 > Install) 후 샘플 프로젝트를 열어 확인하기 바랍니다.

      패키지 프로젝트를 열면 ItemAppearance 속성에 새로운 항목이 추가된 것을 볼 수 있습니다.

      MultiDetailHorzItem

      제가 직접 4개의 열을 갖는 ItemAppearance를 만들어 테스트 해 봤습니다. 기본 제공되는 MultiDetailItem을 수정했습니다.

      확실히 ListBox, Grid, StringGrid에 비해 목록을 스크롤하는 속도가 빠릅니다.


      unit MultiDetailHorzAppearanceU;
      
      interface
      
      uses FMX.ListView, FMX.ListView.Types, System.Classes, System.SysUtils,
      FMX.Types, System.UITypes, FMX.MobilePreview;
      
      type
      
        TMultiDetailHorzAppearanceNames = class
        public const
          ListItem = 'MultiDetailHorzItem';
          ListItemCheck = ListItem + 'ShowCheck';
          ListItemDelete = ListItem + 'Delete';
          Detail1 = 'det1';  // Name of MultiDetail object/data
          Detail2 = 'det2';
          Detail3 = 'det3';
        end;
      
      implementation
      
      uses System.Math, System.Rtti;
      
      type
      
        TMultiDetailHorzItemAppearance = class(TPresetItemObjects)
        public const
          cTextMarginAccessory = 8;
          cDefaultHeight = 40;
        private
          FMultiDetail1: TTextObjectAppearance;
          FMultiDetail2: TTextObjectAppearance;
          FMultiDetail3: TTextObjectAppearance;
          procedure SetMultiDetail1(const Value: TTextObjectAppearance);
          procedure SetMultiDetail2(const Value: TTextObjectAppearance);
          procedure SetMultiDetail3(const Value: TTextObjectAppearance);
        protected
          function DefaultHeight: Integer; override;
          procedure UpdateSizes; override;
          function GetGroupClass: TPresetItemObjects.TGroupClass; override;
          procedure SetObjectData(const AListViewItem: TListViewItem; const AIndex: string; const AValue: TValue; var AHandled: Boolean); override;
        public
          constructor Create; override;
          destructor Destroy; override;
        published
          property MultiDetail1: TTextObjectAppearance read FMultiDetail1 write SetMultiDetail1;
          property MultiDetail2: TTextObjectAppearance read FMultiDetail2 write SetMultiDetail2;
          property MultiDetail3: TTextObjectAppearance read FMultiDetail3 write SetMultiDetail3;
          property Accessory;
        end;
      
        TMultiDetailHorzDeleteAppearance = class(TMultiDetailHorzItemAppearance)
        private const
          cDefaultGlyph = TGlyphButtonType.Delete;
        public
          constructor Create; override;
        published
          property GlyphButton;
        end;
      
        TMultiDetailShowCheckAppearance = class(TMultiDetailHorzItemAppearance)
        private const
          cDefaultGlyph = TGlyphButtonType.Checkbox;
        public
          constructor Create; override;
        published
          property GlyphButton;
        end;
      
      const
        cMultiDetail1Member = 'Detail1';
        cMultiDetail2Member = 'Detail2';
        cMultiDetail3Member = 'Detail3';
      
      constructor TMultiDetailHorzItemAppearance.Create;
      begin
        inherited;
        Accessory.DefaultValues.AccessoryType := TAccessoryType.More;
        Accessory.DefaultValues.Visible := True;
        Accessory.RestoreDefaults;
        Text.DefaultValues.VertAlign := TListItemAlign.Trailing;
        Text.DefaultValues.TextVertAlign := TTextAlign.Center;
        Text.DefaultValues.Visible := True;
        Text.RestoreDefaults;
      
        FMultiDetail1 := TTextObjectAppearance.Create;
        FMultiDetail1.Name := TMultiDetailHorzAppearanceNames.Detail1;
        FMultiDetail1.DefaultValues.Assign(Text.DefaultValues);  // Start with same defaults as Text object
        FMultiDetail1.DefaultValues.IsDetailText := True; // Use detail font
        FMultiDetail1.VertAlign := TListItemAlign.Leading;
        FMultiDetail1.Align := TListItemAlign.Trailing;
        FMultiDetail1.TextVertAlign := TTextAlign.Center;
        FMultiDetail1.RestoreDefaults;
        FMultiDetail1.OnChange := Self.ItemPropertyChange;
        FMultiDetail1.Owner := Self;
      
        FMultiDetail2 := TTextObjectAppearance.Create;
        FMultiDetail2.Name := TMultiDetailHorzAppearanceNames.Detail2;
        FMultiDetail2.DefaultValues.Assign(FMultiDetail1.DefaultValues);  // Start with same defaults as Text object
        FMultiDetail2.VertAlign := TListItemAlign.Leading;
        FMultiDetail2.Align := TListItemAlign.Trailing;
        FMultiDetail2.TextVertAlign := TTextAlign.Center;
        FMultiDetail2.RestoreDefaults;
        FMultiDetail2.OnChange := Self.ItemPropertyChange;
        FMultiDetail2.Owner := Self;
      
        FMultiDetail3 := TTextObjectAppearance.Create;
        FMultiDetail3.Name := TMultiDetailHorzAppearanceNames.Detail3;
        FMultiDetail3.DefaultValues.Assign(FMultiDetail2.DefaultValues);  // Start with same defaults as Text object
      //  FMultiDetail3.DefaultValues.Height := 20; // Move text down
        FMultiDetail3.VertAlign := TListItemAlign.Leading;
        FMultiDetail3.Align := TListItemAlign.Trailing;
        FMultiDetail3.TextVertAlign := TTextAlign.Center;
        FMultiDetail3.RestoreDefaults;
        FMultiDetail3.OnChange := Self.ItemPropertyChange;
        FMultiDetail3.Owner := Self;
      
        // Define livebindings members that make up MultiDetail
        FMultiDetail1.DataMembers :=
          TObjectAppearance.TDataMembers.Create(
            TObjectAppearance.TDataMember.Create(
              cMultiDetail1Member, // Displayed by LiveBindings
              Format('Data["%s"]', [TMultiDetailHorzAppearanceNames.Detail1])));   // Expression to access value from TListViewItem
        FMultiDetail2.DataMembers :=
          TObjectAppearance.TDataMembers.Create(
            TObjectAppearance.TDataMember.Create(
              cMultiDetail2Member, // Displayed by LiveBindings
              Format('Data["%s"]', [TMultiDetailHorzAppearanceNames.Detail2])));   // Expression to access value from TListViewItem
        FMultiDetail3.DataMembers :=
          TObjectAppearance.TDataMembers.Create(
            TObjectAppearance.TDataMember.Create(
              cMultiDetail3Member, // Displayed by LiveBindings
              Format('Data["%s"]', [TMultiDetailHorzAppearanceNames.Detail3])));   // Expression to access value from TListViewItem
      
        GlyphButton.DefaultValues.VertAlign := TListItemAlign.Center;
        GlyphButton.RestoreDefaults;
      
        // Define the appearance objects
        AddObject(Text, True);
        AddObject(MultiDetail1, True);
        AddObject(MultiDetail2, True);
        AddObject(MultiDetail3, True);
        AddObject(Image, True);
        AddObject(Accessory, True);
        AddObject(GlyphButton, IsItemEdit);  // GlyphButton is only visible when in edit mode
      end;
      
      constructor TMultiDetailHorzDeleteAppearance.Create;
      begin
        inherited;
        GlyphButton.DefaultValues.ButtonType := cDefaultGlyph;
        GlyphButton.DefaultValues.Visible := True;
        GlyphButton.RestoreDefaults;
      end;
      
      constructor TMultiDetailShowCheckAppearance.Create;
      begin
        inherited;
        GlyphButton.DefaultValues.ButtonType := cDefaultGlyph;
        GlyphButton.DefaultValues.Visible := True;
        GlyphButton.RestoreDefaults;
      end;
      
      function TMultiDetailHorzItemAppearance.DefaultHeight: Integer;
      begin
        Result := cDefaultHeight;
      end;
      
      destructor TMultiDetailHorzItemAppearance.Destroy;
      begin
        FMultiDetail1.Free;
        FMultiDetail2.Free;
        FMultiDetail3.Free;
        inherited;
      end;
      
      procedure TMultiDetailHorzItemAppearance.SetMultiDetail1(
        const Value: TTextObjectAppearance);
      begin
        FMultiDetail1.Assign(Value);
      end;
      
      procedure TMultiDetailHorzItemAppearance.SetMultiDetail2(
        const Value: TTextObjectAppearance);
      begin
        FMultiDetail2.Assign(Value);
      end;
      
      procedure TMultiDetailHorzItemAppearance.SetMultiDetail3(
        const Value: TTextObjectAppearance);
      begin
        FMultiDetail3.Assign(Value);
      end;
      
      procedure TMultiDetailHorzItemAppearance.SetObjectData(
        const AListViewItem: TListViewItem; const AIndex: string;
        const AValue: TValue; var AHandled: Boolean);
      begin
        inherited;
      
      end;
      
      function TMultiDetailHorzItemAppearance.GetGroupClass: TPresetItemObjects.TGroupClass;
      begin
        Result := TMultiDetailHorzItemAppearance;
      end;
      
      procedure TMultiDetailHorzItemAppearance.UpdateSizes;
      const
          // Total Rate = 1.0
          TextWidthRate = 0.4;
          Det1WidthRate = 0.2;
          Det2WidthRate = 0.2;
          Det3WidthRate = 0.2;
      
      var
        LOuterHeight: Single;
        LOuterWidth: Single;
        LInternalWidth: Single;
        LImagePlaceOffset: Single;
        LImageTextPlaceOffset: Single;
      begin
        BeginUpdate;
        try
          inherited;
      
          // Update the widths and positions of renderening objects within a TListViewItem
          LOuterHeight := Height - Owner.ItemSpaces.Top - Owner.ItemSpaces.Bottom;
          LOuterWidth := Owner.Width - Owner.ItemSpaces.Left - Owner.ItemSpaces.Right;
          Text.InternalPlaceOffset.X :=
            Image.ActualPlaceOffset.X +  Image.ActualWidth + LImageTextPlaceOffset;
      
          LInternalWidth := (LOuterWidth - Text.ActualPlaceOffset.X - Accessory.ActualWidth);
          if Accessory.ActualWidth > 0 then
            LInternalWidth := LInternalWidth - cTextMarginAccessory;
          Text.InternalWidth := Max(1, LInternalWidth * TextWidthRate);
      
          MultiDetail1.InternalWidth := LInternalWidth * Det1WidthRate;
          MultiDetail1.InternalPlaceOffset.X := Text.InternalPlaceOffset.X + Text.InternalWidth;
          MultiDetail2.InternalWidth := LInternalWidth * Det2WidthRate;
          MultiDetail2.InternalPlaceOffset.X := MultiDetail1.InternalPlaceOffset.X + MultiDetail1.InternalWidth;
          MultiDetail3.InternalWidth := LInternalWidth * Det3WidthRate;
          MultiDetail3.InternalPlaceOffset.X := MultiDetail2.InternalPlaceOffset.X + MultiDetail2.InternalWidth;
        finally
          EndUpdate;
        end;
      end;
      
      type
        TOption = TCustomListView.TRegisterAppearanceOption;
      const
        sThisUnit = 'MultiDetailHorzAppearanceU';     // Will be added to the uses list when appearance is used
      initialization
        // MultiDetailItem group
        TCustomListView.RegisterAppearance(
          TMultiDetailHorzItemAppearance, TMultiDetailHorzAppearanceNames.ListItem,
          [TOption.Item], sThisUnit);
        TCustomListView.RegisterAppearance(
          TMultiDetailHorzDeleteAppearance, TMultiDetailHorzAppearanceNames.ListItemDelete,
          [TOption.ItemEdit], sThisUnit);
        TCustomListView.RegisterAppearance(
          TMultiDetailShowCheckAppearance, TMultiDetailHorzAppearanceNames.ListItemCheck,
          [TOption.ItemEdit], sThisUnit);
      finalization
        TCustomListView.UnregisterAppearances(
          TArray.Create(
            TMultiDetailHorzItemAppearance, TMultiDetailHorzDeleteAppearance,
            TMultiDetailShowCheckAppearance));
      end.



      험프리.김현수 파이어몽키

      1. Blog Icon
        고재학

        소스 코드에 버그가 있는것 같네요.
        230 Text.InternalPlaceOffset.X :=
        231 Image.ActualPlaceOffset.X + Image.ActualWidth + LImageTextPlaceOffset;


      2. 어떤 증상이 있죠? 좀 더 자세히 알려주시면 감사하겠습니다.^^

      3. Blog Icon
        고재학

        윈도우에서 테스트할 경우에는 문제가 없었는데 모바일에서 컴파일하면 내용을 표시하는 과정에서 "Invalid floating point operation"가 나타납니다.
        위 코드를 제거하고 테스트해보니 정상작동 하였습니다.

      4. Blog Icon
        고재학

        질문에 답변도 해주시는지요?
        위의 예제를 응용하여 리스트뷰를 만들었는데... MultiDetail1에 대입되는 값에 따라 글자색을 변경하고 싶습니다.

        예를 들어
        MultiDetail1의 값이 1이면 MultiDetail1의 글자색은 빨강색
        MultiDetail1의 값이 2이면 MultiDetail1의 글자색은 파랑색

        모바일앱에서 MultiDetail1의 TextColor를 어떻게 동적으로 변경할 수 있을까요?

      5. C:\Users\Public\Documents\Embarcadero\Studio\17.0\Samples\Object Pascal\Multi-Device Samples\User Interface\ListView\ListViewMultiDetailAppearance

        위 ListView 샘플에 다음 코드로 진행해 봤습니다.(잘되네요^^)

        // ListView의 OnUpdateObjects 이벤트에 아래 코드 구현
        procedure TForm594.ListViewMultiDetailUpdateObjects(const Sender: TObject;
        const AItem: TListViewItem);
        var
        Textitem: TListItemText;
        begin
        Textitem := AItem.View.FindDrawable(TMultiDetailAppearanceNames.Detail1) as TListItemText;
        if Assigned(TextItem) then
        begin
        // M으로 시작하는 text 빨간색으로 표시
        if Textitem.Text.StartsWith('M') then
        begin
        TextItem.TextColor := $FFFF0000;
        end;
        end;
        end;

        위 디렉토리의 샘플을 참고하시면 다양한 기능을 구현할 수 있습니다.

      6. Blog Icon
        고재학

        와~~~ 멋지네요.
        XE8에서 작동하지않아 Seattle로 업그레이드하니 잘 됩니다.
        감사합니다.

      트루타입 폰트(FontAwesome) 파일을 이용해 아이콘 표시하기(안드로이드, iOS)

      2015.01.28 09:20

      트루타입 폰트에 그림을 등록해 아이콘으로 사용하는 방식은 최근 웹개발에서 많이 사용됩니다.

      델파이에서 어떻게 사용할까. 궁금했는데요. 마침 엠바카데로 커뮤니티 블로그에 관련 글이 올라와 소개합니다.



      간단한 개발팁을 보태면
      1. FontAwesome에서 다운로드 후 폰트파일 설치(fontawesome-webfont.ttf 찾아 더블클릭 > 설치)
      2. 윈도우 유틸리지 중 "문자표" 실행 후 FontAwesome 폰트 선택 후 사용할 아이콘 선택 > 클립보드 복사
      3. 델파이에서 Label 등을 추가 후 문자타입(Font.Family)를 "FontAwesome" 선택 후 값에 2번의 클립보드 내용 붙여넣기
      4. 안드로이드, iOS에 폰트파일 배포와 사용하도록 설정(원글 참고)

      관련 글


      험프리.김현수 파이어몽키