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.
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.
Controles de Interfaz:
Subsistema de Base de Datos:
Estado y Almacenamiento:
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