Tabla de Contenidos

Monitor de Clima SQLite

Introducción y Propósito General

Monitor de Clima SQLite es una aplicación de escritorio desarrollada en Lazarus utilizando la Lazarus Component Library (LCL). Su objetivo principal es servir como una herramienta de visualización y análisis de datos meteorológicos almacenados en bases de datos SQLite locales. El sistema genera de manera dinámica toda su interfaz gráfica en tiempo de ejecución, conectándose a bases de datos relacionales para procesar series temporales y representarlas mediante múltiples gráficos de líneas sincronizados.

Código

unit Unit1;
 
{$mode objfpc}{$H+}
 
interface
 
uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls,
  SQLite3Conn, SQLDB, TAGraph, TASeries, TAChartUtils, TAIntervalSources;
 
type
  TMedicionRecord = record
    Fecha: TDateTime;
    Temp: Double;
    Hum: Double;
    Pres: Double;
  end;
 
  { TForm1 }
  TForm1 = class(TForm)
  private
    BtnSeleccionarDB: TButton;
    RadioOpciones: TRadioGroup;
    OpenDialog1: TOpenDialog;
    ChartTemp,
    ChartHum,
    ChartPres: TChart;
    DateSourceTemp,
    DateSourceHum,
    DateSourcePres: TDateTimeIntervalChartSource;
    SQLite3Connection1: TSQLite3Connection;
    SQLTransaction1: TSQLTransaction;
    SQLQuery1: TSQLQuery;
    RutaBD: string;
    SerieTemp,
    SerieHum,
    SeriePresion: TLineSeries;
    Inicializado: Boolean;
    DatosMediciones: array of TMedicionRecord;
    MaxFechaDataset: TDateTime;
    TempMax, TempMin,
    HumMax, HumMin,
    PresMax, PresMin: Double;
    TempMaxFecha, TempMinFecha,
    HumMaxFecha, HumMinFecha,
    PresMaxFecha, PresMinFecha: TDateTime;
 
    procedure BtnSeleccionarDBClick(Sender: TObject);
    procedure RadioOpcionesClick(Sender: TObject);
    procedure CargarYGraficar;
 
    procedure CrearInterfazDinamica;
    procedure ConfigurarConexion;
    procedure ConfigurarGraficas;
 
    procedure ConfigurarEjeHora(
      Chart: TChart;
      DateSource: TDateTimeIntervalChartSource
    );
 
    procedure AxisGetMarkText(
      Sender: TObject;
      var AText: String;
      AMark: Double
    );
 
    procedure FormResize(Sender: TObject);
    procedure ActualizarGraficosYEstadisticas;
 
  protected
    procedure DoShow; override;
 
  public
  end;
 
var
  Form1: TForm1;
 
implementation
 
{$R *.lfm}
 
{ ========================================================= }
{                   FORMULARIO                              }
{ ========================================================= }
 
procedure TForm1.DoShow;
begin
  inherited DoShow;
 
  if not Inicializado then
  begin
    Inicializado := True;
 
    { 1. Crear controles y establecer las dimensiones iniciales }
    CrearInterfazDinamica;
    ConfigurarConexion;
    ConfigurarGraficas;
 
    { 2. Mover al monitor principal DESPUÉS de definir las dimensiones }
    Self.Position := poDesigned;
    Self.Left := Screen.PrimaryMonitor.Left + ((Screen.PrimaryMonitor.Width - Self.Width) div 2);
    Self.Top := Screen.PrimaryMonitor.Top + ((Screen.PrimaryMonitor.Height - Self.Height) div 2);
 
    { 3. Configurar eventos y redimensionar }
    Self.OnResize := @FormResize;
    FormResize(Self);
  end;
end;
 
{ ========================================================= }
{                   CREAR INTERFAZ                          }
{ ========================================================= }
 
