====== Monitor de Clima SQLite ======
* [[Estación meteorológica ESP8266 (D1 Mini), con captación de datos y elaboración de gráficos]]
* [[Informática y cacharreo]]
==== 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 ====
- **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.
- **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:**
* **BtnSeleccionarDB**: Botón de control estándar encargado de disparar el cuadro de diálogo para la selección del archivo de base de datos.
* **RadioOpciones**: Componente de opciones exclusivas (TRadioGroup) que permite al usuario alternar dinámicamente entre visualizar todo el historial o acotar la vista a las últimas 24 horas.
* **OpenDialog1**: Selector de archivos del sistema operativo configurado para aceptar únicamente extensiones SQLite (.db, .sqlite, .sqlite3).
* **ChartTemp, ChartHum, ChartPres**: Instancias de gráficos (TChart) destinadas a representar individualmente la temperatura, la humedad relativa y la presión atmosférica.
* **LabelTempMaxMin, LabelHumMaxMin, LabelPresMaxMin**: Etiquetas de texto flotantes posicionadas sobre las esquinas de los gráficos para mostrar los valores estadísticos máximos y mínimos del conjunto actual.
**Subsistema de Base de Datos:**
* SQLite3Connection1: Componente de conexión física con el motor SQLite.
* SQLTransaction1: Administrador de transacciones requerido por la arquitectura de bases de datos de Free Pascal (SQLDB).
* SQLQuery1: Objeto ejecutor de consultas SQL para la extracción de registros.
**Estado y Almacenamiento:**
* **RutaBD**: Cadena de texto que almacena la ruta del archivo de base de datos seleccionado.
* **DatosMediciones**: Arreglo dinámico (array of TMedicionRecord) que retiene todo el dataset en memoria.
* **MaxFechaDataset**: Marca temporal más reciente detectada en el conjunto de datos, empleada como pivote para calcular el filtro de las últimas 24 horas.
* **TempMax, TempMin, HumMax, HumMin, PresMax, PresMin**: Variables de doble precisión que almacenan los extremos numéricos calculados en tiempo real.
* **Inicializado**: Bandera de control booleano para asegurar que la inicialización y el posicionamiento de la ventana se ejecuten una única vez.
== Métodos y Procedimientos Funcionales ==
**Ciclo de Vida y Configuración Inicial**
* **DoShow; override**: Método protegido heredado de TForm que se ejecuta de manera segura cuando el formulario se hace visible por primera vez. Invoca la creación de la interfaz, centra la ventana en el monitor principal y activa el evento de redimensionamiento.
* **CrearInterfazDinamica**: Procedimiento encargado de instanciar programáticamente todos los controles de la aplicación, asignando sus jerarquías de componentes (Parent), propiedades iniciales y dimensiones base.
* **ConfigurarConexion**: Enlaza estructuralmente los componentes de SQLite, asociando la conexión física a la transacción y esta última al objeto de consulta.
* **ConfigurarGraficas**: Define la estética visual de los componentes TChart, estableciendo títulos, leyendas con fuentes monoespaciadas (Courier New), márgenes y creando las series de líneas (TLineSeries) con sus respectivos grosores y colores distintivos (rojo para temperatura, azul para humedad y verde para presión).
* **CrearEtiquetasMaxMin**: Instancia y posiciona los componentes TLabel asignando los gráficos como sus contenedores contenedores padre, configurando el ajuste automático de tamaño.
**Gestión de Datos y Conectividad**
* **BtnSeleccionarDBClick(Sender: TObject)**: Evento vinculado al botón principal. Abre el diálogo de selección de archivos y, al confirmar una ruta válida, dispara la carga de los registros.
* **CargarYGraficar**: Controla el flujo completo de lectura de la base de datos aplicando configuraciones regionales de formato de fecha y hora personalizadas para evitar errores de conversión. Ejecuta una consulta SELECT ordenada cronológicamente de forma ascendente, recorre iterativamente los resultados poblando el arreglo dinámico en memoria, extrae la fecha máxima y cierra la conexión de forma segura antes de actualizar la interfaz.
* **ActualizarGraficosYEstadisticas**: Procesa la información del arreglo dinámico aplicando el filtro seleccionado en el grupo de opciones. Limpia los gráficos, recorre los puntos filtrados, recalcula dinámicamente los valores máximos y mínimos del subconjunto visible, actualiza las etiquetas estadísticas y fuerza el redibujado de los componentes (Invalidate).
**Representación Temporal y Ejes**
* **ConfigurarEjeHora(Chart: TChart, DateSource: TDateTimeIntervalChartSource)**: Configura el comportamiento del eje horizontal inferior de un gráfico mediante una fuente de intervalos de tiempo (TDateTimeIntervalChartSource), definiendo los límites de longitud de los intervalos y orientando las marcas de texto a 90 grados para optimizar el espacio.
* **AxisGetMarkText(Sender: TObject, var AText: String, AMark: Double)**: Manejador de evento que transforma los valores numéricos internos de fecha y hora en cadenas de texto legibles con formato de horas y minutos (hh:nn).
**Diseño Responsivo y Eventos de Usuario**
* **FormResize(Sender: TObject)**: Procedimiento invocado automáticamente cada vez que el usuario modifica las dimensiones de la ventana. Recalcula y aplica de forma proporcional las coordenadas y tamaños del botón superior, el panel del grupo de opciones, la distribución equitativa del espacio vertical entre las tres gráficas y la ubicación de las etiquetas de estadísticas.
* **RadioOpcionesClick(Sender: TObject)**: Evento accionado al cambiar la opción en el control de rango, el cual invoca de manera inmediata la rutina de actualización para reflejar el filtrado de datos al instante.
{{tag> informática_y_cacharreo Pascal FreePascal Lazarus}}