add light/dark theme with global persistence

- AppTheme.pas: новый файл, TAppTheme record + DarkTheme()/LightTheme()
- FlatSlider: цвета через SetThemeColors вместо хардкода
- SpectrumView: все цвета из TAppTheme (SetTheme)
- SettingsForm: ApplyTheme(T) с рекурсивным обходом контролов
- DeviceForm: SetTheme(T) — перекраска всех элементов включая TEdit
- Settings: SaveTheme/LoadTheme в корневой секции preferences (не привязано к устройству)
- MainForm: тема загружается при старте, сохраняется при изменении

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-02 19:00:45 +03:00
co-authored by Claude Sonnet 4.6
parent f89b15730e
commit d9a2157719
8 changed files with 785 additions and 173 deletions
+303
View File
@@ -0,0 +1,303 @@
unit AppTheme;
{ Цветовые темы приложения EWSDR.
Определяет TAppTheme — запись со всеми цветами интерфейса.
Два конструктора: DarkTheme() и LightTheme(). }
{$mode objfpc}{$H+}
interface
uses Graphics;
type
TAppTheme = record
// Форма / панели
BG, Panel, Border: TColor;
// Текст
Text, TextDim: TColor;
// Обычные кнопки (StyleButton)
BtnNorm, BtnActive, BtnHot: TColor;
BtnBorderNorm, BtnBorderActive: TColor;
BtnText, BtnTextActive: TColor;
// Кнопки SPAN / VFO-copy / HidePanel (StyleSpanButton)
SpanNorm, SpanActive: TColor;
SpanBorder: TColor;
SpanText: TColor;
// Кнопки тулбара (Discover / StartStop)
TbDiscoverNorm, TbDiscoverHot, TbDiscoverBorder: TColor;
TbDiscoverText: TColor;
TbStartNorm, TbStartBorder, TbStartText: TColor;
// VFO-select кнопка — активный VFO
VfoSelActNorm, VfoSelActBorder: TColor;
VfoSelActText, VfoSelActTextAct: TColor;
// VFO-select кнопка — неактивный VFO
VfoSelInaNorm, VfoSelInaBorder: TColor;
VfoSelInaText, VfoSelInaTextAct: TColor;
// VFO-TX кнопка — активна (TX на этом VFO)
VfoTxActNorm, VfoTxActBorder: TColor;
VfoTxActText: TColor;
// VFO-TX кнопка — неактивна
VfoTxInaNorm, VfoTxInaBorder: TColor;
VfoTxInaText: TColor;
// Дисплей частоты
FreqActive, FreqDim: TColor;
// Индикаторы (S-meter, мощность)
MeterOn, MeterOvr: TColor;
Amber: TColor;
// Спектр — фон и сетка
SpecGradTop, SpecGradBot: TColor;
SpecLabelBand: TColor;
SpecLabelText: TColor;
SpecGrid: TColor;
// Спектр — сигнал и маркеры
SpecLine: TColor;
SpecFilter: TColor;
SpecFilterEdge: TColor;
SpecAgcColor: TColor;
SpecAgcHangColor: TColor;
SpecVfoCursor: TColor;
// Спектр — градиент заливки (каналы Blue/Green/Red в сыром пикселе)
SpecGradB, SpecGradG, SpecGradR: Byte; // при Alpha=200
// Линейка частот
RulerGradTop, RulerGradBot: TColor;
RulerBorder: TColor;
RulerText: TColor;
RulerVfo: TColor;
// Ползунки
SliderBG: TColor;
SliderTrackEmpty: TColor;
SliderTrackFill: TColor;
SliderThumbNorm: TColor;
SliderThumbHot: TColor;
SliderThumbDrag: TColor;
SliderThumbBdr: TColor;
// S-метр (широкий)
SMeterBG: TColor;
SMeterBarGreen: TColor;
SMeterBarOver: TColor;
SMeterPeakMarker: TColor;
SMeterTickGreen: TColor;
SMeterTickWhite: TColor;
SMeterTickBlue: TColor;
SMeterLabelDbm: TColor;
SMeterLabelS: TColor;
SMeterScaleDbm: TColor;
SMeterScaleSGreen: TColor;
SMeterScaleSBlue: TColor;
SMeterBdr: TColor;
SMeterDivider: TColor;
end;
function DarkTheme: TAppTheme;
function LightTheme: TAppTheme;
implementation
function DarkTheme: TAppTheme;
begin
FillChar(Result, SizeOf(Result), 0);
Result.BG := TColor($00101010);
Result.Panel := TColor($00181818);
Result.Border := TColor($00303030);
Result.Text := TColor($00CCCCCC);
Result.TextDim := TColor($00666666);
Result.BtnNorm := TColor($001A1A1A);
Result.BtnActive := TColor($000C3010);
Result.BtnHot := TColor($00282828);
Result.BtnBorderNorm := TColor($00444444);
Result.BtnBorderActive := TColor($00387838);
Result.BtnText := TColor($00CCCCCC);
Result.BtnTextActive := TColor($0000FF88);
Result.SpanNorm := TColor($00543F18);
Result.SpanActive := TColor($00624B1D);
Result.SpanBorder := TColor($00685624);
Result.SpanText := TColor($00D8E6EE);
Result.TbDiscoverNorm := TColor($00101828);
Result.TbDiscoverHot := TColor($00182038);
Result.TbDiscoverBorder := TColor($00304860);
Result.TbDiscoverText := TColor($0090B8D0);
Result.TbStartNorm := TColor($00101810);
Result.TbStartBorder := TColor($00304830);
Result.TbStartText := TColor($0090C090);
Result.VfoSelActNorm := TColor($00101C10);
Result.VfoSelActBorder := TColor($004A7A4A);
Result.VfoSelActText := TColor($0060B860);
Result.VfoSelActTextAct := TColor($0040FF60);
Result.VfoSelInaNorm := TColor($001A1A1A);
Result.VfoSelInaBorder := TColor($00404040);
Result.VfoSelInaText := TColor($00666666);
Result.VfoSelInaTextAct := TColor($00CCCCCC);
Result.VfoTxActNorm := TColor($00000060);
Result.VfoTxActBorder := TColor($001010B0);
Result.VfoTxActText := TColor($00C8D8FF);
Result.VfoTxInaNorm := TColor($00303030);
Result.VfoTxInaBorder := TColor($00404040);
Result.VfoTxInaText := TColor($00808080);
Result.FreqActive := TColor($0000FF00);
Result.FreqDim := TColor($00007800);
Result.MeterOn := TColor($0000CC44);
Result.MeterOvr := TColor($000000CC);
Result.Amber := TColor($0000AAFF);
Result.SpecGradTop := TColor($0020160B);
Result.SpecGradBot := TColor($006C4F1F);
Result.SpecLabelBand := TColor($00543F18);
Result.SpecLabelText := TColor($00A8C9D9);
Result.SpecGrid := TColor($00685624);
Result.SpecLine := TColor($0040FF80);
Result.SpecFilter := TColor($006A4E22);
Result.SpecFilterEdge := TColor($0040FFCC);
Result.SpecAgcColor := TColor($0000AAFF);
Result.SpecAgcHangColor := TColor($00FFCC00);
Result.SpecVfoCursor := TColor($0000AAFF);
Result.SpecGradB := 100; Result.SpecGradG := 200; Result.SpecGradR := 0;
Result.RulerGradTop := TColor($00372A12);
Result.RulerGradBot := TColor($00705A2A);
Result.RulerBorder := TColor($00685624);
Result.RulerText := TColor($00C8D8E2);
Result.RulerVfo := TColor($0000FF80);
Result.SliderBG := TColor($00181818);
Result.SliderTrackEmpty := TColor($00323232);
Result.SliderTrackFill := TColor($00205018);
Result.SliderThumbNorm := TColor($00407840);
Result.SliderThumbHot := TColor($0066BB66);
Result.SliderThumbDrag := TColor($0099EE99);
Result.SliderThumbBdr := TColor($00122012);
Result.SMeterBG := TColor($00181818);
Result.SMeterBarGreen := TColor($00091F00);
Result.SMeterBarOver := TColor($00091528);
Result.SMeterPeakMarker := TColor($00F8F8FF);
Result.SMeterTickGreen := TColor($00708070);
Result.SMeterTickWhite := TColor($00E0E0DC);
Result.SMeterTickBlue := TColor($0070A090);
Result.SMeterLabelDbm := TColor($0096D8DC);
Result.SMeterLabelS := TColor($00F0F0F0);
Result.SMeterScaleDbm := TColor($00D0DCDC);
Result.SMeterScaleSGreen:= TColor($00E0E0E0);
Result.SMeterScaleSBlue := TColor($0090D090);
Result.SMeterBdr := TColor($00303028);
Result.SMeterDivider := TColor($00404040);
end;
function LightTheme: TAppTheme;
begin
FillChar(Result, SizeOf(Result), 0);
// Фон — тёплый белый, панели — тёплый светло-серый
Result.BG := TColor($00F0EEE8);
Result.Panel := TColor($00E8E6E0);
Result.Border := TColor($00B8B4AC);
Result.Text := TColor($00181410);
Result.TextDim := TColor($00706860);
// Обычные кнопки — светло-серые, активная — светло-зелёная
Result.BtnNorm := TColor($00DCDAD4);
Result.BtnActive := TColor($00B8D8B8);
Result.BtnHot := TColor($00CFCDC6);
Result.BtnBorderNorm := TColor($00A8A4A0);
Result.BtnBorderActive := TColor($00407840);
Result.BtnText := TColor($00181410);
Result.BtnTextActive := TColor($00004800);
// Span/VFO-copy — тёплый тан/пергамент
Result.SpanNorm := TColor($00D4C8A0);
Result.SpanActive := TColor($00C0B488);
Result.SpanBorder := TColor($00A09070);
Result.SpanText := TColor($00302010);
// Тулбар — более насыщенные оттенки
Result.TbDiscoverNorm := TColor($00C8D8E8);
Result.TbDiscoverHot := TColor($00B8CCE0);
Result.TbDiscoverBorder := TColor($00789CB8);
Result.TbDiscoverText := TColor($00284860);
Result.TbStartNorm := TColor($00C8E0C8);
Result.TbStartBorder := TColor($00609860);
Result.TbStartText := TColor($00204820);
// VFO-select активный — зелёная подсветка
Result.VfoSelActNorm := TColor($00C0E0C0);
Result.VfoSelActBorder := TColor($00508050);
Result.VfoSelActText := TColor($00205020);
Result.VfoSelActTextAct := TColor($00006600);
// VFO-select неактивный — нейтральный
Result.VfoSelInaNorm := TColor($00DCDAD4);
Result.VfoSelInaBorder := TColor($00A8A4A0);
Result.VfoSelInaText := TColor($00706860);
Result.VfoSelInaTextAct := TColor($00181410);
// VFO-TX — синеватый акцент
Result.VfoTxActNorm := TColor($00C0CCEC);
Result.VfoTxActBorder := TColor($006070C0);
Result.VfoTxActText := TColor($00102060);
Result.VfoTxInaNorm := TColor($00DCDAD4);
Result.VfoTxInaBorder := TColor($00A8A4A0);
Result.VfoTxInaText := TColor($00706860);
// Дисплей частоты — насыщенный зелёный
Result.FreqActive := TColor($00005500);
Result.FreqDim := TColor($00447044);
// Индикаторы
Result.MeterOn := TColor($00006820);
Result.MeterOvr := TColor($00AA1010);
Result.Amber := TColor($00904000);
// Спектр — светлый пергаментный фон, тёмно-зелёный сигнал
Result.SpecGradTop := TColor($00E8E4D8);
Result.SpecGradBot := TColor($00D0CCBE);
Result.SpecLabelBand := TColor($00D8D4C8);
Result.SpecLabelText := TColor($00404038);
Result.SpecGrid := TColor($00B8B4A4);
Result.SpecLine := TColor($00006400); // тёмно-зелёная линия
Result.SpecFilter := TColor($00C8DCC8); // светло-зелёная полоса фильтра
Result.SpecFilterEdge := TColor($00407840);
Result.SpecAgcColor := TColor($00904000); // тёмно-оранжевый AGC
Result.SpecAgcHangColor := TColor($00802000);
Result.SpecVfoCursor := TColor($00004800);
// Пиксели градиента: тёмно-зелёный (B=0, G=80, R=10 при Alpha=200)
Result.SpecGradB := 0; Result.SpecGradG := 80; Result.SpecGradR := 10;
// Линейка — тёплый пергамент
Result.RulerGradTop := TColor($00DDD8CC);
Result.RulerGradBot := TColor($00C8C2B4);
Result.RulerBorder := TColor($00A09878);
Result.RulerText := TColor($00302820);
Result.RulerVfo := TColor($00006800);
// Ползунки — светлые с зелёным акцентом
Result.SliderBG := TColor($00E8E6E0);
Result.SliderTrackEmpty := TColor($00CCCAC4);
Result.SliderTrackFill := TColor($0080AA80);
Result.SliderThumbNorm := TColor($00508050);
Result.SliderThumbHot := TColor($003D9040);
Result.SliderThumbDrag := TColor($0028A830);
Result.SliderThumbBdr := TColor($00183018);
// S-метр — светлый
Result.SMeterBG := TColor($00ECE8E0);
Result.SMeterBarGreen := TColor($00C0EAC0);
Result.SMeterBarOver := TColor($00EAC0C0);
Result.SMeterPeakMarker := TColor($00181010);
Result.SMeterTickGreen := TColor($00508050);
Result.SMeterTickWhite := TColor($00282018);
Result.SMeterTickBlue := TColor($00406880);
Result.SMeterLabelDbm := TColor($00204860);
Result.SMeterLabelS := TColor($00201810);
Result.SMeterScaleDbm := TColor($00304848);
Result.SMeterScaleSGreen:= TColor($00204020);
Result.SMeterScaleSBlue := TColor($00204050);
Result.SMeterBdr := TColor($00A0A090);
Result.SMeterDivider := TColor($00B0ACA0);
end;
end.
+66 -2
View File
@@ -5,7 +5,7 @@ unit DeviceForm;
interface
uses
Classes, SysUtils, FlatButton, Forms, Controls, Graphics, Dialogs,
Classes, SysUtils, FlatButton, AppTheme, Forms, Controls, Graphics, Dialogs,
StdCtrls, ExtCtrls, ComCtrls, IniFiles;
// Декодирование типа платы (совпадает с MainForm.BoardTypeName)
@@ -94,6 +94,8 @@ type
public
constructor Create(AOwner: TComponent); override;
procedure SetTheme(const T: TAppTheme);
// Добавить найденное устройство (вызывается из MainForm при discovery)
procedure AddDiscovered(const IP, DisplayName: string; BoardType: Integer = 0);
procedure ClearDiscovered;
@@ -271,7 +273,69 @@ end;
procedure TDeviceDialog.ApplyTheme;
begin
// уже задано в BuildUI
end;
procedure TDeviceDialog.SetTheme(const T: TAppTheme);
procedure StyleBtn(B: TFlatButton; Accent: Boolean);
begin
B.ClrNorm := T.Panel;
B.ClrBorder := T.Border;
B.ClrHot := T.BG;
B.ClrActive := T.Panel;
B.ClrText := T.Text;
if Accent then B.ClrTextAct := T.FreqActive
else B.ClrTextAct := T.Text;
B.Font.Color := T.Text;
B.Invalidate;
end;
procedure WalkLabels(C: TWinControl);
var i: Integer; Ctrl: TControl;
begin
for i := 0 to C.ControlCount - 1 do
begin
Ctrl := C.Controls[i];
if Ctrl is TLabel then
TLabel(Ctrl).Font.Color := T.TextDim
else if Ctrl is TWinControl then
WalkLabels(TWinControl(Ctrl));
end;
end;
begin
Color := T.BG;
Font.Color := T.Text;
if PanelLeft <> nil then PanelLeft.Color := T.Panel;
if PanelRight <> nil then PanelRight.Color := T.Panel;
if LstSaved <> nil then begin
LstSaved.Color := T.BG;
LstSaved.Font.Color := T.Text;
end;
if LstFound <> nil then begin
LstFound.Color := T.BG;
LstFound.Font.Color := T.Text;
end;
if BtnAdd <> nil then StyleBtn(BtnAdd, False);
if BtnRemove <> nil then StyleBtn(BtnRemove, False);
if BtnSetAuto <> nil then StyleBtn(BtnSetAuto, False);
if BtnDiscover <> nil then StyleBtn(BtnDiscover, False);
if BtnAddFound <> nil then StyleBtn(BtnAddFound, False);
if BtnCancel <> nil then StyleBtn(BtnCancel, False);
if BtnConnect <> nil then StyleBtn(BtnConnect, True);
// Edit fields: white bg for light theme, dark bg for dark theme
if Integer(T.BG) > Integer(TColor($00808080)) then
begin
if EdName <> nil then begin EdName.Color := TColor($00FFFFFF); EdName.Font.Color := TColor($00202020); end;
if EdIP <> nil then begin EdIP.Color := TColor($00FFFFFF); EdIP.Font.Color := TColor($00202020); end;
end
else
begin
if EdName <> nil then begin EdName.Color := TColor($00121212); EdName.Font.Color := TColor($00E0E0E0); end;
if EdIP <> nil then begin EdIP.Color := TColor($00121212); EdIP.Font.Color := TColor($00E0E0E0); end;
end;
WalkLabels(Self);
Invalidate;
end;
procedure TDeviceDialog.LoadSaved;
+51 -31
View File
@@ -1,7 +1,8 @@
unit FlatSlider;
{ Кастомный горизонтальный ползунок с тёмной темой — замена TTrackBar.
Полностью отрисован через Canvas, одинаково выглядит на Windows и Linux. }
{ Кастомный горизонтальный ползунок — замена TTrackBar.
Полностью отрисован через Canvas, одинаково выглядит на Windows и Linux.
Цвета настраиваемые: применяй SetThemeColors(T: TAppTheme) для смены темы. }
{$mode objfpc}{$H+}
@@ -20,6 +21,14 @@ type
FDragging: Boolean;
FHot: Boolean;
FOnChange: TNotifyEvent;
// Цвета (настраиваются через SetThemeColors или напрямую)
FClrBG: TColor;
FClrTrackEmpty: TColor;
FClrTrackFill: TColor;
FClrThumbNorm: TColor;
FClrThumbHot: TColor;
FClrThumbDrag: TColor;
FClrThumbBdr: TColor;
procedure SetPosition(V: Integer);
procedure SetMin(V: Integer);
procedure SetMax(V: Integer);
@@ -39,6 +48,11 @@ type
public
constructor Create(AOwner: TComponent); override;
// Применить набор цветов из темы
procedure SetThemeColors(
ABG, ATrackEmpty, ATrackFill,
AThumbNorm, AThumbHot, AThumbDrag, AThumbBdr: TColor);
property Min: Integer read FMin write SetMin;
property Max: Integer read FMax write SetMax;
property Position: Integer read FPosition write SetPosition;
@@ -51,17 +65,9 @@ type
implementation
const
THUMB_R = 6; // радиус ручки (px)
TRACK_H = 4; // высота рельса (px)
PAD = THUMB_R + 2; // боковой отступ чтобы ручка не вылезала
CLR_BG = TColor($00181818); // совпадает с CLR_PANEL
CLR_TRACK_EMPTY = TColor($00323232);
CLR_TRACK_FILL = TColor($00205018); // тёмно-зелёный, как тема
CLR_THUMB_NORM = TColor($00407840);
CLR_THUMB_HOT = TColor($0066BB66);
CLR_THUMB_DRAG = TColor($0099EE99);
CLR_THUMB_BDR = TColor($00122012);
THUMB_R = 6;
TRACK_H = 4;
PAD = THUMB_R + 2;
constructor TFlatSlider.Create(AOwner: TComponent);
begin
@@ -73,11 +79,32 @@ begin
FDragging := False;
FHot := False;
Cursor := crHandPoint;
// Тёмная тема по умолчанию
FClrBG := TColor($00181818);
FClrTrackEmpty := TColor($00323232);
FClrTrackFill := TColor($00205018);
FClrThumbNorm := TColor($00407840);
FClrThumbHot := TColor($0066BB66);
FClrThumbDrag := TColor($0099EE99);
FClrThumbBdr := TColor($00122012);
end;
procedure TFlatSlider.SetThemeColors(
ABG, ATrackEmpty, ATrackFill,
AThumbNorm, AThumbHot, AThumbDrag, AThumbBdr: TColor);
begin
FClrBG := ABG;
FClrTrackEmpty := ATrackEmpty;
FClrTrackFill := ATrackFill;
FClrThumbNorm := AThumbNorm;
FClrThumbHot := AThumbHot;
FClrThumbDrag := AThumbDrag;
FClrThumbBdr := AThumbBdr;
Invalidate;
end;
procedure TFlatSlider.SetPosition(V: Integer);
var
C: Integer;
var C: Integer;
begin
C := EnsureRange(V, FMin, FMax);
if FPosition = C then Exit;
@@ -102,10 +129,8 @@ begin
Invalidate;
end;
// Позиция значения → пиксельная X-координата ручки
function TFlatSlider.PosToX(APos: Integer): Integer;
var
Range, TrackW: Integer;
var Range, TrackW: Integer;
begin
Range := FMax - FMin;
TrackW := Width - PAD * 2;
@@ -116,10 +141,8 @@ begin
Result := PAD + Round((APos - FMin) / Range * TrackW);
end;
// Пиксельная X-координата → значение позиции
function TFlatSlider.XToPos(AX: Integer): Integer;
var
Range, TrackW, RawX: Integer;
var Range, TrackW, RawX: Integer;
begin
Range := FMax - FMin;
TrackW := Width - PAD * 2;
@@ -141,32 +164,29 @@ begin
CY := Height div 2;
TX := PosToX(FPosition);
Canvas.Brush.Color := CLR_BG;
Canvas.Brush.Color := FClrBG;
Canvas.Brush.Style := bsSolid;
Canvas.Pen.Style := psClear;
Canvas.FillRect(ClientRect);
// Рельс — весь
TR := Rect(PAD, CY - TRACK_H div 2,
Width - PAD, CY + (TRACK_H + 1) div 2);
Canvas.Brush.Color := CLR_TRACK_EMPTY;
Canvas.Brush.Color := FClrTrackEmpty;
Canvas.RoundRect(TR.Left, TR.Top, TR.Right, TR.Bottom, TRACK_H, TRACK_H);
// Рельс — заполненная часть
if not FReversed then begin FL := PAD; FR := TX; end
else begin FL := TX; FR := Width - PAD; end;
if FR > FL then
begin
Canvas.Brush.Color := CLR_TRACK_FILL;
Canvas.Brush.Color := FClrTrackFill;
Canvas.RoundRect(FL, TR.Top, FR, TR.Bottom, TRACK_H, TRACK_H);
end;
// Ручка — круг
if FDragging then ThC := CLR_THUMB_DRAG
else if FHot then ThC := CLR_THUMB_HOT
else ThC := CLR_THUMB_NORM;
if FDragging then ThC := FClrThumbDrag
else if FHot then ThC := FClrThumbHot
else ThC := FClrThumbNorm;
Canvas.Brush.Color := ThC;
Canvas.Pen.Color := CLR_THUMB_BDR;
Canvas.Pen.Color := FClrThumbBdr;
Canvas.Pen.Style := psSolid;
Canvas.Pen.Width := 1;
Canvas.Ellipse(TX - THUMB_R, CY - THUMB_R,
+168 -82
View File
@@ -22,7 +22,7 @@ unit MainForm;
interface
uses
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, FlatButton, FlatSlider, Forms, Controls, Graphics, Dialogs,
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, FlatButton, FlatSlider, AppTheme, Forms, Controls, Graphics, Dialogs,
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
LCLIntf, LCLType, GraphType,
HPSDRProtocol, HPSDRNetwork,
@@ -249,6 +249,7 @@ type
FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов
FDevConnected: Boolean; // True после первого подключения
FLightTheme: Boolean; // True = светлая тема
FCenterFreq: Double; // центр спектра = LO (DDC). При CTUN ON не меняется при кручении VFO
FSpanHz: Double;
FSampleRate: Integer; // текущий DDC sample rate (Hz)
@@ -345,6 +346,7 @@ type
// ---- Helpers ----
procedure BuildUI;
procedure ApplyDarkTheme;
procedure SetLightTheme(V: Boolean);
procedure StyleButton(B: TFlatButton; Active: Boolean = False);
procedure StyleSpanButton(B: TFlatButton; Active: Boolean = False);
procedure PositionSpanOverlayButtons;
@@ -845,6 +847,7 @@ begin
Result.ShowSpectrum := FShowSpectrum;
Result.ShowWaterfall := FShowWaterfall;
Result.DisplayFPS := FDisplayFPS;
Result.LightTheme := FLightTheme;
// Audio device names
Result.AudioOutDevice := FAudioOutDevName;
Result.AudioInDevice := FAudioInDevName;
@@ -1053,6 +1056,7 @@ begin
{$ENDIF}
end;
FSettings.LoadCATSettings(FCATLastGlobal);
FLightTheme := FSettings.LoadTheme;
InitCATEngine;
CATApplySettings(FCATLastGlobal);
FSMeterPeak := -130;
@@ -2125,12 +2129,20 @@ end;
// ===========================================================================
procedure TMainForm.ApplyDarkTheme;
var T: TAppTheme; i: Integer;
procedure DP(P: TPanel);
begin P.Color := T.Panel; P.Font.Color := T.Text; end;
procedure DL(L: TLabel; Dim: Boolean = True);
begin if Dim then L.Font.Color := T.TextDim else L.Font.Color := T.Text; end;
procedure DS(S: TFlatSlider);
begin
P.Color := CLR_PANEL; P.Font.Color := CLR_TEXT;
S.SetThemeColors(T.SliderBG, T.SliderTrackEmpty, T.SliderTrackFill,
T.SliderThumbNorm, T.SliderThumbHot, T.SliderThumbDrag, T.SliderThumbBdr);
end;
begin
Color := CLR_BG; Font.Color := CLR_TEXT;
if FLightTheme then T := LightTheme else T := DarkTheme;
Color := T.BG; Font.Color := T.Text;
DP(PanelToolbar); DP(PanelLeft);
DP(PanelTopVfoGroup); DP(PanelTopVfoA); DP(PanelTopVfoB);
DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons);
@@ -2138,38 +2150,107 @@ begin
DP(PanelRX); DP(PanelTX);
DP(PanelRight); DP(PanelSpanButtons);
PbSpectrum.Color := T.BG;
PbWaterfall.Color := T.BG;
if PanelSMeterRight <> nil then PanelSMeterRight.Color := T.Panel;
if PbSMeterRight <> nil then PbSMeterRight.Color := T.Panel;
PbFwdPower.Color := T.BG;
PbSWR.Color := T.BG;
PbSpectrum.Color := CLR_BG;
PbWaterfall.Color := CLR_BG;
if PanelSMeterRight <> nil then PanelSMeterRight.Color := CLR_PANEL;
if PbSMeterRight <> nil then PbSMeterRight.Color := CLR_PANEL;
PbFwdPower.Color := CLR_BG;
PbSWR.Color := CLR_BG;
// Метки
DL(LblAGCTop, False);
DL(LblDrv); DL(LblSpan);
LblFwdPwr.Font.Color := T.MeterOn;
LblSWRVal.Font.Color := T.TextDim;
// Кнопки тулбара
BtnDiscover.ClrNorm := T.TbDiscoverNorm;
BtnDiscover.ClrHot := T.TbDiscoverHot;
BtnDiscover.ClrActive := T.TbDiscoverHot;
BtnDiscover.ClrBorder := T.TbDiscoverBorder;
BtnDiscover.ClrText := T.TbDiscoverText;
BtnDiscover.ClrTextAct := T.TbDiscoverText;
BtnDiscover.Invalidate;
BtnStartStop.ClrNorm := T.TbStartNorm;
BtnStartStop.ClrBorder := T.TbStartBorder;
BtnStartStop.ClrText := T.TbStartText;
BtnStartStop.Invalidate;
// Обычные кнопки — все массивы
for i := 0 to BAND_COUNT-1 do StyleButton(BtnBand[i], BtnBand[i].Active);
for i := 0 to MODE_COUNT-1 do StyleButton(BtnMode[i], BtnMode[i].Active);
for i := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[i], BtnFilter[i].Active);
for i := 0 to 4 do StyleButton(BtnAGCMode[i],BtnAGCMode[i].Active);
StyleButton(BtnCTun, BtnCTun.Active);
StyleButton(BtnNR, BtnNR.Active);
StyleButton(BtnNB, BtnNB.Active);
StyleButton(BtnSNB, BtnSNB.Active);
StyleButton(BtnANF, BtnANF.Active);
StyleButton(BtnMute, BtnMute.Active);
StyleButton(BtnWfAGC, BtnWfAGC.Active);
StyleButton(BtnWfNF, BtnWfNF.Active);
StyleButton(BtnMOX, BtnMOX.Active);
// Span / VFO-copy / HidePanel
StyleSpanButton(BtnSpan48k, BtnSpan48k.Active);
StyleSpanButton(BtnSpan96k, BtnSpan96k.Active);
StyleSpanButton(BtnSpan192k, BtnSpan192k.Active);
StyleSpanButton(BtnSpan384k, BtnSpan384k.Active);
StyleSpanButton(BtnSpan768k, BtnSpan768k.Active);
StyleSpanButton(BtnSpan1536k, BtnSpan1536k.Active);
StyleSpanButton(BtnVfoACopyB, False);
StyleSpanButton(BtnVfoSwap, False);
StyleSpanButton(BtnVfoBCopyA, False);
if BtnHidePanel <> nil then StyleSpanButton(BtnHidePanel, False);
// Ползунки
DS(TrkAGC); DS(TrkVolume); DS(TrkDrive);
// Спектр
FSpecView.SetTheme(T);
// VFO display — вызывает UpdateVfoDisplay, который использует тему
UpdateVfoDisplay;
end;
procedure TMainForm.SetLightTheme(V: Boolean);
begin
if FLightTheme = V then Exit;
FLightTheme := V;
ApplyDarkTheme;
FSettings.SaveTheme(V);
FSettings.Save;
if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then
TSettingsForm(FSettingsForm).LoadLightTheme(V);
end;
procedure TMainForm.StyleButton(B: TFlatButton; Active: Boolean);
var T: TAppTheme;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
B.Active := Active;
B.ClrNorm := CLR_INACTIVE;
B.ClrActive := CLR_ACTIVE;
B.ClrHot := TColor($00282828);
if Active then B.ClrBorder := CLR_ACTIVE_BDR
else B.ClrBorder := TColor($00444444);
B.ClrText := CLR_TEXT;
B.ClrTextAct := TColor($0000FF88);
B.ClrNorm := T.BtnNorm;
B.ClrActive := T.BtnActive;
B.ClrHot := T.BtnHot;
if Active then B.ClrBorder := T.BtnBorderActive
else B.ClrBorder := T.BtnBorderNorm;
B.ClrText := T.BtnText;
B.ClrTextAct := T.BtnTextActive;
B.Font.Name := 'Courier New';
B.Font.Size := 8;
end;
procedure TMainForm.StyleSpanButton(B: TFlatButton; Active: Boolean);
var T: TAppTheme;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
B.Active := Active;
B.ClrNorm := TColor($00543F18);
B.ClrActive := TColor($00624B1D);
B.ClrHot := TColor($00624B1D);
B.ClrBorder := TColor($00685624);
B.ClrText := TColor($00D8E6EE);
B.ClrTextAct := TColor($00F0FAFF);
B.ClrNorm := T.SpanNorm;
B.ClrActive := T.SpanActive;
B.ClrHot := T.SpanActive;
B.ClrBorder := T.SpanBorder;
B.ClrText := T.SpanText;
B.ClrTextAct := T.SpanText;
B.Font.Name := 'Courier New';
B.Font.Size := 7;
end;
@@ -2246,7 +2327,10 @@ end;
procedure TMainForm.UpdateVfoDisplay;
const
TOP_VFO_FREQ_H = 47;
var T: TAppTheme;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
FreqDispA.Frequency := Round(FVfoA);
FreqDispB.Frequency := Round(FVfoB);
FreqDispA.FontSize := TOP_VFO_FONT;
@@ -2256,56 +2340,52 @@ begin
if FActiveVfo = 0 then
begin
FreqDispA.ColorNormal := CLR_FREQ;
FreqDispB.ColorNormal := CLR_FREQ_DIM;
LblVfoALabel.Font.Color := CLR_FREQ;
LblVfoBLabel.Font.Color := CLR_TEXTDIM;
// Активный VFO-A: зелёный акцент
FreqDispA.ColorNormal := T.FreqActive;
FreqDispB.ColorNormal := T.FreqDim;
LblVfoALabel.Font.Color := T.FreqActive;
LblVfoBLabel.Font.Color := T.TextDim;
if BtnTopVfoASelect <> nil then begin
BtnTopVfoASelect.ClrNorm := TColor($00101C10);
BtnTopVfoASelect.ClrActive := TColor($00182818);
BtnTopVfoASelect.ClrHot := TColor($00182818);
BtnTopVfoASelect.ClrBorder := TColor($004A7A4A);
BtnTopVfoASelect.ClrText := TColor($0060B860);
BtnTopVfoASelect.ClrTextAct := TColor($0040FF60);
BtnTopVfoASelect.ClrNorm := T.VfoSelActNorm;
BtnTopVfoASelect.ClrActive := T.VfoSelActNorm;
BtnTopVfoASelect.ClrHot := T.VfoSelActNorm;
BtnTopVfoASelect.ClrBorder := T.VfoSelActBorder;
BtnTopVfoASelect.ClrText := T.VfoSelActText;
BtnTopVfoASelect.ClrTextAct := T.VfoSelActTextAct;
BtnTopVfoASelect.Active := True;
BtnTopVfoASelect.Invalidate;
end;
// Неактивный VFO-B: приглушённый
if BtnTopVfoBSelect <> nil then begin
BtnTopVfoBSelect.ClrNorm := TColor($001A1A1A);
BtnTopVfoBSelect.ClrActive := TColor($00282828);
BtnTopVfoBSelect.ClrHot := TColor($00252525);
BtnTopVfoBSelect.ClrBorder := TColor($00404040);
BtnTopVfoBSelect.ClrText := CLR_TEXTDIM;
BtnTopVfoBSelect.ClrTextAct := CLR_TEXT;
BtnTopVfoBSelect.ClrNorm := T.VfoSelInaNorm;
BtnTopVfoBSelect.ClrActive := T.VfoSelInaNorm;
BtnTopVfoBSelect.ClrHot := T.VfoSelInaNorm;
BtnTopVfoBSelect.ClrBorder := T.VfoSelInaBorder;
BtnTopVfoBSelect.ClrText := T.VfoSelInaText;
BtnTopVfoBSelect.ClrTextAct := T.VfoSelInaTextAct;
BtnTopVfoBSelect.Active := False;
BtnTopVfoBSelect.Invalidate;
end;
end else begin
FreqDispA.ColorNormal := CLR_FREQ_DIM;
FreqDispB.ColorNormal := CLR_FREQ;
LblVfoALabel.Font.Color := CLR_TEXTDIM;
LblVfoBLabel.Font.Color := CLR_FREQ;
// Неактивный VFO-A: приглушённый
FreqDispA.ColorNormal := T.FreqDim;
FreqDispB.ColorNormal := T.FreqActive;
LblVfoALabel.Font.Color := T.TextDim;
LblVfoBLabel.Font.Color := T.FreqActive;
if BtnTopVfoASelect <> nil then begin
BtnTopVfoASelect.ClrNorm := TColor($001A1A1A);
BtnTopVfoASelect.ClrActive := TColor($00282828);
BtnTopVfoASelect.ClrHot := TColor($00252525);
BtnTopVfoASelect.ClrBorder := TColor($00404040);
BtnTopVfoASelect.ClrText := CLR_TEXTDIM;
BtnTopVfoASelect.ClrTextAct := CLR_TEXT;
BtnTopVfoASelect.ClrNorm := T.VfoSelInaNorm;
BtnTopVfoASelect.ClrActive := T.VfoSelInaNorm;
BtnTopVfoASelect.ClrHot := T.VfoSelInaNorm;
BtnTopVfoASelect.ClrBorder := T.VfoSelInaBorder;
BtnTopVfoASelect.ClrText := T.VfoSelInaText;
BtnTopVfoASelect.ClrTextAct := T.VfoSelInaTextAct;
BtnTopVfoASelect.Active := False;
BtnTopVfoASelect.Invalidate;
end;
// Активный VFO-B: зелёный акцент
if BtnTopVfoBSelect <> nil then begin
BtnTopVfoBSelect.ClrNorm := TColor($00101C10);
BtnTopVfoBSelect.ClrActive := TColor($00182818);
BtnTopVfoBSelect.ClrHot := TColor($00182818);
BtnTopVfoBSelect.ClrBorder := TColor($004A7A4A);
BtnTopVfoBSelect.ClrText := TColor($0060B860);
BtnTopVfoBSelect.ClrTextAct := TColor($0040FF60);
BtnTopVfoBSelect.ClrNorm := T.VfoSelActNorm;
BtnTopVfoBSelect.ClrActive := T.VfoSelActNorm;
BtnTopVfoBSelect.ClrHot := T.VfoSelActNorm;
BtnTopVfoBSelect.ClrBorder := T.VfoSelActBorder;
BtnTopVfoBSelect.ClrText := T.VfoSelActText;
BtnTopVfoBSelect.ClrTextAct := T.VfoSelActTextAct;
BtnTopVfoBSelect.Active := True;
BtnTopVfoBSelect.Invalidate;
end;
@@ -2314,22 +2394,22 @@ begin
begin
if not FSplitTxB then
begin
BtnTopVfoATX.ClrNorm := TColor($00000060);
BtnTopVfoATX.ClrHot := TColor($00000090);
BtnTopVfoATX.ClrActive := TColor($00000090);
BtnTopVfoATX.ClrBorder := TColor($001010B0);
BtnTopVfoATX.ClrText := TColor($00C8D8FF);
BtnTopVfoATX.ClrTextAct := TColor($00E8F0FF);
BtnTopVfoATX.ClrNorm := T.VfoTxActNorm;
BtnTopVfoATX.ClrHot := T.VfoTxActNorm;
BtnTopVfoATX.ClrActive := T.VfoTxActNorm;
BtnTopVfoATX.ClrBorder := T.VfoTxActBorder;
BtnTopVfoATX.ClrText := T.VfoTxActText;
BtnTopVfoATX.ClrTextAct := T.VfoTxActText;
BtnTopVfoATX.Active := True;
end
else
begin
BtnTopVfoATX.ClrNorm := TColor($00303030);
BtnTopVfoATX.ClrHot := TColor($00404040);
BtnTopVfoATX.ClrActive := TColor($00404040);
BtnTopVfoATX.ClrBorder := TColor($00505050);
BtnTopVfoATX.ClrText := CLR_TEXTDIM;
BtnTopVfoATX.ClrTextAct := CLR_TEXT;
BtnTopVfoATX.ClrNorm := T.VfoTxInaNorm;
BtnTopVfoATX.ClrHot := T.VfoTxInaNorm;
BtnTopVfoATX.ClrActive := T.VfoTxInaNorm;
BtnTopVfoATX.ClrBorder := T.VfoTxInaBorder;
BtnTopVfoATX.ClrText := T.VfoTxInaText;
BtnTopVfoATX.ClrTextAct := T.VfoTxInaText;
BtnTopVfoATX.Active := False;
end;
BtnTopVfoATX.Invalidate;
@@ -2338,22 +2418,22 @@ begin
begin
if FSplitTxB then
begin
BtnTopVfoBTX.ClrNorm := TColor($00000060);
BtnTopVfoBTX.ClrHot := TColor($00000090);
BtnTopVfoBTX.ClrActive := TColor($00000090);
BtnTopVfoBTX.ClrBorder := TColor($001010B0);
BtnTopVfoBTX.ClrText := TColor($00C8D8FF);
BtnTopVfoBTX.ClrTextAct := TColor($00E8F0FF);
BtnTopVfoBTX.ClrNorm := T.VfoTxActNorm;
BtnTopVfoBTX.ClrHot := T.VfoTxActNorm;
BtnTopVfoBTX.ClrActive := T.VfoTxActNorm;
BtnTopVfoBTX.ClrBorder := T.VfoTxActBorder;
BtnTopVfoBTX.ClrText := T.VfoTxActText;
BtnTopVfoBTX.ClrTextAct := T.VfoTxActText;
BtnTopVfoBTX.Active := True;
end
else
begin
BtnTopVfoBTX.ClrNorm := TColor($00303030);
BtnTopVfoBTX.ClrHot := TColor($00404040);
BtnTopVfoBTX.ClrActive := TColor($00404040);
BtnTopVfoBTX.ClrBorder := TColor($00505050);
BtnTopVfoBTX.ClrText := CLR_TEXTDIM;
BtnTopVfoBTX.ClrTextAct := CLR_TEXT;
BtnTopVfoBTX.ClrNorm := T.VfoTxInaNorm;
BtnTopVfoBTX.ClrHot := T.VfoTxInaNorm;
BtnTopVfoBTX.ClrActive := T.VfoTxInaNorm;
BtnTopVfoBTX.ClrBorder := T.VfoTxInaBorder;
BtnTopVfoBTX.ClrText := T.VfoTxInaText;
BtnTopVfoBTX.ClrTextAct := T.VfoTxInaText;
BtnTopVfoBTX.Active := False;
end;
BtnTopVfoBTX.Invalidate;
@@ -2907,6 +2987,8 @@ begin
if not Assigned(FDeviceDialog) then
FDeviceDialog := TDeviceDialog.Create(Self);
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
if FLightTheme then FDeviceDialog.SetTheme(LightTheme)
else FDeviceDialog.SetTheme(DarkTheme);
FDeviceDialog.ShowModal;
// Результат диалога не обрабатываем здесь — START сам спросит если нужно
end;
@@ -2986,6 +3068,8 @@ begin
if not Assigned(FDeviceDialog) then
FDeviceDialog := TDeviceDialog.Create(Self);
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
if FLightTheme then FDeviceDialog.SetTheme(LightTheme)
else FDeviceDialog.SetTheme(DarkTheme);
if FDeviceDialog.ShowModal <> mrOk then Exit;
if not FDeviceDialog.DialogResult.Accepted then Exit;
FPendingIP := FDeviceDialog.DialogResult.IPAddress;
@@ -5016,6 +5100,7 @@ begin
SF.OnFPSChange := ApplyFPS;
SF.OnPAChange := OnPASettingsChange;
SF.OnCATChange := OnCATSettingsChange;
SF.OnThemeChange := SetLightTheme;
end;
SF := TSettingsForm(FSettingsForm);
SF.LoadPASettings(FPAMaxPower, FPABandCal);
@@ -5032,6 +5117,7 @@ begin
SF.RefreshAudioDevices(FAudioOut, FAudioIn);
SF.LoadVisibility(FShowSpectrum, FShowWaterfall);
SF.LoadFPS(FDisplayFPS);
SF.LoadLightTheme(FLightTheme);
// Загружаем текущие значения
if FWDSPReady then
SF.LoadValues(
+23
View File
@@ -72,6 +72,8 @@ type
ShowWaterfall: Boolean; // True = draw waterfall on main form
// --- Display FPS ---
DisplayFPS: Integer; // spectrum/waterfall timer fps (1..100)
// --- Theme ---
LightTheme: Boolean; // False = dark (default), True = light
// --- PA (Power Amplifier) settings ---
PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200)
PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0
@@ -128,6 +130,9 @@ type
// CAT settings — не привязаны к MAC, хранятся в корне JSON (доступны до подключения)
procedure SaveCATSettings(const G: TGlobalSettings);
procedure LoadCATSettings(var G: TGlobalSettings);
// Тема — глобальная настройка, не привязана к устройству
procedure SaveTheme(ALightTheme: Boolean);
function LoadTheme: Boolean;
end;
implementation
@@ -413,6 +418,7 @@ begin
G.ShowSpectrum := JB(GObj,'show_spectrum',True);
G.ShowWaterfall := JB(GObj,'show_waterfall',True);
G.DisplayFPS := JI(GObj,'display_fps',60);
G.LightTheme := JB(GObj,'light_theme',False);
// PA settings
G.PAMaxPower := JD(GObj,'pa_max_power',100.0);
for i := 0 to CFG_BAND_COUNT-1 do
@@ -489,6 +495,7 @@ begin
JW(O,'show_spectrum',G.ShowSpectrum);
JW(O,'show_waterfall',G.ShowWaterfall);
JW(O,'display_fps',G.DisplayFPS);
JW(O,'light_theme',G.LightTheme);
// PA settings
JW(O,'pa_max_power',G.PAMaxPower);
for i := 0 to CFG_BAND_COUNT-1 do
@@ -621,4 +628,20 @@ begin
end;
end;
procedure TSettingsManager.SaveTheme(ALightTheme: Boolean);
var O: TJSONObject;
begin
O := EnsureObj(FRoot, 'preferences');
JW(O, 'light_theme', ALightTheme);
end;
function TSettingsManager.LoadTheme: Boolean;
var O: TJSONObject;
begin
Result := False;
if FRoot.Find('preferences') = nil then Exit;
O := EnsureObj(FRoot, 'preferences');
Result := JB(O, 'light_theme', False);
end;
end.
+101 -14
View File
@@ -27,7 +27,7 @@ uses
Classes, SysUtils, Math,
Forms, Controls, Graphics, Dialogs,
StdCtrls, ExtCtrls, Spin,
FlatButton, AudioOutput, AudioInput;
FlatButton, AudioOutput, AudioInput, AppTheme;
type
// PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
@@ -43,6 +43,7 @@ type
TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
TOnVisibilityChange = procedure(ShowSpectrum, ShowWaterfall: Boolean) of object;
TOnFPSChange = procedure(FPS: Integer) of object;
TOnThemeChange = procedure(LightTheme: Boolean) of object;
TOnCATChange = procedure(
const SerEnabled: array of Boolean;
const SerPort: array of string;
@@ -101,6 +102,7 @@ type
FChkShowSpectrum: TCheckBox;
FChkShowWaterfall: TCheckBox;
FCmbFPS: TComboBox;
FChkLightTheme: TCheckBox;
// ---- PA Settings tab controls ----
FEdMaxPower: TSpinEdit;
@@ -129,6 +131,7 @@ type
FOnAudioInDevChange: TOnAudioInDevChange;
FOnVisibilityChange: TOnVisibilityChange;
FOnFPSChange: TOnFPSChange;
FOnThemeChange: TOnThemeChange;
FLoading: Boolean;
@@ -138,7 +141,7 @@ type
procedure BuildRX1Tab;
procedure BuildPATab;
procedure BuildCATTab;
procedure ApplyTheme;
procedure ApplyTheme(const T: TAppTheme);
procedure NavClick(Sender: TObject);
procedure SelectPage(APage: TWinControl; ANav: TFlatButton);
@@ -167,6 +170,7 @@ type
procedure OnTXDevChange(Sender: TObject);
procedure OnVisibilityChkChange(Sender: TObject);
procedure OnFPSCmbChange(Sender: TObject);
procedure OnLightThemeChkChange(Sender: TObject);
procedure OnMaxPowerChange(Sender: TObject);
procedure OnBandCalChange(Sender: TObject);
procedure OnCATSerialChange(Sender: TObject);
@@ -203,6 +207,7 @@ type
procedure RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput);
procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean);
procedure LoadFPS(FPS: Integer);
procedure LoadLightTheme(ALight: Boolean);
procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double);
procedure LoadCATSettings(
const SerEnabled: array of Boolean;
@@ -219,6 +224,7 @@ type
property OnAudioInDevChange: TOnAudioInDevChange read FOnAudioInDevChange write FOnAudioInDevChange;
property OnVisibilityChange: TOnVisibilityChange read FOnVisibilityChange write FOnVisibilityChange;
property OnFPSChange: TOnFPSChange read FOnFPSChange write FOnFPSChange;
property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange;
end;
implementation
@@ -287,7 +293,7 @@ begin
FLoading := False;
FRXDevCount := 0;
BuildUI;
ApplyTheme;
ApplyTheme(DarkTheme);
end;
// ---------------------------------------------------------------------------
@@ -611,6 +617,18 @@ begin
for i := 0 to High(FPS_NAMES) do
FCmbFPS.Items.Add(FPS_NAMES[i]);
FCmbFPS.ItemIndex := 8;
Grp := MakeGroupPanel(FPageDisplay, 'Appearance', MARGIN, 292, 650, 68);
FChkLightTheme := TCheckBox.Create(Self);
FChkLightTheme.Parent := Grp;
FChkLightTheme.Caption := 'Light theme';
FChkLightTheme.SetBounds(PAD, R1, CHKW, 22);
FChkLightTheme.Checked := False;
FChkLightTheme.Font.Color := CLR_TEXT;
FChkLightTheme.Font.Name := UI_FONT;
FChkLightTheme.Font.Size := 9;
FChkLightTheme.OnChange := OnLightThemeChkChange;
end;
// ---------------------------------------------------------------------------
@@ -888,18 +906,66 @@ begin
end;
end;
procedure TSettingsForm.ApplyTheme;
procedure TSettingsForm.ApplyTheme(const T: TAppTheme);
procedure StyleBtn(B: TFlatButton);
begin
B.ClrNorm := T.Panel;
B.ClrBorder := T.Panel;
B.ClrHot := T.BG;
B.ClrActive := T.Panel;
B.ClrText := T.Text;
B.ClrTextAct := T.FreqActive;
B.Invalidate;
end;
procedure WalkControls(C: TWinControl);
var i: Integer; Ctrl: TControl;
begin
for i := 0 to C.ControlCount - 1 do
begin
Ctrl := C.Controls[i];
if Ctrl is TFlatButton then
StyleBtn(TFlatButton(Ctrl))
else if Ctrl is TScrollBox then
begin
TScrollBox(Ctrl).Color := T.BG;
WalkControls(TScrollBox(Ctrl));
end
else if Ctrl is TPanel then
begin
if TPanel(Ctrl).Height = 1 then
TPanel(Ctrl).Color := T.Border
else
TPanel(Ctrl).Color := T.Panel;
WalkControls(TPanel(Ctrl));
end
else if Ctrl is TLabel then
begin
if Ctrl.Font.Size >= 12 then
TLabel(Ctrl).Font.Color := T.Text
else if fsBold in Ctrl.Font.Style then
TLabel(Ctrl).Font.Color := T.FreqActive
else
TLabel(Ctrl).Font.Color := T.TextDim;
end
else if Ctrl is TCheckBox then
TCheckBox(Ctrl).Font.Color := T.Text
else if Ctrl is TListBox then
begin
TListBox(Ctrl).Color := T.BG;
TListBox(Ctrl).Font.Color := T.Text;
end
else if Ctrl is TWinControl then
WalkControls(TWinControl(Ctrl));
end;
end;
begin
Color := CLR_BG;
if Assigned(FNavPanel) then FNavPanel.Color := TColor($00151515);
if Assigned(FContentPanel) then FContentPanel.Color := CLR_BG;
if Assigned(FPageAudio) then FPageAudio.Color := CLR_BG;
if Assigned(FPageDisplay) then FPageDisplay.Color := CLR_BG;
if Assigned(FPageSpectrum) then FPageSpectrum.Color := CLR_BG;
if Assigned(FPageWaterfall) then FPageWaterfall.Color := CLR_BG;
if Assigned(FPagePA) then FPagePA.Color := CLR_BG;
if Assigned(FPageAdvanced) then FPageAdvanced.Color := CLR_BG;
if Assigned(FPageCAT) then FPageCAT.Color := CLR_BG;
Color := T.BG;
Font.Color := T.Text;
WalkControls(Self);
Invalidate;
end;
// ---------------------------------------------------------------------------
@@ -1265,6 +1331,15 @@ begin
FOnVisibilityChange(FChkShowSpectrum.Checked, FChkShowWaterfall.Checked);
end;
procedure TSettingsForm.OnLightThemeChkChange(Sender: TObject);
begin
if FLoading then Exit;
if FChkLightTheme.Checked then ApplyTheme(LightTheme)
else ApplyTheme(DarkTheme);
if Assigned(FOnThemeChange) then
FOnThemeChange(FChkLightTheme.Checked);
end;
procedure TSettingsForm.OnFPSCmbChange(Sender: TObject);
var
Idx: Integer;
@@ -1291,6 +1366,18 @@ begin
end;
end;
procedure TSettingsForm.LoadLightTheme(ALight: Boolean);
begin
FLoading := True;
try
FChkLightTheme.Checked := ALight;
finally
FLoading := False;
end;
if ALight then ApplyTheme(LightTheme)
else ApplyTheme(DarkTheme);
end;
procedure TSettingsForm.LoadPASettings(MaxPower: Double;
const BandCal: array of Double);
var i, n: Integer;
+69 -44
View File
@@ -18,7 +18,7 @@ interface
uses
Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
IntfGraphics, FPImage, LCLIntf, LCLType, GraphType;
IntfGraphics, FPImage, LCLIntf, LCLType, GraphType, AppTheme;
const
SV_CLR_BG = TColor($00101010);
@@ -44,6 +44,9 @@ type
FWfIntfImg: TLazIntfImage;
FWfPixels: array of LongWord;
FSpPts: array of TPoint;
// ── Тема ─────────────────────────────────────────────────────────────────
FTheme: TAppTheme;
FLightTheme: Boolean;
FSpPtsLen: Integer;
FSmBitmap: TBitmap;
FRulerLastFreq: Double;
@@ -197,6 +200,8 @@ type
// ── Утилиты ───────────────────────────────────────────────────────────────
procedure InvalidateGridCache;
// ── Тема ─────────────────────────────────────────────────────────────────
procedure SetTheme(const T: TAppTheme);
procedure InvalidateRulerCache;
procedure ResetSpectrumBuf;
procedure ResetWfAvgBuf;
@@ -360,6 +365,8 @@ begin
FWaterfallDirty := True;
FLastSMeter := -130; FSMeterPeak := -130; FSMeterMin := -130;
FLastFwdW := 0; FLastSWR := 1;
FLightTheme := False;
FTheme := DarkTheme;
ResetSpectrumBuf;
end;
@@ -439,14 +446,17 @@ begin
Alpha := Round((1.0 - Sqr((Row - YMin) / Max(1.0, H - YMin - 1.0))) * 200);
if Alpha <= 0 then Continue;
if Alpha > 200 then Alpha := 200;
GG := Byte(Alpha shr 1); RG := Byte(Alpha);
GG := Byte(FTheme.SpecGradB * Alpha div 200);
RG := Byte(FTheme.SpecGradG * Alpha div 200);
RowPtr := FSpecGradImg.GetDataLineStart(Row);
if RowPtr = nil then Continue;
for Col := 0 to W - 1 do
begin
if SpPts[Col].Y <= Row then
begin
RowPtr[0] := GG; RowPtr[1] := RG; RowPtr[2] := 0;
RowPtr[0] := GG;
RowPtr[1] := RG;
RowPtr[2] := Byte(FTheme.SpecGradR * Alpha div 200);
if BPPg >= 4 then RowPtr[3] := Byte(Alpha);
end else begin
RowPtr[0] := 0; RowPtr[1] := 0; RowPtr[2] := 0;
@@ -495,7 +505,7 @@ procedure TSpectrumView.SetSpectrumBitmapSize(W, H: Integer);
begin
if (W <= 0) or (H <= 0) then Exit;
FSpectrumBitmap.SetSize(W, H);
FSpectrumBitmap.Canvas.Brush.Color := SV_CLR_BG;
FSpectrumBitmap.Canvas.Brush.Color := FTheme.BG;
FSpectrumBitmap.Canvas.FillRect(Rect(0, 0, W, H));
FreeAndNil(FSpecGradImg);
InvalidateGridCache;
@@ -532,6 +542,16 @@ begin
FSpectrumDirty := True;
end;
procedure TSpectrumView.SetTheme(const T: TAppTheme);
begin
FTheme := T;
FLightTheme := T.BG > TColor($00808080); // светлая тема если фон светлее 50%
FreeAndNil(FSpecGradImg); // пересчитать градиент
InvalidateGridCache;
InvalidateRulerCache;
FWaterfallDirty := True;
end;
procedure TSpectrumView.InvalidateRulerCache;
begin
FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; FRulerLastSpan := -1.0;
@@ -606,20 +626,20 @@ begin
FGridBitmapW := W; FGridBitmapH := H;
FGridBitmap.SetSize(W, H);
GC := FGridBitmap.Canvas;
PaintVerticalGradient(GC, W, H, TColor($0020160B), TColor($006C4F1F));
PaintVerticalGradient(GC, W, H, FTheme.SpecGradTop, FTheme.SpecGradBot);
LabelBandW := 28;
GC.Pen.Style := psClear; GC.Brush.Style := bsSolid;
GC.Brush.Color := TColor($00543F18);
GC.Brush.Color := FTheme.SpecLabelBand;
GC.FillRect(Rect(0, 0, LabelBandW, H));
GC.Pen.Style := psSolid;
GC.Font.Size := 7; GC.Font.Name := 'Courier New';
GC.Font.Color := TColor($00A8C9D9);
GC.Font.Color := FTheme.SpecLabelText;
GC.Brush.Style := bsClear;
dB := DBmax - FSpecGridStep;
while dB >= DBmin do
begin
Yp := Round((DBmax - dB) * InvRange * H);
GC.Pen.Color := TColor($00685624); GC.Pen.Width := 1;
GC.Pen.Color := FTheme.SpecGrid; GC.Pen.Width := 1;
GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp);
GC.TextOut(1, Yp - 9, Format('%4.0f', [dB]));
dB := dB - FSpecGridStep;
@@ -627,7 +647,7 @@ begin
for i := 0 to 8 do
begin
GX := ScaleX(i, 8, W);
GC.Pen.Color := TColor($00685624);
GC.Pen.Color := FTheme.SpecGrid;
GC.MoveTo(GX, 0); GC.LineTo(GX, H);
end;
end;
@@ -646,7 +666,7 @@ begin
end;
X1 := Max(0, Min(W-1, VfoX + Round(Lo_Hz / FSpanHz * W)));
X2 := Max(0, Min(W-1, VfoX + Round(Hi_Hz / FSpanHz * W)));
C.Brush.Color := TColor($006A4E22); C.Brush.Style := bsSolid; C.Pen.Style := psClear;
C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid; C.Pen.Style := psClear;
if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H));
C.Pen.Style := psSolid;
@@ -656,26 +676,26 @@ begin
AGCy := Round((DBmax - FAGCThresh) * InvRange * H);
if (AGCy >= 0) and (AGCy < H) then
begin
C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash;
C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash;
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6;
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6;
C.TextOut(4, AGCy - 9, 'AGC T');
end;
AGCHangY := Round((DBmax - FAGCHangLevel) * InvRange * H);
if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then
begin
C.Pen.Color := TColor($00FFCC00); C.Pen.Width := 1; C.Pen.Style := psDot;
C.Pen.Color := FTheme.SpecAgcHangColor; C.Pen.Width := 1; C.Pen.Style := psDot;
C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY);
C.Pen.Style := psSolid; C.Font.Color := TColor($00FFCC00); C.Font.Size := 6;
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcHangColor; C.Font.Size := 6;
C.TextOut(4, AGCHangY + 2, 'AGC H');
end;
end else begin
AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H);
if (AGCy >= 0) and (AGCy < H) then
begin
C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash;
C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash;
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6;
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6;
C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop]));
end;
end;
@@ -700,16 +720,16 @@ begin
DrawSpectrumGradient(FSpPts, W, H);
// ── 6. Линия спектра ──────────────────────────────────────────────────────
C.Pen.Style := psSolid; C.Pen.Color := TColor($0040FF80); C.Pen.Width := 1;
C.Pen.Style := psSolid; C.Pen.Color := FTheme.SpecLine; C.Pen.Width := 1;
C.Polyline(Slice(FSpPts, W));
// ── 7. Края фильтра + VFO ─────────────────────────────────────────────────
C.Pen.Color := TColor($0040FFCC); C.Pen.Width := 1; C.Pen.Style := psSolid;
C.Pen.Color := FTheme.SpecFilterEdge; C.Pen.Width := 1; C.Pen.Style := psSolid;
C.MoveTo(X1, 0); C.LineTo(X1, H);
C.MoveTo(X2, 0); C.LineTo(X2, H);
C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 2;
C.Pen.Color := FTheme.SpecVfoCursor; C.Pen.Width := 2;
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12);
C.Brush.Color := TColor($0000AAFF); C.Brush.Style := bsSolid; C.Pen.Width := 1;
C.Brush.Color := FTheme.SpecVfoCursor; C.Brush.Style := bsSolid; C.Pen.Width := 1;
C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]);
if FMarkerActive then DrawMarkerLine(C, W, H);
@@ -862,8 +882,8 @@ begin
if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then
FRulerBitmap.SetSize(W, H);
C := FRulerBitmap.Canvas;
PaintVerticalGradient(C, W, H, TColor($00372A12), TColor($00705A2A));
C.Pen.Color := TColor($00685624); C.Pen.Width := 1;
PaintVerticalGradient(C, W, H, FTheme.RulerGradTop, FTheme.RulerGradBot);
C.Pen.Color := FTheme.RulerBorder; C.Pen.Width := 1;
C.MoveTo(0, 0); C.LineTo(W, 0);
C.MoveTo(0, H-1); C.LineTo(W, H-1);
C.Font.Name := 'Courier New'; C.Font.Size := 7;
@@ -872,18 +892,18 @@ begin
for i := 0 to 8 do
begin
X := ScaleX(i, 8, W);
C.Pen.Color := TColor($00685624);
C.Pen.Color := FTheme.RulerBorder;
C.MoveTo(X, 0); C.LineTo(X, H div 2);
FreqHz := FreqStart + i * FSpanHz / 8;
Lbl := Format('%.3f', [FreqHz / 1e6]);
TW := C.TextWidth(Lbl);
C.Font.Color := TColor($00C8D8E2);
C.Font.Color := FTheme.RulerText;
C.TextOut(X - TW div 2, H div 2 - 1, Lbl);
end;
VfoX := Round((ActiveVfoFreq - FCenterFreq + FSpanHz/2) / FSpanHz * W);
if (VfoX >= 0) and (VfoX < W) then
begin
C.Pen.Color := TColor($0000FF80); C.Pen.Width := 2;
C.Pen.Color := FTheme.RulerVfo; C.Pen.Width := 2;
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 1); C.Pen.Width := 1;
end;
end;
@@ -896,7 +916,7 @@ procedure TSpectrumView.DrawBarMeter(ACanvas: TCanvas; R: TRect;
Value, MaxVal: Double; BarColor: TColor);
var Pct, BarW: Integer;
begin
ACanvas.Brush.Color := TColor($00080808);
ACanvas.Brush.Color := FTheme.SMeterBG;
ACanvas.FillRect(R);
if MaxVal > 0 then
Pct := Round(Max(0.0, Min(1.0, Value / MaxVal)) * (R.Right - R.Left - 2))
@@ -918,22 +938,13 @@ const
DBM_LABELS: array[0..5] of string = ('-120','-100','-80','-60','-40','-20');
S_MARKS_DBM: array[0..7] of Double = (-121,-109,-97,-85,-73,-53,-33,-13);
S_MARKS_LBL: array[0..7] of string = ('S1','S3','S5','S7','S9','+20','+40','+60');
CLR_SMETER_BG = TColor($00181818);
CLR_BAR_GREEN = TColor($00091F00);
CLR_BAR_OVER = TColor($00091528);
CLR_PEAK_MARKER = TColor($00F8F8FF);
CLR_TICK_GREEN = TColor($00708070);
CLR_TICK_WHITE = TColor($00E0E0DC);
CLR_TICK_BLUE = TColor($0070A090);
CLR_LABEL_DBM = TColor($0096D8DC);
CLR_LABEL_S = TColor($00F0F0F0);
CLR_SCALE_DBM = TColor($00D0DCDC);
CLR_SCALE_S_GREEN = TColor($00E0E0E0);
CLR_SCALE_S_BLUE = TColor($0090D090);
CLR_BDR = TColor($00303028);
LEFT_INFO_MIN = 62; RIGHT_PAD = 8; LABEL_PAD = 4;
TICK_LONG = 5; TICK_MED = 3;
var
CLR_SMETER_BG, CLR_BAR_GREEN, CLR_BAR_OVER: TColor;
CLR_PEAK_MARKER, CLR_TICK_GREEN, CLR_TICK_WHITE, CLR_TICK_BLUE: TColor;
CLR_LABEL_DBM, CLR_LABEL_S, CLR_SCALE_DBM: TColor;
CLR_SCALE_S_GREEN, CLR_SCALE_S_BLUE, CLR_BDR: TColor;
W, H, BX, BW, LeftInfo: Integer;
Y_BAR_TOP, Y_BAR_BOT: Integer;
BarEnd, PeakLeft, PeakRight, S9X, OvrX: Integer;
@@ -960,6 +971,20 @@ var
end;
begin
CLR_SMETER_BG := FTheme.SMeterBG;
CLR_BAR_GREEN := FTheme.SMeterBarGreen;
CLR_BAR_OVER := FTheme.SMeterBarOver;
CLR_PEAK_MARKER := FTheme.SMeterPeakMarker;
CLR_TICK_GREEN := FTheme.SMeterTickGreen;
CLR_TICK_WHITE := FTheme.SMeterTickWhite;
CLR_TICK_BLUE := FTheme.SMeterTickBlue;
CLR_LABEL_DBM := FTheme.SMeterLabelDbm;
CLR_LABEL_S := FTheme.SMeterLabelS;
CLR_SCALE_DBM := FTheme.SMeterScaleDbm;
CLR_SCALE_S_GREEN:= FTheme.SMeterScaleSGreen;
CLR_SCALE_S_BLUE := FTheme.SMeterScaleSBlue;
CLR_BDR := FTheme.SMeterBdr;
W := R.Right - R.Left; H := R.Bottom - R.Top;
ZX1 := 0; ZX2 := 0; ZY1 := 0; ZY2 := 0;
if (W < 80) or (H < 16) then Exit;
@@ -997,9 +1022,9 @@ begin
ACanvas.Brush.Color := CLR_SMETER_BG; ACanvas.Brush.Style := bsSolid;
ACanvas.Pen.Style := psClear; ACanvas.FillRect(R); ACanvas.Pen.Style := psSolid;
FillBar(BX, Y_BAR_TOP, S9X, Y_BAR_BOT, TColor($00061006));
FillBar(S9X, Y_BAR_TOP, OvrX, Y_BAR_BOT, TColor($000A1006));
FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, TColor($00060A10));
FillBar(BX, Y_BAR_TOP, S9X, Y_BAR_BOT, CLR_BAR_GREEN);
FillBar(S9X, Y_BAR_TOP, OvrX, Y_BAR_BOT, CLR_BAR_GREEN);
FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, CLR_BAR_OVER);
BarEnd := DBtoX(Value);
PeakLeft := DBtoX(MinVal);
@@ -1029,7 +1054,7 @@ begin
ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR;
ACanvas.Rectangle(LblZoneL, Y_BAR_TOP, BX + BW, Y_BAR_BOT);
// разделитель между зоной надписей и шкалой
ACanvas.Pen.Color := TColor($00404040);
ACanvas.Pen.Color := FTheme.SMeterDivider;
ACanvas.MoveTo(BX, Y_BAR_TOP + 1); ACanvas.LineTo(BX, Y_BAR_BOT - 1);
// --- Шкальные тики и надписи ---
@@ -1066,7 +1091,7 @@ begin
// "-85.3" над "dBm", снизу прижаты к верхнему краю полосы
ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM;
ACanvas.TextOut(LblZoneL + 4, Y_BAR_TOP - TH10 - 2, LblDbm);
ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Font.Color := TColor($00507070);
ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Font.Color := CLR_TICK_BLUE;
ACanvas.TextOut(LblZoneL + 4 + TWDbm + 2, Y_BAR_TOP - TH6 - 4, 'dBm');
// S-значение под полосой, левый край у LblZoneL+4
ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_S;
+4
View File
@@ -115,6 +115,10 @@
<Filename Value="FlatButton.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="AppTheme.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="FlatSlider.pas"/>
<IsPartOfProject Value="True"/>