procedure TForm1.CrearInterfazDinamica;
begin
  Self.Caption := 'Monitor de Clima';
  Self.SetBounds(100, 100, 950, 780);
 
  { Botón seleccionar BD }
  BtnSeleccionarDB := TButton.Create(Self);
  BtnSeleccionarDB.Parent := Self;
  BtnSeleccionarDB.SetBounds(0, 0, 220, 38);
  BtnSeleccionarDB.Caption := 'Seleccionar BD SQLite...';
  BtnSeleccionarDB.OnClick := @BtnSeleccionarDBClick;
 
  { Control Radio (TRadioGroup) para el rango }
  RadioOpciones := TRadioGroup.Create(Self);
  RadioOpciones.Parent := Self;
  RadioOpciones.SetBounds(0, 0, 280, 48);
  RadioOpciones.Caption := 'Rango de visualización';
  RadioOpciones.Items.Add('Todo el rango');
  RadioOpciones.Items.Add('Últimas 24 horas');
  RadioOpciones.Columns := 2;
  RadioOpciones.ItemIndex := 0;
  RadioOpciones.OnClick := @RadioOpcionesClick;
 
  { Diálogo de archivos }
  OpenDialog1 := TOpenDialog.Create(Self);
  OpenDialog1.Filter := 'Archivos SQLite (*.db;*.sqlite;*.sqlite3)|' +
                        '*.db;*.sqlite;*.sqlite3|' +
                        'Todos los archivos (*.*)|*.*';
 
  { Gráficas }
  ChartTemp := TChart.Create(Self);
  ChartTemp.Parent := Self;
 
  ChartHum := TChart.Create(Self);
  ChartHum.Parent := Self;
 
  ChartPres := TChart.Create(Self);
  ChartPres.Parent := Self;
 
  { Fuentes de fechas }
  DateSourceTemp := TDateTimeIntervalChartSource.Create(Self);
  DateSourceHum := TDateTimeIntervalChartSource.Create(Self);
  DateSourcePres := TDateTimeIntervalChartSource.Create(Self);
 
  { SQLite }
  SQLite3Connection1 := TSQLite3Connection.Create(Self);
  SQLTransaction1 := TSQLTransaction.Create(Self);
  SQLQuery1 := TSQLQuery.Create(Self);
end;
 
{ ========================================================= }
{         EVENTO DEL CONTROL RADIO                          }
{ ========================================================= }
 
procedure TForm1.RadioOpcionesClick(Sender: TObject);
begin
  ActualizarGraficosYEstadisticas;
end;
 
{ ========================================================= }
{         ACTUALIZAR GRÁFICOS, FILTROS Y ESTADÍSTICAS       }
{ ========================================================= }
 
procedure TForm1.ActualizarGraficosYEstadisticas;
var
  i: Integer;
  FiltroFechaMin: TDateTime;
  HayTemp, HayHum, HayPres: Boolean;
  TVal, HVal, PVal: Double;
  FechaVal: TDateTime;
