Cómo utilizar los componentes Multiview, TabBox, Listbox

A continuación os muestro un vídeo de la empresa IT Tools, en el que se explica con gran claridad y detalle el desarrollo de una app para visualizar carátulas de películas utilizando los componentes Multiview, TabBox y Listbox.
Espero que os sea útil.


Obtener la elevación en un punto con Google Maps en Android (I)

Google Maps elevation api nos proporciona un interfaz HTTP para obtener la altitud en un punto de la superficie terrestre, incluso en ubicaciones en el fondo marino, para ello lo único que tenemos que proporcionarle es la latitud y la longitud.

El formato de la URL que hay que utilizar es:

 https://maps.googleapis.com/maps/api/elevation/outputFormat?parameters

El formato de salida puede ser json o xml, en este ejemplo utilizaremos json.

Ejemplo: 
https://maps.googleapis.com/maps/api/elevation/json?locations=30.88144,-5.57505 

El navegador nos devolvería  lo siguiente:

 { 
 "results" :
 [
 { "elevation" : 992.9691772460938,
   "location" : { "lat" : 30.88144, "lng" : -5.57505 },
   "resolution" : 152.7032318115234
 }
 ],
 "status" : "OK" 
}


Sabiendo esto, lo único que necesitamos es tratar el resultado para obtener el dato "elevation", con Delphi Berlín tenemos una unit que nos facilita las cosas System.json, también necesitaré algunos de los componentes Indy: idhttp y tidSSLIOHandlerSocketOpenSSL (para obtener el código de páginas https).
También se podría utilizar http en google maps, pero pensando en futuro desarrollos de aplicaciones públicas y dado que estamos utilizando las coordenadas de un usuario, creo que dichas coordenadas se deberían proteger bajo la capa de SSL.

CONFIGURACIÓN:

idhttp1:tidhttp; 
ssl1:idSSLIOHandlerSocketOpenSSL;


 SSL1.ssloption.method:=sslvtlsv1_2;
 SSL1.ssloption.mode:=sslmunassigned;
 idhttp1. IDHTTP1.IOHANDLER:=SSL1 ;
 IdHTTP1.HandleRedirects := true;

PROPIEDADES de SSL1;


PROPIEDADES de  IDHTTP1:

Como para acceder a GoogleMaps se hace desde una página segura "https" tendremos que utilizar las librerías de INDY para SSL 

openssl-0.9.8s-i386-win32.zip y openssl-0.9.8s-x64_86-win64.zip 


Los archivos del zip openssl-0.9.8s-i386-win32.zip Se copiarán a la carpeta SYSTEMWOW64 los archivos del zip openssl-0.9.8s-x64_86-win64.zip irán la carpeta  SYSTEM32.



Ya sólo nos queda hacer GET para obtener el json de Google Maps, utilizando como parámetros la latitud y la longitud de un punto de la superficie terrestre.


    Texto := IdHTTP1.get('https://maps.googleapis.com/maps/api/elevation/json?locations=' + lat + ',' + long);

Con esto hemos almacenado el archivo en formato json dentro del string "Texto", después necesitamos extraer el dato "elevation", no sin antes comprobar si el "Get" no ha fallado comprobando si devuelve alguno de los siguientes códigos de error:

OK indica que la solicitud de API se realizó con éxito.
INVALID_REQUEST indica que el formato de la solicitud de API era incorrecto.
OVER_QUERY_LIMIT indica que el solicitante excedió su cuota.
REQUEST_DENIED indica que la API no completó la solicitud.
UNKNOWN_ERROR indica un error desconocido.



FUNCTION ObtenerAltitudDesdeGMaps(lat, long: STRING): STRING;
VAR
  joStatus, joElev: TJSONObject;
  Texto, texto1, estado: STRING;
begin

  IF Texto <> '' THEN
  BEGIN
    // quito los retornos de linea
    Texto := stringreplace(Texto, '''#$A''', '', [rfreplaceall]);
    joStatus := NIL;
    joElev := NIL;
    TRY

      joStatus := TJSONObject.Create;
      joElev := TJSONObject.Create;
      TRY
        joStatus := TJSONObject.ParseJSONValue(Texto) AS TJSONObject;
        joStatus.Parse(BytesOf(Texto), 0);
        estado := joStatus.GetValue('status').Value;

        //SI GOOGLE MAPS NO HA FALLADO ENTONCES...
        IF estado = 'OK' THEN
        BEGIN
          texto1 := joStatus.GetValue('results').ToString;
          texto1 := stringreplace(texto1, '[', '', [rfreplaceall]);
          texto1 := stringreplace(texto1, ']', '', [rfreplaceall]);
          joElev := TJSONObject.ParseJSONValue(texto1) AS TJSONObject;
          joElev.Parse(BytesOf(texto1), 0);
          //AQUI OBTENEMOS EL DATO BUSCADO
          result := joElev.GetValue('elevation').Value;
        END;
      EXCEPT
        ON E: Exception DO
        BEGIN
          MessageDlg('Error en parser.-' + E.Message, TMsgDlgType.mtError, [TMsgDlgBtn.mbOK], 0);
        END;
      END;

    FINALLY
      joStatus.free;
      joElev.free;
    END;
