====== 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}}