begin
  if Length(DatosMediciones) = 0 then
    Exit;
 
  SerieTemp.Clear;
  SerieHum.Clear;
  SeriePresion.Clear;
 
  HayTemp := False;
  HayHum := False;
  HayPres := False;
 
  if RadioOpciones.ItemIndex = 1 then
    FiltroFechaMin := MaxFechaDataset - 1.0
  else
    FiltroFechaMin := 0;
 
  for i := 0 to High(DatosMediciones) do
  begin
    FechaVal := DatosMediciones[i].Fecha;
 
    if (RadioOpciones.ItemIndex = 0) or (FechaVal >= FiltroFechaMin) then
    begin
      TVal := DatosMediciones[i].Temp;
      HVal := DatosMediciones[i].Hum;
      PVal := DatosMediciones[i].Pres;
 
      SerieTemp.AddXY(FechaVal, TVal);
      SerieHum.AddXY(FechaVal, HVal);
      SeriePresion.AddXY(FechaVal, PVal);
 
      { Temperatura Máx y Mín }
      if not HayTemp then
      begin
        TempMax := TVal; TempMin := TVal;
        TempMaxFecha := FechaVal; TempMinFecha := FechaVal;
        HayTemp := True;
      end
      else
      begin
        if TVal > TempMax then begin TempMax := TVal; TempMaxFecha := FechaVal; end;
        if TVal < TempMin then begin TempMin := TVal; TempMinFecha := FechaVal; end;
      end;
 
      { Humedad Máx y Mín }
      if not HayHum then
      begin
        HumMax := HVal; HumMin := HVal;
        HumMaxFecha := FechaVal; HumMinFecha := FechaVal;
        HayHum := True;
      end
      else
      begin
        if HVal > HumMax then begin HumMax := HVal; HumMaxFecha := FechaVal; end;
        if HVal < HumMin then begin HumMin := HVal; HumMinFecha := FechaVal; end;
      end;
 
      { Presión Máx y Mín }
      if not HayPres then
      begin
        PresMax := PVal; PresMin := PVal;
        PresMaxFecha := FechaVal; PresMinFecha := FechaVal;
        HayPres := True;
      end
      else
      begin
        if PVal > PresMax then begin PresMax := PVal; PresMaxFecha := FechaVal; end;
        if PVal < PresMin then begin PresMin := PVal; PresMinFecha := FechaVal; end;
      end;
    end;
  end;
 
  {
    FORMATO ESTANDARIZADO DE LEYENDA (Uso de %% para imprimir el carácter % en Format)
    Línea 1: Titular igualado a 20 caracteres visibles exactos.
    Línea 2 y 3: Mismo formato de ancho fijo (%7.2f).
  }
  if HayTemp then
    SerieTemp.Title := Format('Temperatura (°C)    ' + LineEnding +
                              ' Máx: %7.2f (%s)' + LineEnding +
                              ' Mín: %7.2f (%s)',
                              [TempMax, FormatDateTime('dd-mm hh:nn', TempMaxFecha),
                               TempMin, FormatDateTime('dd-mm hh:nn', TempMinFecha)])
  else
    SerieTemp.Title := 'Temperatura (°C)    ';
 
  if HayHum then
    SerieHum.Title := Format('Humedad (%%)          ' + LineEnding +
                             ' Máx: %7.2f (%s)' + LineEnding +
                             ' Mín: %7.2f (%s)',
                             [HumMax, FormatDateTime('dd-mm hh:nn', HumMaxFecha),
                              HumMin, FormatDateTime('dd-mm hh:nn', HumMinFecha)])
  else
    SerieHum.Title := 'Humedad (%%)          ';
 
  if HayPres then
    SeriePresion.Title := Format('Presión (hPa)       ' + LineEnding +
                              ' Máx: %7.2f (%s)' + LineEnding +
                              ' Mín: %7.2f (%s)',
                              [PresMax, FormatDateTime('dd-mm hh:nn', PresMaxFecha),
                               PresMin, FormatDateTime('dd-mm hh:nn', PresMinFecha)])
  else
    SeriePresion.Title := 'Presión (hPa)       ';
 
  ChartTemp.Invalidate;
  ChartHum.Invalidate;
  ChartPres.Invalidate;
end;
 
{ ========================================================= }
{                       REDIMENSIONAR                       }
{ ========================================================= }
 
procedure TForm1.FormResize(Sender: TObject);
var
  AltoDisponible, AltoGrafico, MargenTop, AnchoGrafica: Integer;
  AnchoTotalPanel, PosInicioPanel, EspacioEntreControles: Integer;
begin
  MargenTop := 65;
 
  { 1. Centrar Botón y RadioGroup en la parte superior }
  EspacioEntreControles := 20;
  AnchoTotalPanel := BtnSeleccionarDB.Width + EspacioEntreControles + RadioOpciones.Width;
  PosInicioPanel := (Self.ClientWidth - AnchoTotalPanel) div 2;
 
  if PosInicioPanel < 10 then
    PosInicioPanel := 10;
 
  BtnSeleccionarDB.Left := PosInicioPanel;
  BtnSeleccionarDB.Top := 10;
 
  RadioOpciones.Left := PosInicioPanel + BtnSeleccionarDB.Width + EspacioEntreControles;
  RadioOpciones.Top := 2;
 
  { 2. Calcular espacio para los gráficos }
  AltoDisponible := Self.ClientHeight - MargenTop - 10;
 
  if AltoDisponible < 300 then
    Exit;
 
  AltoGrafico := AltoDisponible div 3;
  AnchoGrafica := Self.ClientWidth - 20;
 
  if AnchoGrafica < 300 then
    AnchoGrafica := 300;
 
  ChartTemp.SetBounds(10, MargenTop, AnchoGrafica, AltoGrafico - 10);
  ChartHum.SetBounds(10, MargenTop + AltoGrafico, AnchoGrafica, AltoGrafico - 10);
  ChartPres.SetBounds(10, MargenTop + (AltoGrafico * 2), AnchoGrafica, AltoGrafico - 10);