END;

RESUMEN:
Hemos visto
- Cómo obtener la altitud con Google Maps desde un navegador.
- Cómo obtener el código de una página https con indy y Delphi Berlín.
- Un ejemplo de cómo se parsea un archivo json con Delphi Berlín.

Ejecutar un fichero desde la memoria del PC

{ uExecFromMem

  Author: steve10120
  Description: Run an executable from another's memory.
  Credits: Tan Chew Keong: Dynamic Forking of Win32 EXE; Author of BTMemoryModule: PerformBaseRelocation().
  Reference: http://www.security.org.sg/code/loadexe.html
  Release Date: 26th August 2009
  Website: http://ic0de.org
  History: First try

  Additions by testest 15th July 2010:
    - Parameter support
    - Win7 x64 support
}

unit uExecFromMem;

interface

uses Windows;

function ExecuteFromMem(szFilePath, szParams: string; pFile: Pointer):DWORD;

implementation

function NtUnmapViewOfSection(ProcessHandle:DWORD; BaseAddress:Pointer):DWORD; stdcall; external 'ntdll';

type
  PImageBaseRelocation = ^TImageBaseRelocation;
  TImageBaseRelocation = packed record
     VirtualAddress: DWORD;
     SizeOfBlock: DWORD;
  end;

procedure PerformBaseRelocation(f_module: Pointer; INH:PImageNtHeaders; f_delta: Cardinal); stdcall;
var
  l_i: Cardinal;
  l_codebase: Pointer;
  l_relocation: PImageBaseRelocation;
  l_dest: Pointer;
  l_relInfo: ^Word;
  l_patchAddrHL: ^DWord;
  l_type, l_offset: integer;
begin
  l_codebase := f_module;
  if INH^.OptionalHeader.DataDirectory[5].Size > 0 then
  begin
    l_relocation := PImageBaseRelocation(Cardinal(l_codebase) + INH^.OptionalHeader.DataDirectory[5].VirtualAddress);
    while l_relocation.VirtualAddress > 0 do
    begin
      l_dest := Pointer((Cardinal(l_codebase) + l_relocation.VirtualAddress));
      l_relInfo := Pointer(Cardinal(l_relocation) + 8);
      for l_i := 0 to (trunc(((l_relocation.SizeOfBlock - 8) / 2)) - 1) do
      begin
        l_type := (l_relInfo^ shr 12);
        l_offset := l_relInfo^ and $FFF;
        if l_type = 3 then
        begin
          l_patchAddrHL := Pointer(Cardinal(l_dest) + Cardinal(l_offset));
          l_patchAddrHL^ := l_patchAddrHL^ + f_delta;
        end;
        inc(l_relInfo);
      end;
      l_relocation := Pointer(cardinal(l_relocation) + l_relocation.SizeOfBlock);
    end;
  end;
end;

function AlignImage(pImage:Pointer):Pointer;
var
  IDH:          PImageDosHeader;
  INH:          PImageNtHeaders;
  ISH:          PImageSectionHeader;
  i:            WORD;
begin
  IDH := pImage;
  INH := Pointer(Integer(pImage) + IDH^._lfanew);
  GetMem(Result, INH^.OptionalHeader.SizeOfImage);
  ZeroMemory(Result, INH^.OptionalHeader.SizeOfImage);
  CopyMemory(Result, pImage, INH^.OptionalHeader.SizeOfHeaders);
  for i := 0 to INH^.FileHeader.NumberOfSections - 1 do
  begin
    ISH := Pointer(Integer(pImage) + IDH^._lfanew + 248 + i * 40);
    CopyMemory(Pointer(DWORD(Result) + ISH^.VirtualAddress), Pointer(DWORD(pImage) + ISH^.PointerToRawData), ISH^.SizeOfRawData);
  end;
end;

function Get4ByteAlignedContext(var Base: PContext): PContext;
begin
  Base := VirtualAlloc(nil, SizeOf(TContext) + 4, MEM_COMMIT, PAGE_READWRITE);
  Result := Base;
  if Base <> nil then
    while ((DWORD(Result) mod 4) <> 0) do
      Result := Pointer(DWORD(Result) + 1);
end;

function ExecuteFromMem(szFilePath, szParams:string; pFile:Pointer):DWORD;
var
  PI:           TProcessInformation;
  SI:           TStartupInfo;
  CT:           PContext;
  CTBase:       PContext;
  IDH:          PImageDosHeader;
  INH:          PImageNtHeaders;
  dwImageBase:  DWORD;
  pModule:      Pointer;
  dwNull:       DWORD;