end;
 
{ ========================================================= }
{                     CONFIGURAR SQLITE                     }
{ ========================================================= }
 
procedure TForm1.ConfigurarConexion;
begin
  SQLTransaction1.DataBase := SQLite3Connection1;
  SQLQuery1.DataBase := SQLite3Connection1;
  SQLQuery1.Transaction := SQLTransaction1;
end;
 
{ ========================================================= }
{                     FORMATO DE HORA                       }
{ ========================================================= }
 
procedure TForm1.AxisGetMarkText(
  Sender: TObject;
  var AText: String;
  AMark: Double
);
begin
  AText := FormatDateTime('hh:nn', AMark);
end;
 
{ ========================================================= }
{                     EJE DE TIEMPO                         }
{ ========================================================= }
 
procedure TForm1.ConfigurarEjeHora(
  Chart: TChart;
  DateSource: TDateTimeIntervalChartSource
);
begin
  DateSource.DateTimeFormat := 'hh:nn';
  DateSource.Params.MaxLength := 80;
  DateSource.Params.MinLength := 20;
 
  Chart.BottomAxis.Marks.Source := DateSource;
  Chart.BottomAxis.Marks.Style := smsLabel;
  Chart.BottomAxis.OnGetMarkText := @AxisGetMarkText;
  Chart.BottomAxis.Marks.LabelFont.Orientation := 900;
end;
 
{ ========================================================= }
{                    CONFIGURAR GRÁFICAS                    }
{ ========================================================= }
 
procedure TForm1.ConfigurarGraficas;
begin
  ConfigurarEjeHora(ChartTemp, DateSourceTemp);
  ConfigurarEjeHora(ChartHum, DateSourceHum);
  ConfigurarEjeHora(ChartPres, DateSourcePres);
 
  { Temperatura }
  ChartTemp.ClearSeries;
  ChartTemp.Title.Text.Text := 'Temperatura';
  ChartTemp.Title.Visible := True;
  ChartTemp.Legend.Visible := True;
  ChartTemp.Legend.Font.Name := 'Courier New';
  ChartTemp.Margins.Top := 10;
  ChartTemp.Margins.Bottom := 10;
 
  SerieTemp := TLineSeries.Create(ChartTemp);
  SerieTemp.SeriesColor := clRed;
  SerieTemp.LinePen.Width := 3;
  ChartTemp.AddSeries(SerieTemp);
 
  { Humedad }
  ChartHum.ClearSeries;
  ChartHum.Title.Text.Text := 'Humedad Relativa';
  ChartHum.Title.Visible := True;
  ChartHum.Legend.Visible := True;
  ChartHum.Legend.Font.Name := 'Courier New';
  ChartHum.Margins.Top := 10;
  ChartHum.Margins.Bottom := 10;
 
  SerieHum := TLineSeries.Create(ChartHum);
  SerieHum.SeriesColor := clBlue;
  SerieHum.LinePen.Width := 3;
  ChartHum.AddSeries(SerieHum);
 
  { Presión }
  ChartPres.ClearSeries;
  ChartPres.Title.Text.Text := 'Presión Atmosférica';
  ChartPres.Title.Visible := True;
  ChartPres.Legend.Visible := True;
  ChartPres.Legend.Font.Name := 'Courier New';
  ChartPres.Margins.Top := 10;
  ChartPres.Margins.Bottom := 10;
 
  SeriePresion := TLineSeries.Create(ChartPres);
  SeriePresion.SeriesColor := clGreen;
  SeriePresion.LinePen.Width := 3;
  ChartPres.AddSeries(SeriePresion);
end;
 
{ ========================================================= }
{                 SELECCIONAR BASE DE DATOS                 }
{ ========================================================= }
 
procedure TForm1.BtnSeleccionarDBClick(Sender: TObject);
begin
  if OpenDialog1.Execute then
  begin
    RutaBD := OpenDialog1.FileName;
    CargarYGraficar;
  end;
end;
 
{ ========================================================= }
{                  CARGAR Y GRAFICAR DATOS                  }
{ ========================================================= }
 
procedure TForm1.CargarYGraficar;
var
  HoraVal: TDateTime;
  StrFecha: string;
  Temp, Hum, Pres: Double;
  FS: TFormatSettings;
  Count: Integer;
begin
  if not FileExists(RutaBD) then
  begin
    ShowMessage('El archivo especificado no existe: ' + RutaBD);
    Exit;
  end;
 
  FS := DefaultFormatSettings;
  FS.DateSeparator := '-';
  FS.TimeSeparator := ':';
  FS.ShortDateFormat := 'yyyy-mm-dd';
  FS.LongTimeFormat := 'hh:nn:ss';
 
  try
    SQLite3Connection1.DatabaseName := RutaBD;
    SQLite3Connection1.Connected := True;
    SQLTransaction1.StartTransaction;
 
    SQLQuery1.SQL.Text :=
      'SELECT fecha, temperatura, humedad, presion ' +
      'FROM mediciones ' +
      'ORDER BY id ASC';
    SQLQuery1.Open;
 
    Count := 0;
    SetLength(DatosMediciones, 0);
 
    while not SQLQuery1.EOF do
    begin
      StrFecha := SQLQuery1.FieldByName('fecha').AsString;
 
      if not TryStrToDateTime(StrFecha, HoraVal, FS) then
        TryStrToDateTime(StrFecha, HoraVal);
 
      Temp := SQLQuery1.FieldByName('temperatura').AsFloat;
      Hum := SQLQuery1.FieldByName('humedad').AsFloat;
      Pres := SQLQuery1.FieldByName('presion').AsFloat;
 
      SetLength(DatosMediciones, Count + 1);
      DatosMediciones[Count].Fecha := HoraVal;
      DatosMediciones[Count].Temp := Temp;
      DatosMediciones[Count].Hum := Hum;
      DatosMediciones[Count].Pres := Pres;
 
      Inc(Count);
      SQLQuery1.Next;
    end;
 
    if Count > 0 then
      MaxFechaDataset := DatosMediciones[Count - 1].Fecha;
 
    SQLQuery1.Close;
    SQLTransaction1.Commit;
    SQLite3Connection1.Connected := False;
 
    ActualizarGraficosYEstadisticas;
 
  except
    on E: Exception do
    begin
      ShowMessage('Error al consultar la base de datos: ' + E.Message);
      if SQLTransaction1.Active then
        SQLTransaction1.Rollback;
      if SQLite3Connection1.Connected then
        SQLite3Connection1.Connected := False;
    end;
  end;
end;
 
end.

Estructura de Datos y Clases Principales

  1. TMedicionRecord: Estructura de registro optimizada para almacenar de forma temporal en la memoria RAM los parámetros clave de cada lectura meteorológica. Contiene los campos Fecha (TDateTime), Temp (Double), Hum (Double) y Pres (Double), evitando consultas repetitivas al disco al alternar entre filtros visuales.
  2. TForm1: La clase central del programa que hereda de TForm. Encapsula la lógica de los controles visuales creados por código, los componentes de conectividad a bases de datos, los algoritmos de filtrado temporal, el redimensionamiento responsivo y el renderizado gráfico.

TForm1

Componentes y Variables Privadas

Controles de Interfaz:

Subsistema de Base de Datos:

Estado y Almacenamiento:

Métodos y Procedimientos Funcionales

Ciclo de Vida y Configuración Inicial

Gestión de Datos y Conectividad

Representación Temporal y Ejes

Diseño Responsivo y Eventos de Usuario

, , ,