begin
  if szParams <> '' then szParams := '"'+szFilePath+'" '+szParams;

  Result := 0;
  IDH := pFile;
  if IDH^.e_magic = IMAGE_DOS_SIGNATURE then
  begin
    INH := Pointer(Integer(pFile) + IDH^._lfanew);
    if INH^.Signature = IMAGE_NT_SIGNATURE then
    begin
      FillChar(SI, SizeOf(TStartupInfo), #0);
      FillChar(PI, SizeOf(TProcessInformation), #0);
      SI.cb := SizeOf(TStartupInfo);
      if CreateProcess(PChar(szFilePath), PChar(szParams), nil, nil, FALSE, CREATE_SUSPENDED, nil, nil, SI, PI) then
      begin
        CT := Get4ByteAlignedContext(CTBase);
        if CT <> nil then
        begin
          CT.ContextFlags := CONTEXT_FULL;
          if GetThreadContext(PI.hThread, CT^) then
          begin
            ReadProcessMemory(PI.hProcess, Pointer(CT.Ebx + 8), @dwImageBase, 4, dwNull);
            if dwImageBase = INH^.OptionalHeader.ImageBase then
            begin
              if NtUnmapViewOfSection(PI.hProcess, Pointer(INH^.OptionalHeader.ImageBase)) = 0 then
                pModule := VirtualAllocEx(PI.hProcess, Pointer(INH^.OptionalHeader.ImageBase), INH^.OptionalHeader.SizeOfImage, MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE)
              else
                pModule := VirtualAllocEx(PI.hProcess, nil, INH^.OptionalHeader.SizeOfImage, MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE);
            end
            else
              pModule := VirtualAllocEx(PI.hProcess, Pointer(INH^.OptionalHeader.ImageBase), INH^.OptionalHeader.SizeOfImage, MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE);
            if pModule <> nil then
            begin
              pFile := AlignImage(pFile);
              if DWORD(pModule) <> INH^.OptionalHeader.ImageBase then
              begin
                PerformBaseRelocation(pFile, INH, (DWORD(pModule) - INH^.OptionalHeader.ImageBase));
                INH^.OptionalHeader.ImageBase := DWORD(pModule);
                CopyMemory(Pointer(Integer(pFile) + IDH^._lfanew), INH, 248);
              end;
              WriteProcessMemory(PI.hProcess, pModule, pFile, INH.OptionalHeader.SizeOfImage, dwNull);
              WriteProcessMemory(PI.hProcess, Pointer(CT.Ebx + 8), @pModule, 4, dwNull);
              CT.Eax := DWORD(pModule) + INH^.OptionalHeader.AddressOfEntryPoint;
              SetThreadContext(PI.hThread, CT^);
              ResumeThread(PI.hThread);
              Result := PI.hThread;

FreeMem(pFile);  // IMPORTANTE YA QUE EVITA UN MEMORY LEAK
            end;
          end;
          VirtualFree(CTBase, 0, MEM_RELEASE);
        end;
        if Result = 0 then
          TerminateProcess(PI.hProcess, 0);
      end;
    end;
  end;
end;

end.

Si queréis saber más sobre cómo programar con Delphi, aquí tenéis una buena ayuda

Delphi 10.1 Berlín Starter Edition de 392 a 0 Euros



Como parte de Delphi Boot Camp, Embarcadero ofrece la versión Delphi 10.1 Berlín Starter Edition por 0 Euros.
Delphi Boot Camp son unas jornadas profesionales ( del 5 al 9 de Septiembre ) realizadas por expertos muy conocidos en Delphi, como son:


David Intersimone 
David I.
Evangelist & Educator
Embarcadero Technologies

 Jim McKeeth 
Jim McKeeth
Evangelist & Engineer
Embarcadero Technologies

 Marco Cantu 
Marco Cantu
Product Manager
Embarcadero Technologies

 Eli M.
Eli M.
Developer & Entrepreneur
FMXExpress.com


 La agenda para estos días es la siguiente:


Agenda for the week  

Day 1: Introduction to Delphi: The IDE and Your First App

Day 2: Getting to Know the Delphi Language

Day 3: Building Effective User Interfaces with FireMonkey

Day 4: Game Development with Delphi

Day 5: Stepping up to Mobile and Database Development



El link para registrarse es:

[Register TODAY]


Una vez registrado puedes solicitar gratuitamente una copia de Delphi 10.1 Berlín Starter Edition desde aquí

Y por si esto no fuera poco puedes pedir también gratuitamente una copia del libro Marco Cantu's Object Pascal Handbook 




Una app para visualizar moléculas

"Molecule Hero" 3D Interactive Chemical Molecule Viewer

A continuación os presento una app llamada "Molecule Heroque permite ver moléculas en 3D , su autor es  Pawel Glowacki

Tiene entre otras opciones la posibilidad de mostrar el "modelo de alambre"  p.ej, en el caso de la molécula de la Cafeína sería el siguiente:


La app está disponible en Play Store (abajo tenéis el link) y  el código fuente está en GitHub bajo licencia MIT de código abierto por lo que se puede reutilizar el código indicando su procedencia.
Utiliza la renderización de esferas (para mostrar los átomos de la molécula) en tiempo real, además permite girar desde todos los ángulos el modelo y ver las conexiones entre los diferentes átomos.
Los datos los obtiene desde el "Protein Data Bank" PDB que es el formato estándar de archivo para el intercambio de datos de estructuras de moléculas.
Así que si queréis ver cómo es la molécula de la cafeína, del colesterol o del diamante, entre otras muchas, esta es vuestra app.

Pawel Glowacki forma parte del equipo de Embarcadero como "Technical Lead for Developer Tools", aquí tenéis su Twitter 


Link de la app Molecule Hero en Play Store


Código para programadores de apps Android

A continuación tienen una lista de trucos que seguro les serán de utilidad para aquellos que se dediquen a la programación de aplicaciones para móviles Android:


ÍNDICE:

 -OBTENER EL IMEI
 -CERRAR UNA APP
 -CÓMO UTILIZAR LA VIBRACIÓN 
-OBTENER EL ACTUAL VOLUMEN DE AUDIO
 -OBTENER UNA LISTA DE LAS LLAMADAS
 -OBTENER LA RELACIÓN DE SENSORES
 -LISTAR LOS SENSORES DE SISTEMA
 -OBTENER EL TIPO-MIME DE UN FICHERO


CÓDIGO FUENTE:


 - OBTENER EL IMEI

uses
  FMX.Helpers.Android, Androidapi.JNI.JavaTypes,
  Androidapi.JNI.GraphicsContentViewText,
  Androidapi.JNI.Telephony, Androidapi.JNIBridge;
 
var
  TelephonyObj: JObject;
  TelephonyManager: JTelephonyManager;
begin
  TelephonyObj := SharedActivityContext.getSystemService(TJContext.JavaClass.TELEPHONY_SERVICE);
  TelephonyManager := TJTelephonyManager.Wrap((TelephonyObj as ILocalObject).GetObjectID);
  Label1.Text := JStringToString(TelephonyManager.getDeviceId);


- CERRAR UNA APP

uses
  FMX.Platform.Android;
  
procedure TForm1.Button4Click(Sender: TObject);
begin
  MainActivity.finish;
end;


- CÓMO UTILIZAR LA VIBRACIÓN


uses
  FMX.Helpers.Android, Androidapi.JNI.JavaTypes, Androidapi.JNI.Os,
  Androidapi.JNI.App, Androidapi.JNIBridge, Androidapi.JNI.GraphicsContentViewText;
  
function IntArrayToJArray(const OrigArray: array of integer): TJavaArray;
var
  i: integer;
begin
  Result := TJavaArray.Create(Length(OrigArray));
  for i := Low(OrigArray) to High(OrigArray) do
    Result.Items[i] := OrigArray[i];
end;
  
procedure TForm1.Button1Click(Sender: TObject);
var
  VibratorObj: JObject;
  Vibrator: JVibrator;
begin
  VibratorObj := SharedActivity.getSystemService(TJActivity.JavaClass.VIBRATOR_SERVICE);
  Vibrator := TJVibrator.Wrap((VibratorObj as ILocalObject).GetObjectID);
   
    Vibrator.vibrate(StrToInt(ClearingEdit1.Text));
  //or
  //Vibrator.vibrate(IntArrayToJArray([1000, 5000, 3000, 1000]), -1);
  //or
  //Vibrator.cancel();
  //or
  //if Vibrator.hasVibrator() then 
  //... 
  //else 
  //...
end;


- OBTENER EL ACTUAL VOLUMEN DE AUDIO

uses
  Androidapi.JNI.JavaTypes,
  Androidapi.JNI.Media,
  Androidapi.Helpers,
  Androidapi.JNI.App,
  Androidapi.JNIBridge;
 
procedure TForm1.Button1Click(Sender: TObject);
var
  AudioObj: JObject;
  Audio: JAudioManager;
  CurrentVolume: Integer;
begin
  AudioObj = SharedActivity.getSystemService(TJActivity.JavaClass.AUDIO_SERVICE);
  Audio := TJAudioManager.Wrap((AudioObj as ILocalObject).GetObjectID);
  CurrentVolume = Audio.getStreamVolume(TJAudioManager.JavaClass.STREAM_MUSIC);
end;


- OBTENER UNA LISTA DE LAS LLAMADAS DEL MÓVIL


uses
  FMX.Helpers.Android, Androidapi.JNI.GraphicsContentViewText,
  Androidapi.JNI.Provider, Androidapi.JNI.JavaTypes, System.DateUtils;

procedure TForm1.Button1Click(Sender: TObject);
var
  cursor: JCursor;
  CallTypeInt: Integer;
  CallDuration, CallDate: Int64;
  CallName, CallTypeStr: String;
  ListBoxItem: TListBoxItem;
begin

  cursor := SharedActivity.getContentResolver.query(
   TJCallLog_Calls.JavaClass.CONTENT_URI,
    nil,
     nil,
      nil,
       nil);

  ListBox1.Clear;
  ListBox1.BeginUpdate;

  if (cursor.getCount > 0) then
  begin

    while (cursor.moveToNext) do
    begin

      CallName := JStringToString(cursor.getString(
        cursor.getColumnIndex(TJCallLog_Calls.JavaClass.CACHED_NAME)));

      if CallName = '' then begin
        CallName := JStringToString(cursor.getString(
        cursor.getColumnIndex(TJCallLog_Calls.JavaClass.NUMBER)));
      end;

      CallTypeInt := StrToInt(JStringToString(cursor.getString(
        cursor.getColumnIndex(TJCallLog_Calls.JavaClass.&TYPE))));

      case CallTypeInt of
        1: CallTypeStr := 'Bandeja de entrada';
        2: CallTypeStr := 'Bandeja de salida';
        3: CallTypeStr := 'Desconocido';
        else CallTypeStr := 'Cancelado';
      end;

      CallDuration := StrToInt64(JStringToString(cursor.getString(
        cursor.getColumnIndex(TJCallLog_Calls.JavaClass.DURATION))));

      CallDate := StrToInt64(JStringToString(cursor.getString(
        cursor.getColumnIndex(TJCallLog_Calls.JavaClass.DATE))));

      ListBoxItem := TListBoxItem.Create(ListBox1);

      ListBoxItem.ItemData.Text := CallTypeStr + ': ' + CallName;

      ListBoxItem.ItemData.Detail := DateTimeToStr(TTimeZone.Local.ToLocalTime(
        UnixToDateTime(CallDate div 1000))) + '   Duración: '
         + Format('%2.2d:%2.2d', [CallDuration div 60, CallDuration mod 60]);

      ListBox1.AddObject(ListBoxItem);

      Label1.Text := 'Всего: ' + IntToStr(ListBox1.Count);

    end;
  end;

  cursor.close;

  ListBox1.EndUpdate;

end;


- OBTENER LA RELACIÓN DE SENSORES


uses
  android.hardware.SensorManager, android.hardware.Sensor,
  Androidapi.JNI.JavaTypes, FMX.Helpers.android, Androidapi.JNIBridge,
  Androidapi.JNI.GraphicsContentViewText, Androidapi.Helpers;
 
procedure TForm1.Button1Click(Sender: TObject);
var
  SensorsManagerObj: JObject;
  SensorsManager: JSensorManager;
  Sensors: JList;
  Sensor: JSensor;
  i: Integer;
  StringType: String;
begin
  Memo1.Lines.Clear;
 
  SensorsManagerObj := SharedActivityContext.getSystemService
    (TJContext.JavaClass.SENSOR_SERVICE);
  SensorsManager := TJSensorManager.Wrap((SensorsManagerObj as ILocalObject)
    .GetObjectID);
 
  Sensors := SensorsManager.getSensorList(TJSensor.JavaClass.TYPE_ALL);
 
  Label1.Text := 'Found: ' + IntToStr(Sensors.size) + ' sensors';
 
  for i := 0 to Sensors.size - 1 do
  begin
    Sensor := TJSensor.Wrap((Sensors.get(i) as ILocalObject).GetObjectID);
 
    Memo1.Lines.Add('Name: ' + JStringToString(Sensor.getName));
 
    case Sensor.getType of
      TJSensorTYPE_ACCELEROMETER:
        StringType := 'Acelerometro';
      TJSensorTYPE_GRAVITY:
        StringType := 'Gravedad';
      TJSensorTYPE_GYROSCOPE:
        StringType := 'Giroscopio';
      TJSensorTYPE_LIGHT:
        StringType := 'Iluminación';
      TJSensorTYPE_LINEAR_ACCELERATION:
        StringType := 'Aceleración';
      TJSensorTYPE_MAGNETIC_FIELD:
        StringType := 'Campo magnético';
      TJSensorTYPE_ORIENTATION:
        StringType := 'Orientación';
      TJSensorTYPE_PRESSURE:
        StringType := 'Presión';
      TJSensorTYPE_PROXIMITY:
        StringType := 'Proximidad';
      TJSensorTYPE_ROTATION_VECTOR:
        StringType := 'Vector de rotación';
      TJSensorTYPE_TEMPERATURE:
        StringType := 'Temperatura';
    else
      StringType := 'Undefined';
    end;
 
    Memo1.Lines.Add('Type: ' + StringType);
    Memo1.Lines.Add('Vendor: ' + JStringToString(Sensor.getVendor));
    Memo1.Lines.Add('Version: ' + IntToStr(Sensor.getVersion));
    Memo1.Lines.Add('Resolution: ' + FloatToStr(Sensor.getResolution));
    Memo1.Lines.Add('Max Range: ' + FloatToStr(Sensor.getMaximumRange));
    Memo1.Lines.Add('Power: ' + FloatToStr(Sensor.getPower) + 'mA');
    Memo1.Lines.Add('MinDelay: ' + IntToStr(Sensor.getMinDelay));
    Memo1.Lines.Add('----------------');
 
  end;
end;


- LISTAR LOS SENSORES DE SISTEMA

uses
  System.Sensors, System.TypInfo;
 
procedure TForm1.Button2Click(Sender: TObject);
var
  SensorsManager: TSensorManager;
  i: Integer;
  Sensor: TCustomSensor;
begin
  SensorsManager := TSensorManager.Current;
  if SensorsManager.CanActivate then
  begin
    SensorsManager.Activate;
 
    Memo2.Lines.Clear;
    Label2.Text := 'Found: ' + IntToStr(SensorsManager.Count) + ' sensors';
 
    for i := 0 to SensorsManager.Count - 1 do
    begin
      Sensor := SensorsManager.Sensors[i];
      Memo2.Lines.Add('Name: ' + Sensor.Name);
      Memo2.Lines.Add('Category: ' + GetEnumName(TypeInfo(TSensorCategory),
        Ord(Sensor.Category)));
      Memo2.Lines.Add('Manufacturer: ' + Sensor.Manufacturer);
      Memo2.Lines.Add('Model: ' + Sensor.Model);
      Memo2.Lines.Add('State: ' + GetEnumName(TypeInfo(TSensorState),
        Ord(Sensor.State)));
      Memo2.Lines.Add('SerialNo: ' + Sensor.SerialNo);
      Memo2.Lines.Add('Description: ' + Sensor.Description);
      Memo2.Lines.Add('UniqueID: ' + Sensor.UniqueID);
      Memo2.Lines.Add('----------------');
    end;
  end;
  SensorsManager.Current.Deactivate;
end;


- OBTENER EL TIPO-MIME DE UN FICHERO

uses
  Androidapi.JNI.JavaTypes, Androidapi.JNI.Webkit;
  
var
  ExtFile: string;
  mime: JMimeTypeMap;
  ExtToMime: JString;
begin
  //Determinar la extensión de archivo y el tipo MIME
  ExtFile := AnsiLowerCase(StringReplace(TPath.GetExtension(path), '.', '',[]));
  mime := TJMimeTypeMap.JavaClass.getSingleton();
  ExtToMime := mime.getMimeTypeFromExtension(StringToJString(ExtFile));

El fin de los archivos ini - Utilizando SaveState con Firemonkey

El título del post no es una afirmación tajante, sino más bien una apreciación personal viendo la  funcionalidad que he encontrado en Delphi, que hasta ahora desconocía y que me ha parecido muy práctica y fácil de utilizar.

Todo gira respecto a la utilidad SaveState

Cuando estamos trabajando con Android si nuestra app está en segundo plano, Android puede decidir que hay cerrarla bajo ciertas condiciones (Low Memory, escasez de recursos del S.O., etc...), cuando esto sucede necesitaríamos guardar ciertos parámetros que permitan restaurar la app en la misma situación en la que estaba antes de cerrarse, es decir es necesario salvar el estado del programa.

Para ello lo que se hace es crear un archivo temporal que se borra cuando la app se restaura, pero si ese archivo lo guardamos en "tpath.GetHomePath" el archivo es permanente hasta que nosotros decidamos suprimirlo.

Este archivo puede contener los siguientes tipo de datos (procede de la clase TBinaryReader, que permite leer tipos de datos desde un stream como valores binarios)

    function ReadBoolean: Boolean; virtual;
    function ReadByte: Byte; virtual;
    function ReadBytes(Count: Integer): TBytes; virtual;
    function ReadChar: Char; virtual;
    function ReadChars(Count: Integer): TCharArray; virtual;
    function ReadDouble: Double; virtual;
    function ReadSByte: ShortInt; inline;
    function ReadShortInt: ShortInt; virtual;
    function ReadSmallInt: SmallInt; virtual;
    function ReadInt16: SmallInt; inline;
    function ReadInteger: Integer; virtual;
    function ReadInt32: Integer; inline;
    function ReadInt64: Int64; virtual;
    function ReadSingle: Single; virtual;
    function ReadString: string; virtual;
    function ReadWord: Word; virtual;
    function ReadUInt16: Word; inline;
    function ReadCardinal: Cardinal; virtual;
    function ReadUInt32: Cardinal; inline;
    function ReadUInt64: UInt64; virtual;


Por ejemplo, para leer un parámetro del tipo Boolean de nuestra app, escribiríamos:

...
VAR
  Reader: TBinaryReader;
...
Parametro1:=Reader.ReadBoolean;
...

Y para almacenarlo:



...
VAR
  Writer: TBinaryWriter;
...
Writer.Write(Parametro1);



En la propiedad "StoragePath"  especificamos el Path para almacenar el estado, si no indicamos nada el estado se perderá al reiniciar la app, en caso contrario el estado se grabará en un fichero, en este caso le he llamado "MIARCHIVODEDATOS.tmp".

El procedimiento para la lectura de parámetros sería el siguiente: (yo lo suelo poner en el ONCREATE del form)

PROCEDURE Tform1.LeerArchivoParametros;
VAR
  Reader: TBinaryReader;

BEGIN
  SaveState.Name := 'MIARCHIVODEDATOS.tmp';
// IMPORTANTE al poner tpath.GetHomePath se crea un archivo permanente
// que no se borra cuando se restaura la app

  SaveState.StoragePath := tpath.GetHomePath;

  IF SaveState.Stream.Size > 0 THEN
  BEGIN
    Reader := TBinaryReader.Create(SaveState.Stream);

    TRY

      Parametro1 := Reader.ReadBoolean;
      Parametro2 := Reader.ReadInteger;
      Parametro3 := Reader.ReadString;

    FINALLY
      Reader.Free;
    END;

  END;

END;

y el procedimiento de escritura sería en el evento "ONSAVESTATE" del form de esta manera:

PROCEDURE Tform1.FormSaveState(Sender: TObject);

VAR
  Writer: TBinaryWriter;

BEGIN
  SaveState.Stream.Clear;

  Writer := TBinaryWriter.Create(SaveState.Stream);

  TRY
    Writer.Write(Parametro1);

    Writer.Write(Parametro2);

    Writer.Write(Parametro3);

  FINALLY
    Writer.Free;
  END;
END;


Lo que me ha parecido interesante de esta forma de almacenar los parámetros de una app es que según las pruebas que he hecho, funciona para cualquier S.O., Android, IOS y Windows.
Recordad que si lo usáis en Android, para que funcione bien, previamente tendréis que desinstalar las versiones previas de la app.


Espero que os haya sido útil, 

...hasta el próximo post...

Cómo añadir gráficos a vuestra app



A continuación os voy a explicar cómo hacer la pantalla de la app “señales de tráfico de España” en la que se muestran en varios gráficos los resultados de los diferentes tests.
En el gráfico superior muestra el resultado de los test realizados en el día de hoy y en el gráfico inferior muestra el resultado de todos los test realizados desde que se instaló la aplicación.
Los valores de ambos gráficos se pueden resetear desde la pestaña "Ajustes"




DISEÑO

Como siempre empezamos añadiendo un toolbar (Align=Top) y varios speedbuttons

  Para volver a la pantalla anterior


Form.Close;
FormAnterior.Show;

   Para mostrar una imagen de ayuda

image2.Bitmap.loadfromfile(‘Imagen1.jpg’)

 Pone o quita las marcas numéricas asociadas a cada barra

  graficoHoy.Series[0].Marks.Visible := not graficoHoy.Series[0].Marks.Visible
  graficoTotal.Series[1].Marks.Visible := not graficoTotal.Series[1].Marks.Visible;


 Pone o quita la leyenda de los gráficos

  graficoHoy.Legend.Visible := not graficoHoy.Legend.Visible;
  graficoglobal.Legend.Visible := not graficoGlobal.Legend.Visible;


Después añadiremos un tTabControl con tres pestañas PROGRESO, GRÁFICOS y AJUSTES. Para añadir una pestaña hay que pulsar con el botón derecho sobre el tTabControl y activar el menú Add tTabitem.

Posteriormente seleccionaremos  dos componentes tLayout (Align=top y Align=client) y dos  tChart (Align=client) y los colocaremos sobre la pestaña Gráficos. Hay que comprobar que en la la vista de estructura quede de esta forma:


Los dos Layout se deben comportar como contenedores de los charts "GraficoGlobal" y "GraficoHoy", esto hay que hacerlo de esta forma para que todas las propiedades de alineamiento del chart  (excepto la propiedad contains) se apliquen sobre los Layout.


AÑADIENDO DATOS A LOS GRÁFICOS.

Haciendo doble clic sobre los tchart abrimos el diseñador de gráficos




Este componente nos permitirá modificar la forma en el que el gráfico se muestra al usuario. Podremos añadir nuevas series, cambiar el tipo de gráfico, colores, quitar y poner leyenda, márgenes, cambiar el fondo, añadir profundidad 3D, etc…

En la app he añadido 2 series, ya que quería mostrar el número de señales correctas e incorrectas de cada test:

- Series[0] llamada LineSeries1 que corresponde con el número de aciertos.
- Series[1] llamada Series1 que se corresponde con el número de errores.

Para añadir valores a la serie 0 asociada con el tchart ( Name=grafico)

grafico.Series[0].AddXY(contador, NumAciertos);

y para añadir un nuevo dato correspondiente a la serie 1

grafico.Series[1].AddXY(contador, NumErrores);

Recordar que siempre hay que inicializar los datos de las series (Yo lo hago en el evento OnShow del Form)

grafico.Series[0].Clear;

grafico.Series[1].Clear;

Es útil crear unos datos de prueba de la forma que os muestro a continuación para ver cómo queda:

for i:=1 to 10 do 
begin
grafico.Series[o].AddXY(i, random(10));
grafico.Series[1].AddXY(i, random(10));
end;


Cómo configurar los estilos en un tlistbox con rad studio


En este post os voy a explicar cómo se configuran los estilos en un listbox utilizando como ejemplo lo que he desarrollado para preparar la pantalla de la app “señales de tráfico de España”.
En esta pantalla se muestran al usuario los diferentes grupos de señales.




DISEÑO DEL FORM:

Como siempre es muy recomendable abrir el módulo Structure View de Rad Studio para ver cómo se organizan en el form los diferentes elementos que lo componen.
Seguidamente empezaremos a configurar la barra superior del form:

Para ello vamos al menú Tool Palette  y seleccionamos:

- un ttoolbar con la propiedad Align=Top
- tRectangle con las siguientes propiedades:
  Align=Client, 
  Fill-Gradient-Edit=#FFE0E0E0
  Fill-Kind=Gradient. 
- dos speedbutton (Uno con Align=Right y otro con Alig=Left)

Después tenemos que crear un objeto que indique qué grupos de señales se muestran en cada momento:


- tLayout (Align = Top) 
-tRectangle (Fill.Color=Blue, Fill.Kind=Solid, XRadius=6, YRadius=6, para poner los bordes redondeados)
- tlabel con el texto “GRUPOS DE SEÑALES DE TRÁFICO”

No olvidar que desde Structure View debemos dejar definido qué componentes dependen unos de otros, por ejemplo en el caso anterior hay que hacer que del tLayout (Contenedor principal) cuelgue un tRectangle, y que a su vez un tLabel cuelgue del tRectangle.

Después situaremos debajo del componente anterior un tListbox (Align=client)

Y por último seleccionaremos un tStylebook (StylebookCateg) para poder diseñar el estilo que queremos aplicar a cada uno de los componentes del ListBox.

Al final tiene que quedar de esta forma:


















DISEÑO DEL TLISTBOX:

Para diseñar cada uno de los ítems del Listbox, vamos a crear un estilo.
Hacemos doble clic sobre el componente StyleBookCateg, para abrir el Style Designer:
En la vista de Estructura tendremos que ir añadiendo componentes, de la misma forma que cuando añadimos componentes al form, para que nos quede del siguiente modo:





En la parte de diseño nos debería quedar así:





Ahora ya se puede ver más claro cómo se corresponde el estilo que hemos creado con cada uno de los ítems del listbox.



Tenemos:

- a la derecha un tspeedbutton:Speedbutton1Style con el símbolo de flecha (Align=Right)
- abajo un tText:Text4Style (Align=Bottom), 
- a la izquierda un tImage:ICON (Align=Left)
- en la parte central un tText:Text (Align=Client). 

Como veis al final siempre es lo mismo, hay que ir seleccionando componentes y jugando con el tipo de alineación para conseguir el efecto deseado.

Por último pulsamos sobre Style Designer con el botón derecho para cerrar la página y guardar cambios










PROGRAMACIÓN

Para diseñar cada uno de los ítems del tListbox:LBSENALES, procederemos a crear un elemento tListBoxItem

Para manejar los objetos que componen cada ítem, Delphi nos proporciona el evento OnApplyStyleLookup, en nuestro caso previamente he creado un array con las descripciones de las categorías de las señales (InfoCateg) para poderlas asociar al estilo “Text4Style”.

Var
Item: TListBoxItem;
Descripción:string;

begin
LBSenales.BeginUpdate;
Item := TListBoxItem.Create(NIL);
Item.Parent := LBSenales;
Item.StyleLookup := 'CustomItemCateg';
Item.Tag := NumSenal;
Item.StylesData['SpeedButton1Style.OnClick'] :=TValue.From(DoInfoClick2);
Item.text := descripcion;
Item.OnApplyStyleLookup := DoItemApplyStyle;
LBSenales.EndUpdate;
End;

PROCEDURE TForm.DoItemApplyStyle(Sender: TObject);
VAR
Item: TListBoxItem;
ItemKey: integer;
StyleObject: TFmxObject;
MiTexto: tText;
BEGIN

Assert(Sender IS TListBoxItem,
'Este handler solo controla ListboxItem');

IF NOT(Sender IS TListBoxItem) THEN
Exit;
Item := Sender AS TListBoxItem;
ItemKey := Item.Tag;

StyleObject := Item.FindStyleResource('Text4Style');
IF Assigned(StyleObject) AND (StyleObject IS tText) THEN
BEGIN
MiTexto := StyleObject AS tText;
BEGIN
MiTexto.text := InfoCateg[ItemKey];
END;
END;

END;



El procedimiento  DoInfoClick2 se disparará cuando el usuario pulse el botón “>”.



PROCEDURE TForm.DoInfoClick2(Sender: TObject);
VAR
Item: TListBoxItem;
BEGIN
Item := TListBoxItem(FindItemParent(Sender AS TFmxObject, TListBoxItem));
IF Assigned(Item) THEN
BEGIN
// Aquí escribiremos lo que queramos que suceda cuando el usuario pulse el botón “>”
// en mi caso cierro el form y abro uno nuevo donde se ven las señales de la categoría
// seleccionada.
END;

END;