diff --git a/RulerView.pas b/RulerView.pas
new file mode 100644
index 0000000..bc32868
--- /dev/null
+++ b/RulerView.pas
@@ -0,0 +1,248 @@
+unit RulerView;
+
+{
+ RulerView.pas — рендеринг линейки частот под спектром/водопадом.
+ TRulerView — изолированный класс, не зависит от SpectrumView.
+}
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+interface
+
+uses
+ Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
+ LCLIntf, LCLType, AppTheme;
+
+type
+ TRulerView = class
+ private
+ FRulerBitmap: TBitmap;
+ FRulerLastFreq: Double;
+ FRulerLastVfo: Double;
+ FRulerLastSpan: Double;
+ FVfoA: Double;
+ FVfoB: Double;
+ FActiveVfo: Integer;
+ FCenterFreq: Double;
+ FSpanHz: Double;
+ FFMGridStepHz: Double;
+ FTheme: TAppTheme;
+ FPbRuler: TPaintBox;
+
+ function ActiveVfoFreq: Double;
+ function ScaleX(X, Total, Width: Integer): Integer;
+ public
+ constructor Create;
+ destructor Destroy; override;
+
+ property VfoA: Double read FVfoA write FVfoA;
+ property VfoB: Double read FVfoB write FVfoB;
+ property ActiveVfo: Integer read FActiveVfo write FActiveVfo;
+ property CenterFreq: Double read FCenterFreq write FCenterFreq;
+ property SpanHz: Double read FSpanHz write FSpanHz;
+ property FMGridStepHz: Double read FFMGridStepHz write FFMGridStepHz;
+ property PbRuler: TPaintBox write FPbRuler;
+
+ procedure DrawRuler;
+ procedure PaintRuler(Sender: TObject);
+ procedure SetRulerSize(W, H: Integer);
+ procedure InvalidateRulerCache;
+ function NeedsRulerRedraw: Boolean;
+ procedure SetTheme(const T: TAppTheme);
+ end;
+
+implementation
+
+// ════════════════════════════════════════════════════════════════════════════
+// Вспомогательные функции (приватные копии)
+// ════════════════════════════════════════════════════════════════════════════
+
+function FormatFreqRuler(Hz: Double): string;
+var Mhz: Int64; KHz, Rest: Integer;
+begin
+ Mhz := Trunc(Hz / 1000000);
+ KHz := Trunc((Hz - Mhz * 1000000) / 1000);
+ Rest := Trunc(Hz) mod 1000;
+ Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
+end;
+
+function MixColorBGRR(C1, C2: TColor; T: Double): TColor;
+var
+ B1, G1, R1, B2, G2, R2, B, G, R: Integer;
+begin
+ if T < 0.0 then T := 0.0 else if T > 1.0 then T := 1.0;
+ B1 := (C1 shr 16) and $FF; G1 := (C1 shr 8) and $FF; R1 := C1 and $FF;
+ B2 := (C2 shr 16) and $FF; G2 := (C2 shr 8) and $FF; R2 := C2 and $FF;
+ B := Round(B1 + (B2-B1)*T); G := Round(G1 + (G2-G1)*T); R := Round(R1 + (R2-R1)*T);
+ Result := TColor((B shl 16) or (G shl 8) or R);
+end;
+
+procedure PaintVerticalGradientR(C: TCanvas; W, H: Integer; TopColor, BottomColor: TColor);
+var Y: Integer; T: Double;
+begin
+ if (W <= 0) or (H <= 0) then Exit;
+ C.Pen.Style := psClear; C.Brush.Style := bsSolid;
+ for Y := 0 to H - 1 do
+ begin
+ T := Y / Max(1, H - 1);
+ C.Brush.Color := MixColorBGRR(TopColor, BottomColor, T);
+ C.FillRect(Rect(0, Y, W, Y + 1));
+ end;
+ C.Pen.Style := psSolid;
+end;
+
+// ════════════════════════════════════════════════════════════════════════════
+// TRulerView
+// ════════════════════════════════════════════════════════════════════════════
+
+constructor TRulerView.Create;
+begin
+ inherited Create;
+ FRulerBitmap := TBitmap.Create;
+ FRulerLastFreq := -1.0;
+ FRulerLastVfo := -1.0;
+ FRulerLastSpan := -1.0;
+ FTheme := DarkTheme;
+ FSpanHz := 192000;
+end;
+
+destructor TRulerView.Destroy;
+begin
+ FRulerBitmap.Free;
+ inherited;
+end;
+
+function TRulerView.ActiveVfoFreq: Double;
+begin
+ if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB;
+end;
+
+function TRulerView.ScaleX(X, Total, Width: Integer): Integer;
+begin
+ if Total = 0 then Result := 0
+ else Result := Round(X / Total * Width);
+end;
+
+procedure TRulerView.SetTheme(const T: TAppTheme);
+begin
+ FTheme := T;
+ InvalidateRulerCache;
+end;
+
+procedure TRulerView.SetRulerSize(W, H: Integer);
+begin
+ if (W <= 0) or (H <= 0) then Exit;
+ FRulerBitmap.SetSize(W, H);
+ InvalidateRulerCache;
+end;
+
+procedure TRulerView.InvalidateRulerCache;
+begin
+ FRulerLastFreq := -1.0;
+ FRulerLastVfo := -1.0;
+ FRulerLastSpan := -1.0;
+end;
+
+function TRulerView.NeedsRulerRedraw: Boolean;
+begin
+ Result := (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or
+ (Abs(ActiveVfoFreq - FRulerLastVfo) >= 0.5) or
+ (Abs(FSpanHz - FRulerLastSpan) >= 1.0);
+end;
+
+// ────────────────────────────────────────────────────────────────────────────
+// DrawRuler
+// ────────────────────────────────────────────────────────────────────────────
+
+procedure TRulerView.DrawRuler;
+var
+ C: TCanvas; W, H, i, X: Integer;
+ FreqStart, FreqHz, GridLine, pixPerStep: Double; VfoX: Integer;
+ Lbl: string; TW: Integer;
+ N, labelMult: Integer;
+begin
+ if FPbRuler = nil then Exit;
+ W := FPbRuler.Width; H := FPbRuler.Height;
+ if (W <= 0) or (H <= 0) then Exit;
+ if (Abs(FCenterFreq - FRulerLastFreq) < 0.5) and
+ (Abs(ActiveVfoFreq - FRulerLastVfo) < 0.5) and
+ (Abs(FSpanHz - FRulerLastSpan) < 1.0) then Exit;
+ FRulerLastFreq := FCenterFreq; FRulerLastVfo := ActiveVfoFreq; FRulerLastSpan := FSpanHz;
+ if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then
+ FRulerBitmap.SetSize(W, H);
+ C := FRulerBitmap.Canvas;
+ PaintVerticalGradientR(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;
+ C.Brush.Style := bsClear;
+ FreqStart := FCenterFreq - FSpanHz / 2;
+ if FFMGridStepHz > 0 then
+ begin
+ pixPerStep := W * FFMGridStepHz / FSpanHz;
+ if pixPerStep >= 1.0 then
+ labelMult := Max(1, Ceil((C.TextWidth('000.000') + 6) / pixPerStep))
+ else
+ labelMult := MaxInt;
+ N := Ceil(FreqStart / FFMGridStepHz);
+ GridLine := N * FFMGridStepHz;
+ while GridLine <= FCenterFreq + FSpanHz / 2 + 0.5 do
+ begin
+ X := Round((GridLine - FreqStart) / FSpanHz * W);
+ if (X >= 0) and (X < W) then
+ begin
+ C.Pen.Color := FTheme.RulerBorder;
+ if (N mod labelMult) = 0 then
+ begin
+ C.MoveTo(X, 0); C.LineTo(X, H div 2);
+ Lbl := Format('%.3f', [GridLine / 1e6]);
+ TW := C.TextWidth(Lbl);
+ C.Font.Color := FTheme.RulerText;
+ C.TextOut(X - TW div 2, H div 2 - 1, Lbl);
+ end else
+ begin
+ C.MoveTo(X, 0); C.LineTo(X, H div 4);
+ end;
+ end;
+ Inc(N);
+ GridLine := GridLine + FFMGridStepHz;
+ end;
+ end else
+ begin
+ for i := 0 to 8 do
+ begin
+ X := ScaleX(i, 8, W);
+ 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 := FTheme.RulerText;
+ C.TextOut(X - TW div 2, H div 2 - 1, Lbl);
+ end;
+ end;
+ VfoX := Round((ActiveVfoFreq - FCenterFreq + FSpanHz/2) / FSpanHz * W);
+ if (VfoX >= 0) and (VfoX < W) then
+ begin
+ C.Pen.Color := FTheme.RulerVfo; C.Pen.Width := 2;
+ C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 1); C.Pen.Width := 1;
+ end;
+end;
+
+// ────────────────────────────────────────────────────────────────────────────
+// PaintRuler
+// ────────────────────────────────────────────────────────────────────────────
+
+procedure TRulerView.PaintRuler(Sender: TObject);
+var PB: TPaintBox;
+begin
+ PB := TPaintBox(Sender);
+ DrawRuler;
+ if (FRulerBitmap.Width > 0) and (FRulerBitmap.Height > 0) then
+ PB.Canvas.Draw(0, 0, FRulerBitmap);
+end;
+
+end.
diff --git a/SMeterView.pas b/SMeterView.pas
new file mode 100644
index 0000000..8bf1b63
--- /dev/null
+++ b/SMeterView.pas
@@ -0,0 +1,507 @@
+unit SMeterView;
+
+{
+ SMeterView.pas — рендеринг S-метра, TX-метра и индикаторов мощности/SWR.
+ TSMeterView — изолированный класс без зависимостей на SpectrumView.
+}
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+interface
+
+uses
+ Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
+ IntfGraphics, FPImage, LCLType, GraphType, AppTheme;
+
+const
+ SM_CLR_METER_ON = TColor($0000CC44);
+ SM_CLR_AMBER = TColor($0000AAFF);
+
+type
+ TSMeterView = class
+ private
+ FSmBitmap: TBitmap;
+ FTheme: TAppTheme;
+ FLastSMeter: Double;
+ FSMeterPeak: Double;
+ FSMeterMin: Double;
+ FLastFwdW: Double;
+ FLastSWR: Double;
+ FPAMaxPower: Double;
+ FTransmitting: Boolean;
+ FPbSMeterRight: TPaintBox;
+
+ procedure DrawSMeterWide(ACanvas: TCanvas; R: TRect;
+ Value, Peak, MinVal: Double;
+ out ZX1, ZX2, ZY1, ZY2: Integer);
+ procedure DrawSMeterZone(ACanvas: TCanvas; R: TRect;
+ X1, X2, Y1, Y2: Integer);
+ procedure DrawTXMeter(ACanvas: TCanvas; R: TRect);
+ public
+ constructor Create;
+ destructor Destroy; override;
+
+ property LastSMeter: Double read FLastSMeter write FLastSMeter;
+ property SMeterPeak: Double read FSMeterPeak write FSMeterPeak;
+ property SMeterMin: Double read FSMeterMin write FSMeterMin;
+ property LastFwdW: Double read FLastFwdW write FLastFwdW;
+ property LastSWR: Double read FLastSWR write FLastSWR;
+ property PAMaxPower: Double read FPAMaxPower write FPAMaxPower;
+ property Transmitting: Boolean read FTransmitting write FTransmitting;
+ property PbSMeterRight: TPaintBox write FPbSMeterRight;
+
+ procedure PaintSMeterRight(Sender: TObject);
+ procedure SetTheme(const T: TAppTheme);
+ end;
+
+implementation
+
+// ════════════════════════════════════════════════════════════════════════════
+// TSMeterView
+// ════════════════════════════════════════════════════════════════════════════
+
+constructor TSMeterView.Create;
+begin
+ inherited Create;
+ FSmBitmap := nil;
+ FTheme := DarkTheme;
+ FLastSMeter := -130;
+ FSMeterPeak := -130;
+ FSMeterMin := -130;
+ FLastFwdW := 0;
+ FLastSWR := 1;
+ FPAMaxPower := 100.0;
+ FTransmitting := False;
+end;
+
+destructor TSMeterView.Destroy;
+begin
+ FreeAndNil(FSmBitmap);
+ inherited;
+end;
+
+procedure TSMeterView.SetTheme(const T: TAppTheme);
+begin
+ FTheme := T;
+end;
+
+// ────────────────────────────────────────────────────────────────────────────
+// DrawSMeterWide
+// ────────────────────────────────────────────────────────────────────────────
+
+procedure TSMeterView.DrawSMeterWide(ACanvas: TCanvas; R: TRect;
+ Value, Peak, MinVal: Double; out ZX1, ZX2, ZY1, ZY2: Integer);
+const
+ DB_MIN = -127.0; DB_MAX = -13.0; DB_S9 = -73.0; DB_OVR = -43.0;
+ DBM_MARKS: array[0..5] of Double = (-120,-100,-80,-60,-40,-20);
+ 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');
+ 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;
+ i, X, TW: Integer; Lbl: string; SNum, Over: Integer;
+ LblDbm, LblS: string;
+ TWDbm, LblZoneL, TH10, TH6, MaxTWDbm, MaxTWS, LabelNeed: Integer;
+
+ function DBtoX(dB: Double): Integer; inline;
+ begin
+ Result := BX + Round((dB - DB_MIN) / (DB_MAX - DB_MIN) * BW);
+ if Result < BX then Result := BX;
+ if Result > BX + BW then Result := BX + BW;
+ end;
+
+ procedure VLine(X2, Y1, Y2: Integer; C: TColor);
+ begin ACanvas.Pen.Color := C; ACanvas.MoveTo(X2, Y1); ACanvas.LineTo(X2, Y2); end;
+
+ procedure FillBar(X1, Y1, X2, Y2: Integer; C: TColor);
+ begin
+ ACanvas.Brush.Color := C; ACanvas.Brush.Style := bsSolid;
+ ACanvas.Pen.Style := psClear;
+ if X2 > X1 then ACanvas.FillRect(Rect(X1, Y1, X2, Y2));
+ ACanvas.Pen.Style := psSolid;
+ 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;
+
+ LblDbm := Format('%.1f', [Value]);
+ if Value >= -73.0 then
+ begin
+ Over := Round(Value - (-73.0));
+ if Over < 5 then LblS := 'S9'
+ else begin Over := ((Over + 5) div 10) * 10; if Over = 0 then Over := 10; LblS := Format('S9+%d', [Over]); end;
+ end else if Value <= -121.0 then LblS := 'S1'
+ else begin
+ LblS := 'S1';
+ for SNum := 1 to 8 do
+ if Value >= (-121.0 + (SNum - 1) * 6.0) then LblS := 'S' + IntToStr(SNum);
+ end;
+
+ ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Style := [fsBold];
+ ACanvas.Font.Size := 10; TWDbm := ACanvas.TextWidth(LblDbm);
+ MaxTWDbm := ACanvas.TextWidth('-120.0');
+ MaxTWS := ACanvas.TextWidth('S9+60');
+ ACanvas.Font.Size := 6; ACanvas.Font.Style := [];
+ LabelNeed := Max(MaxTWDbm + 2 + ACanvas.TextWidth('dBm '), MaxTWS) + LABEL_PAD * 2;
+ LeftInfo := Max(LEFT_INFO_MIN, LabelNeed + 4);
+ BX := R.Left + LeftInfo; BW := W - LeftInfo - RIGHT_PAD;
+ if BW < 10 then Exit;
+ LblZoneL := BX - LabelNeed;
+
+ Y_BAR_TOP := R.Top + H * 44 div 100;
+ Y_BAR_BOT := R.Top + H * 58 div 100;
+ S9X := DBtoX(DB_S9); OvrX := DBtoX(DB_OVR);
+
+ 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, 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);
+ PeakRight := DBtoX(Peak);
+ ZX1 := Max(BX + 1, Min(PeakLeft, PeakRight));
+ ZX2 := Min(BX + BW - 1, Max(PeakLeft, PeakRight));
+ ZY1 := Y_BAR_TOP + 1; ZY2 := Y_BAR_BOT - 1;
+
+ if BarEnd > BX then
+ begin
+ if BarEnd <= S9X then
+ FillBar(BX, Y_BAR_TOP+1, BarEnd, Y_BAR_BOT-1, CLR_BAR_GREEN)
+ else begin
+ FillBar(BX, Y_BAR_TOP+1, S9X, Y_BAR_BOT-1, CLR_BAR_GREEN);
+ FillBar(S9X, Y_BAR_TOP+1, BarEnd, Y_BAR_BOT-1, CLR_BAR_OVER);
+ end;
+ end;
+ if (PeakRight > BX) and (PeakRight <= BX + BW) then
+ begin
+ ACanvas.Pen.Color := CLR_PEAK_MARKER; ACanvas.Pen.Width := 2;
+ ACanvas.MoveTo(PeakRight, Y_BAR_TOP - (Y_BAR_BOT - Y_BAR_TOP));
+ ACanvas.LineTo(PeakRight, Y_BAR_BOT + (Y_BAR_BOT - Y_BAR_TOP));
+ ACanvas.Pen.Width := 1;
+ end;
+
+ ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR;
+ ACanvas.Rectangle(LblZoneL, Y_BAR_TOP, BX + BW, Y_BAR_BOT);
+ ACanvas.Pen.Color := FTheme.SMeterDivider;
+ ACanvas.MoveTo(BX, Y_BAR_TOP + 1); ACanvas.LineTo(BX, Y_BAR_BOT - 1);
+
+ ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Brush.Style := bsClear;
+ for i := 0 to High(DBM_MARKS) do
+ begin
+ X := DBtoX(DBM_MARKS[i]);
+ VLine(X, Y_BAR_TOP - TICK_LONG, Y_BAR_TOP - 1, CLR_TICK_WHITE);
+ Lbl := DBM_LABELS[i]; TW := ACanvas.TextWidth(Lbl);
+ ACanvas.Font.Color := CLR_SCALE_DBM;
+ ACanvas.TextOut(X - TW div 2, Y_BAR_TOP - TICK_LONG - ACanvas.TextHeight(Lbl) - 1, Lbl);
+ end;
+ i := -125;
+ while i < -13 do
+ begin
+ X := DBtoX(i); VLine(X, Y_BAR_TOP - TICK_MED, Y_BAR_TOP - 1, CLR_TICK_GREEN);
+ Inc(i, 5);
+ end;
+ for i := 0 to High(S_MARKS_DBM) do
+ begin
+ X := DBtoX(S_MARKS_DBM[i]);
+ if S_MARKS_DBM[i] >= DB_S9 then VLine(X, Y_BAR_BOT+1, Y_BAR_BOT+TICK_LONG, CLR_TICK_BLUE)
+ else VLine(X, Y_BAR_BOT+1, Y_BAR_BOT+TICK_LONG, CLR_TICK_GREEN);
+ Lbl := S_MARKS_LBL[i]; TW := ACanvas.TextWidth(Lbl);
+ if S_MARKS_DBM[i] >= DB_S9 then ACanvas.Font.Color := CLR_SCALE_S_BLUE
+ else ACanvas.Font.Color := CLR_SCALE_S_GREEN;
+ ACanvas.TextOut(X - TW div 2, Y_BAR_BOT + TICK_LONG + 1, Lbl);
+ end;
+
+ ACanvas.Brush.Style := bsClear;
+ ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; TH10 := ACanvas.TextHeight('0');
+ ACanvas.Font.Size := 6; ACanvas.Font.Style := []; TH6 := ACanvas.TextHeight('0');
+ 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 := CLR_TICK_BLUE;
+ ACanvas.TextOut(LblZoneL + 4 + TWDbm + 2, Y_BAR_TOP - TH6 - 4, 'dBm');
+ ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_S;
+ ACanvas.TextOut(LblZoneL + 4, Y_BAR_BOT + 2, LblS);
+end;
+
+// ────────────────────────────────────────────────────────────────────────────
+// DrawSMeterZone
+// ────────────────────────────────────────────────────────────────────────────
+
+procedure TSMeterView.DrawSMeterZone(ACanvas: TCanvas; R: TRect;
+ X1, X2, Y1, Y2: Integer);
+const ALPHA = 16384;
+var Img: TLazIntfImage; FC: TFPColor; IX, IY: Integer;
+begin
+ if (X2 <= X1) or (FSmBitmap = nil) then Exit;
+ Img := FSmBitmap.CreateIntfImage;
+ try
+ for IY := Y1 to Y2 - 1 do
+ for IX := X1 to X2 - 1 do
+ begin
+ FC := Img.Colors[IX, IY];
+ FC.Red := Min(65535, FC.Red + ALPHA);
+ FC.Green := Min(65535, FC.Green + ALPHA);
+ FC.Blue := Min(65535, FC.Blue + ALPHA);
+ Img.Colors[IX, IY] := FC;
+ end;
+ FSmBitmap.LoadFromIntfImage(Img);
+ finally Img.Free; end;
+end;
+
+// ────────────────────────────────────────────────────────────────────────────
+// DrawTXMeter
+// ────────────────────────────────────────────────────────────────────────────
+
+procedure TSMeterView.DrawTXMeter(ACanvas: TCanvas; R: TRect);
+const
+ SWR_HIGH_THRESH = 2.5;
+ SWR_MAX = 10.0;
+ LEFT_INFO_MIN = 62; RIGHT_PAD = 8; LABEL_PAD = 4;
+ TICK_LONG = 5; TICK_MED = 3;
+ PWR_MARKS: array[0..4] of Double = (0.0, 0.25, 0.5, 0.75, 1.0);
+ SWR_MARKS_VAL: array[0..3] of Double = (1.0, 2.5, 5.0, 10.0);
+ SWR_MARKS_LBL: array[0..3] of string = ('1', '2.5', '5', '10');
+var
+ CLR_SMETER_BG, CLR_BAR_GREEN, CLR_BAR_OVER: TColor;
+ CLR_TICK_GREEN, CLR_TICK_WHITE, CLR_TICK_BLUE: TColor;
+ CLR_LABEL_DBM, CLR_LABEL_S: TColor;
+ CLR_SCALE_DBM, CLR_SCALE_S_GREEN, CLR_SCALE_S_BLUE, CLR_BDR: TColor;
+ CLR_SWR_HIGH: TColor;
+ W, H, BX, BW, LeftInfo: Integer;
+ Y_BAR_TOP, Y_BAR_BOT, Y_BAR_MID: Integer;
+ PY1, PY2, SY1, SY2: Integer;
+ PBarEnd, SBarEnd, S_WarnX: Integer;
+ LblPwr, LblSWR: string;
+ TWPwr, LblZoneL, TH10, TH6, MaxTWPwr, MaxTWSWR, LabelNeed: Integer;
+ i, X, TW: Integer;
+ Lbl: string;
+ SWRHigh: Boolean;
+ PwrPct: Double;
+
+ function PwrToX(Pct: Double): Integer; inline;
+ begin
+ Result := BX + Round(Max(0.0, Min(1.0, Pct)) * BW);
+ if Result < BX then Result := BX;
+ if Result > BX + BW then Result := BX + BW;
+ end;
+
+ function SWRToX(SWR: Double): Integer; inline;
+ begin
+ Result := BX + Round(Max(0.0, Min(1.0, (SWR - 1.0) / (SWR_MAX - 1.0))) * BW);
+ if Result < BX then Result := BX;
+ if Result > BX + BW then Result := BX + BW;
+ end;
+
+ procedure VLine(X2, Y1, Y2: Integer; C: TColor);
+ begin ACanvas.Pen.Color := C; ACanvas.MoveTo(X2, Y1); ACanvas.LineTo(X2, Y2); end;
+
+ procedure FillBar(X1, Y1, X2, Y2: Integer; C: TColor);
+ begin
+ ACanvas.Brush.Color := C; ACanvas.Brush.Style := bsSolid;
+ ACanvas.Pen.Style := psClear;
+ if X2 > X1 then ACanvas.FillRect(Rect(X1, Y1, X2, Y2));
+ ACanvas.Pen.Style := psSolid;
+ end;
+
+begin
+ CLR_SMETER_BG := FTheme.SMeterBG;
+ CLR_BAR_GREEN := FTheme.SMeterBarGreen;
+ CLR_BAR_OVER := FTheme.SMeterBarOver;
+ 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;
+ CLR_SWR_HIGH := TColor($000000CC);
+
+ W := R.Right - R.Left; H := R.Bottom - R.Top;
+ if (W < 80) or (H < 16) then Exit;
+
+ SWRHigh := FLastSWR > SWR_HIGH_THRESH;
+ LblPwr := Format('%.0fW', [FLastFwdW]);
+ LblSWR := Format('SWR:%.1f', [FLastSWR]);
+
+ ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Style := [fsBold];
+ ACanvas.Font.Size := 10;
+ TWPwr := ACanvas.TextWidth(LblPwr);
+ MaxTWPwr := ACanvas.TextWidth(Format('%.0fW', [FPAMaxPower]));
+ MaxTWSWR := ACanvas.TextWidth('SWR:10.0');
+ ACanvas.Font.Size := 6; ACanvas.Font.Style := [];
+ LabelNeed := Max(MaxTWPwr, MaxTWSWR) + LABEL_PAD * 2;
+ LeftInfo := Max(LEFT_INFO_MIN, LabelNeed + 4);
+ BX := R.Left + LeftInfo; BW := W - LeftInfo - RIGHT_PAD;
+ if BW < 10 then Exit;
+ LblZoneL := BX - LabelNeed;
+
+ Y_BAR_TOP := R.Top + H * 44 div 100;
+ Y_BAR_BOT := R.Top + H * 58 div 100;
+ Y_BAR_MID := (Y_BAR_TOP + Y_BAR_BOT) div 2;
+
+ PY1 := Y_BAR_TOP;
+ PY2 := Y_BAR_MID - 1;
+ SY1 := Y_BAR_MID + 1;
+ SY2 := Y_BAR_BOT;
+
+ S_WarnX := SWRToX(SWR_HIGH_THRESH);
+
+ ACanvas.Brush.Color := CLR_SMETER_BG; ACanvas.Brush.Style := bsSolid;
+ ACanvas.Pen.Style := psClear; ACanvas.FillRect(R); ACanvas.Pen.Style := psSolid;
+
+ FillBar(BX, PY1, BX + BW, PY2, CLR_BAR_GREEN);
+ FillBar(BX, SY1, S_WarnX, SY2, CLR_BAR_GREEN);
+ FillBar(S_WarnX, SY1, BX + BW, SY2, CLR_BAR_OVER);
+
+ if FPAMaxPower > 0 then PwrPct := FLastFwdW / FPAMaxPower else PwrPct := 0;
+ PBarEnd := PwrToX(PwrPct);
+ FillBar(BX, PY1 + 1, PBarEnd, PY2 - 1, SM_CLR_METER_ON);
+
+ SBarEnd := SWRToX(FLastSWR);
+ if not SWRHigh then
+ FillBar(BX, SY1 + 1, SBarEnd, SY2 - 1, SM_CLR_AMBER)
+ else
+ begin
+ FillBar(BX, SY1 + 1, Min(S_WarnX, SBarEnd), SY2 - 1, SM_CLR_AMBER);
+ if SBarEnd > S_WarnX then
+ FillBar(S_WarnX, SY1 + 1, SBarEnd, SY2 - 1, CLR_SWR_HIGH);
+ end;
+
+ VLine(S_WarnX, SY1, SY2, FTheme.SMeterDivider);
+
+ ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR;
+ ACanvas.Rectangle(LblZoneL, PY1, BX + BW, PY2);
+ if SWRHigh then ACanvas.Pen.Color := CLR_SWR_HIGH
+ else ACanvas.Pen.Color := CLR_BDR;
+ ACanvas.Rectangle(LblZoneL, SY1, BX + BW, SY2);
+
+ ACanvas.Pen.Color := FTheme.SMeterDivider;
+ ACanvas.MoveTo(BX, PY1 + 1); ACanvas.LineTo(BX, PY2 - 1);
+ ACanvas.MoveTo(BX, SY1 + 1); ACanvas.LineTo(BX, SY2 - 1);
+
+ ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Brush.Style := bsClear;
+
+ i := 5;
+ while i < 100 do
+ begin
+ if (i mod 25) <> 0 then
+ begin
+ X := PwrToX(i / 100.0);
+ VLine(X, PY1 - TICK_MED, PY1 - 1, CLR_TICK_GREEN);
+ end;
+ Inc(i, 5);
+ end;
+
+ for i := 0 to 4 do
+ begin
+ X := PwrToX(PWR_MARKS[i]);
+ VLine(X, PY1 - TICK_LONG, PY1 - 1, CLR_TICK_WHITE);
+ Lbl := Format('%.0f', [PWR_MARKS[i] * FPAMaxPower]);
+ TW := ACanvas.TextWidth(Lbl);
+ ACanvas.Font.Color := CLR_SCALE_DBM;
+ ACanvas.TextOut(X - TW div 2, PY1 - TICK_LONG - ACanvas.TextHeight(Lbl) - 1, Lbl);
+ end;
+
+ for i := 2 to 9 do
+ begin
+ if i = 5 then Continue;
+ X := SWRToX(i);
+ if i >= 3 then VLine(X, SY2 + 1, SY2 + TICK_MED, CLR_TICK_BLUE)
+ else VLine(X, SY2 + 1, SY2 + TICK_MED, CLR_TICK_GREEN);
+ end;
+
+ for i := 0 to 3 do
+ begin
+ X := SWRToX(SWR_MARKS_VAL[i]);
+ if SWR_MARKS_VAL[i] >= SWR_HIGH_THRESH then
+ VLine(X, SY2 + 1, SY2 + TICK_LONG, CLR_TICK_BLUE)
+ else
+ VLine(X, SY2 + 1, SY2 + TICK_LONG, CLR_TICK_GREEN);
+ Lbl := SWR_MARKS_LBL[i]; TW := ACanvas.TextWidth(Lbl);
+ if SWR_MARKS_VAL[i] >= SWR_HIGH_THRESH then
+ begin
+ if SWRHigh then ACanvas.Font.Color := CLR_SWR_HIGH
+ else ACanvas.Font.Color := CLR_SCALE_S_BLUE;
+ end else
+ ACanvas.Font.Color := CLR_SCALE_S_GREEN;
+ ACanvas.TextOut(X - TW div 2, SY2 + TICK_LONG + 1, Lbl);
+ end;
+
+ ACanvas.Brush.Style := bsClear;
+ ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; TH10 := ACanvas.TextHeight('0');
+ ACanvas.Font.Size := 6; ACanvas.Font.Style := []; TH6 := ACanvas.TextHeight('0');
+
+ ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM;
+ ACanvas.TextOut(LblZoneL + 4, PY1 - TH10 - 2, LblPwr);
+
+ ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold];
+ if SWRHigh then ACanvas.Font.Color := CLR_SWR_HIGH
+ else ACanvas.Font.Color := CLR_LABEL_S;
+ ACanvas.TextOut(LblZoneL + 4, SY2 + 2, LblSWR);
+
+ if SWRHigh then
+ begin
+ ACanvas.Font.Size := 6; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_SWR_HIGH;
+ ACanvas.TextOut(LblZoneL + 4 + TWPwr + 2, PY1 - TH6 - 4, 'SWR High');
+ end;
+end;
+
+// ────────────────────────────────────────────────────────────────────────────
+// PaintSMeterRight
+// ────────────────────────────────────────────────────────────────────────────
+
+procedure TSMeterView.PaintSMeterRight(Sender: TObject);
+var PB: TPaintBox; W, H, ZX1, ZX2, ZY1, ZY2: Integer;
+begin
+ PB := TPaintBox(Sender);
+ W := PB.Width; H := PB.Height;
+ if (W <= 0) or (H <= 0) then Exit;
+ if (FSmBitmap = nil) or (FSmBitmap.Width <> W) or (FSmBitmap.Height <> H) then
+ begin
+ FreeAndNil(FSmBitmap);
+ FSmBitmap := TBitmap.Create;
+ FSmBitmap.SetSize(W, H);
+ end;
+ if FTransmitting then
+ begin
+ DrawTXMeter(FSmBitmap.Canvas, Rect(0, 0, W, H));
+ end else
+ begin
+ DrawSMeterWide(FSmBitmap.Canvas, Rect(0, 0, W, H),
+ FLastSMeter, FSMeterPeak, FSMeterMin,
+ ZX1, ZX2, ZY1, ZY2);
+ DrawSMeterZone(FSmBitmap.Canvas, Rect(0, 0, W, H), ZX1, ZX2, ZY1, ZY2);
+ end;
+ PB.Canvas.Draw(0, 0, FSmBitmap);
+end;
+
+end.
diff --git a/SpectrumView.pas b/SpectrumView.pas
index 01b3ebe..e54a291 100644
--- a/SpectrumView.pas
+++ b/SpectrumView.pas
@@ -1,13 +1,14 @@
unit SpectrumView;
{
- SpectrumView.pas — рендеринг спектра, водопада и S-метра.
+ SpectrumView.pas — рендеринг спектрограммы.
- TSpectrumView инкапсулирует всё рисование: спектр, водопад, линейка частот,
- S-метр, индикаторы мощности/SWR. MainForm создаёт один экземпляр, передаёт
- ссылки на TPaintBox после BuildUI, обновляет свойства при изменении состояния.
+ TSpectrumView инкапсулирует рисование спектра и агрегирует дочерние
+ компоненты: TWaterfallView, TSMeterView, TRulerView. MainForm создаёт один
+ экземпляр TSpectrumView и работает с ним единообразно: свойства/методы
+ водопада, S-метра и линейки делегируются в соответствующие sub-view.
- Зависимости: нет обратной зависимости на MainForm (нет circular dep).
+ Зависимости: нет обратной зависимости на MainForm.
}
{$IFDEF FPC}
@@ -19,44 +20,30 @@ interface
uses
Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
IntfGraphics, FPImage, LCLIntf, LCLType, GraphType, AppTheme,
- AlertOverlay;
+ AlertOverlay,
+ WaterfallView, SMeterView, RulerView;
const
- SV_CLR_BG = TColor($00101010);
- SV_CLR_BORDER = TColor($00303030);
- SV_CLR_METER_ON = TColor($0000CC44);
- SV_CLR_AMBER = TColor($0000AAFF);
+ SV_CLR_BG = TColor($00101010);
type
TSpectrumView = class
private
// ── Off-screen bitmaps ────────────────────────────────────────────────────
FSpectrumBitmap: TBitmap;
- FWaterfallBitmap: TBitmap; // размер-placeholder для DrawWaterfall
- FWfBitmap: TBitmap;
- FWfBitmapW: Integer;
- FWfBitmapH: Integer;
FGridBitmap: TBitmap;
FGridBitmapW: Integer;
FGridBitmapH: Integer;
FFMGridLastCenter: Double;
FFMGridLastStepHz: Double;
FFMGridLastSpan: Double;
- FRulerBitmap: TBitmap;
FSpecGradImg: TLazIntfImage;
FSpecGradBmp: TBitmap;
- FWfIntfImg: TLazIntfImage;
- FWfPixels: array of LongWord;
FSpPts: array of TPoint;
+ FSpPtsLen: Integer;
// ── Тема ─────────────────────────────────────────────────────────────────
FTheme: TAppTheme;
FLightTheme: Boolean;
- FSpPtsLen: Integer;
- FSmBitmap: TBitmap;
- FRulerLastFreq: Double;
- FRulerLastVfo: Double;
- FRulerLastSpan: Double;
-
// ── Radio state ───────────────────────────────────────────────────────────
FVfoA: Double;
FVfoB: Double;
@@ -69,96 +56,96 @@ type
FAGCThresh: Double;
FAGCHangLevel: Double;
FWDSPReady: Boolean;
-
- // ── Waterfall settings ────────────────────────────────────────────────────
- FWfAGCEnabled: Boolean;
- FWfNFEnabled: Boolean;
- FWfManualHigh: Double;
- FWfManualLow: Double;
- FWfAGCOffset: Double;
- FWfHigh: Double;
- FWfLow: Double;
-
// ── Display settings ──────────────────────────────────────────────────────
FSpecRefLevel: Double;
FSpecRange: Double;
FSpecGridStep: Double;
FFMGridStepHz: Double;
-
- // ── TX overlay (Thetis-style: при TX данные в FSpectrumBuf приходят
- // от TX-анализатора @ FTXSpanHz Гц вокруг FTXFreq, и должны быть
- // отрисованы в RX-координатах (FCenterFreq/FSpanHz), чтобы линейка
- // частот совпадала с RX и сигнал стоял на правильной частоте даже
- // при включённом CTUN. Если RX rate ≠ TX rate, видна только пересечение.
+ // ── TX overlay ────────────────────────────────────────────────────────────
FTXMode: Boolean;
- FTXFreq: Double; // центр TX-сигнала (Hz)
- FTXSpanHz: Double; // ширина TX-окна (192000 для Hermes/Saturn)
- FTXVfoIndex: Integer; // 0=A, 1=B — какой VFO привязан к TX (для split)
- // FTXOverlay — независимый от FTXMode флаг: «нарисовать TX-фильтр поверх
- // данных». DUP-режим: FTXMode=False (данные RX), но FTXOverlay=True (TX-полоса
- // красным сверху). Без DUP при TX оба флага True. Без TX оба False.
+ FTXFreq: Double;
+ FTXSpanHz: Double;
+ FTXVfoIndex: Integer;
FTXOverlay: Boolean;
-
// ── ADC overload overlay ─────────────────────────────────────────────────
FADCOverloadVisible: Boolean;
-
// ── Marker ────────────────────────────────────────────────────────────────
FMarkerActive: Boolean;
FMarkerX: Integer;
-
- // ── Meter values ──────────────────────────────────────────────────────────
- FLastSMeter: Double;
- FSMeterPeak: Double;
- FSMeterMin: Double;
- FLastFwdW: Double;
- FLastSWR: Double;
- FPAMaxPower: Double;
- FTransmitting: Boolean;
-
- // ── Буферы данных (пишутся из DSP-потока) ────────────────────────────────
+ // ── Spectrum buffer ───────────────────────────────────────────────────────
FSpectrumBuf: array[0..1023] of Single;
- FWaterfallBuf: array[0..1023] of Single;
FSpectrumBufCount: Integer;
- FWaterfallBufCount: Integer;
FSpectrumDirty: Boolean;
- FWaterfallDirty: Boolean;
- FWfFrameCounter: Integer;
- FWfFrameInterval: Integer;
-
- // ── Ссылки на TPaintBox (устанавливаются из MainForm после BuildUI) ───────
- FPbSpectrum: TPaintBox;
- FPbWaterfall: TPaintBox;
- FPbRuler: TPaintBox;
- FPbSMeterRight: TPaintBox;
- FPbFwdPower: TPaintBox;
- FPbSWR: TPaintBox;
+ // ── Sub-views ─────────────────────────────────────────────────────────────
+ FWaterfall: TWaterfallView;
+ FSMeter: TSMeterView;
+ FRuler: TRulerView;
// ── Приватные методы рендеринга ───────────────────────────────────────────
function ActiveVfoFreq: Double;
function ScaleX(X, Total, Width: Integer): Integer;
procedure SetFMGridStepHz(V: Double);
+ procedure SetCenterFreq(V: Double);
+ procedure SetSpanHz(V: Double);
+ procedure SetVfoA(V: Double);
+ procedure SetVfoB(V: Double);
+ procedure SetActiveVfo(V: Integer);
+ procedure SetTXMode(V: Boolean);
+ procedure SetTXFreq(V: Double);
+ procedure SetTXSpanHz(V: Double);
+ procedure SetMarkerActive(V: Boolean);
+ procedure SetMarkerX(V: Integer);
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
- // Альфа-blending вертикальной полосы поверх FSpectrumBitmap (pf32bit, BGRA).
- // Используется для прозрачной заливки полосы фильтра разными цветами
- // (RX-зелёный / TX-красный) поверх градиента сетки.
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
procedure DrawADCOverloadOverlay(C: TCanvas; W, H: Integer);
- // Считает X1/X2 для полосы фильтра вокруг указанной частоты VFO, c учётом
- // текущего режима (LSB/USB/DSB и пр.) и FFilterBW.
procedure CalcFilterBandX(VfoFreq: Double; W: Integer;
out X1, X2, VfoX: Integer);
+ function GetWaterfallDirty: Boolean;
+ procedure SetWaterfallDirty(V: Boolean);
+ // Waterfall sub-view delegating accessors
+ function GetWfAGCEnabled: Boolean;
+ procedure SetWfAGCEnabled(V: Boolean);
+ function GetWfNFEnabled: Boolean;
+ procedure SetWfNFEnabled(V: Boolean);
+ function GetWfManualHigh: Double;
+ procedure SetWfManualHigh(V: Double);
+ function GetWfManualLow: Double;
+ procedure SetWfManualLow(V: Double);
+ function GetWfAGCOffset: Double;
+ procedure SetWfAGCOffset(V: Double);
+ function GetWfFrameInterval: Integer;
+ procedure SetWfFrameInterval(V: Integer);
+ procedure SetPbWaterfall(V: TPaintBox);
+ // SMeter sub-view delegating accessors
+ function GetLastSMeter: Double;
+ procedure SetLastSMeter(V: Double);
+ function GetSMeterPeak: Double;
+ procedure SetSMeterPeak(V: Double);
+ function GetSMeterMin: Double;
+ procedure SetSMeterMin(V: Double);
+ function GetLastFwdW: Double;
+ procedure SetLastFwdW(V: Double);
+ function GetLastSWR: Double;
+ procedure SetLastSWR(V: Double);
+ function GetPAMaxPower: Double;
+ procedure SetPAMaxPower(V: Double);
+ function GetTransmitting: Boolean;
+ procedure SetTransmitting(V: Boolean);
+ procedure SetPbSMeterRight(V: TPaintBox);
+ // Ruler sub-view delegating accessor
+ procedure SetPbRuler(V: TPaintBox);
public
constructor Create;
destructor Destroy; override;
// ── Состояние радио ───────────────────────────────────────────────────────
- property VfoA: Double read FVfoA write FVfoA;
- property VfoB: Double read FVfoB write FVfoB;
- property ActiveVfo: Integer read FActiveVfo write FActiveVfo;
- property CenterFreq: Double read FCenterFreq write FCenterFreq;
- property SpanHz: Double read FSpanHz write FSpanHz;
+ property VfoA: Double read FVfoA write SetVfoA;
+ property VfoB: Double read FVfoB write SetVfoB;
+ property ActiveVfo: Integer read FActiveVfo write SetActiveVfo;
+ property CenterFreq: Double read FCenterFreq write SetCenterFreq;
+ property SpanHz: Double read FSpanHz write SetSpanHz;
property Mode: Integer read FMode write FMode;
property FilterBW: Integer read FFilterBW write FFilterBW;
property AGCTop: Integer read FAGCTop write FAGCTop;
@@ -167,11 +154,11 @@ type
property WDSPReady: Boolean read FWDSPReady write FWDSPReady;
// ── Водопад ───────────────────────────────────────────────────────────────
- property WfAGCEnabled: Boolean read FWfAGCEnabled write FWfAGCEnabled;
- property WfNFEnabled: Boolean read FWfNFEnabled write FWfNFEnabled;
- property WfManualHigh: Double read FWfManualHigh write FWfManualHigh;
- property WfManualLow: Double read FWfManualLow write FWfManualLow;
- property WfAGCOffset: Double read FWfAGCOffset write FWfAGCOffset;
+ property WfAGCEnabled: Boolean read GetWfAGCEnabled write SetWfAGCEnabled;
+ property WfNFEnabled: Boolean read GetWfNFEnabled write SetWfNFEnabled;
+ property WfManualHigh: Double read GetWfManualHigh write SetWfManualHigh;
+ property WfManualLow: Double read GetWfManualLow write SetWfManualLow;
+ property WfAGCOffset: Double read GetWfAGCOffset write SetWfAGCOffset;
// ── Отображение ───────────────────────────────────────────────────────────
property SpecRefLevel: Double read FSpecRefLevel write FSpecRefLevel;
@@ -180,42 +167,38 @@ type
property FMGridStepHz: Double read FFMGridStepHz write SetFMGridStepHz;
// ── TX overlay ────────────────────────────────────────────────────────────
- property TXMode: Boolean read FTXMode write FTXMode;
- property TXFreq: Double read FTXFreq write FTXFreq;
- property TXSpanHz: Double read FTXSpanHz write FTXSpanHz;
+ property TXMode: Boolean read FTXMode write SetTXMode;
+ property TXFreq: Double read FTXFreq write SetTXFreq;
+ property TXSpanHz: Double read FTXSpanHz write SetTXSpanHz;
property TXVfoIndex: Integer read FTXVfoIndex write FTXVfoIndex;
property TXOverlay: Boolean read FTXOverlay write FTXOverlay;
- // Test overlay for ADC overload; later this will be driven from HP Status.
property ADCOverloadVisible: Boolean read FADCOverloadVisible write FADCOverloadVisible;
// ── Маркер ────────────────────────────────────────────────────────────────
- property MarkerActive: Boolean read FMarkerActive write FMarkerActive;
- property MarkerX: Integer read FMarkerX write FMarkerX;
+ property MarkerActive: Boolean read FMarkerActive write SetMarkerActive;
+ property MarkerX: Integer read FMarkerX write SetMarkerX;
// ── S-метр ────────────────────────────────────────────────────────────────
- property LastSMeter: Double read FLastSMeter write FLastSMeter;
- property SMeterPeak: Double read FSMeterPeak write FSMeterPeak;
- property SMeterMin: Double read FSMeterMin write FSMeterMin;
- property LastFwdW: Double read FLastFwdW write FLastFwdW;
- property LastSWR: Double read FLastSWR write FLastSWR;
- property PAMaxPower: Double read FPAMaxPower write FPAMaxPower;
- property Transmitting: Boolean read FTransmitting write FTransmitting;
+ property LastSMeter: Double read GetLastSMeter write SetLastSMeter;
+ property SMeterPeak: Double read GetSMeterPeak write SetSMeterPeak;
+ property SMeterMin: Double read GetSMeterMin write SetSMeterMin;
+ property LastFwdW: Double read GetLastFwdW write SetLastFwdW;
+ property LastSWR: Double read GetLastSWR write SetLastSWR;
+ property PAMaxPower: Double read GetPAMaxPower write SetPAMaxPower;
+ property Transmitting: Boolean read GetTransmitting write SetTransmitting;
// ── Флаги обновления ──────────────────────────────────────────────────────
property SpectrumDirty: Boolean read FSpectrumDirty write FSpectrumDirty;
- property WaterfallDirty: Boolean read FWaterfallDirty write FWaterfallDirty;
- property WfFrameInterval: Integer read FWfFrameInterval write FWfFrameInterval;
+ property WaterfallDirty: Boolean read GetWaterfallDirty write SetWaterfallDirty;
+ property WfFrameInterval: Integer read GetWfFrameInterval write SetWfFrameInterval;
// ── Ссылки на PaintBox ────────────────────────────────────────────────────
- property PbSpectrum: TPaintBox write FPbSpectrum;
- property PbWaterfall: TPaintBox write FPbWaterfall;
- property PbRuler: TPaintBox write FPbRuler;
- property PbSMeterRight: TPaintBox write FPbSMeterRight;
- property PbFwdPower: TPaintBox write FPbFwdPower;
- property PbSWR: TPaintBox write FPbSWR;
+ property PbWaterfall: TPaintBox write SetPbWaterfall;
+ property PbRuler: TPaintBox write SetPbRuler;
+ property PbSMeterRight: TPaintBox write SetPbSMeterRight;
- // ── Данные от DSP (вызываются из DSP-потока) ──────────────────────────────
+ // ── Данные от DSP ─────────────────────────────────────────────────────────
procedure SetSpectrumData(const Pixels: array of Single; Count: Integer);
procedure SetWaterfallData(const Pixels: array of Single; Count: Integer);
@@ -223,14 +206,6 @@ type
procedure DrawSpectrum;
procedure DrawWaterfall;
procedure DrawRuler;
- procedure DrawBarMeter(ACanvas: TCanvas; R: TRect;
- Value, MaxVal: Double; BarColor: TColor);
- procedure DrawSMeterWide(ACanvas: TCanvas; R: TRect;
- Value, Peak, MinVal: Double;
- out ZX1, ZX2, ZY1, ZY2: Integer);
- procedure DrawSMeterZone(ACanvas: TCanvas; R: TRect;
- X1, X2, Y1, Y2: Integer);
- procedure DrawTXMeter(ACanvas: TCanvas; R: TRect);
// ── Управление размером bitmap ────────────────────────────────────────────
procedure SetSpectrumBitmapSize(W, H: Integer);
@@ -238,17 +213,14 @@ type
procedure SetRulerSize(W, H: Integer);
function SpectrumBitmapWidth: Integer;
- // ── Paint-обработчики (назначаются в BuildUI) ─────────────────────────────
+ // ── Paint-обработчики ────────────────────────────────────────────────────
procedure PaintSpectrum(Sender: TObject);
procedure PaintWaterfall(Sender: TObject);
procedure PaintRuler(Sender: TObject);
procedure PaintSMeterRight(Sender: TObject);
- procedure PaintFwdPower(Sender: TObject);
- procedure PaintSWR(Sender: TObject);
// ── Утилиты ───────────────────────────────────────────────────────────────
procedure InvalidateGridCache;
- // ── Тема ─────────────────────────────────────────────────────────────────
procedure SetTheme(const T: TAppTheme);
procedure InvalidateRulerCache;
procedure ResetSpectrumBuf;
@@ -257,93 +229,19 @@ type
function NeedsRulerRedraw: Boolean;
end;
-// Вспомогательные функции (публичные для возможного использования в MainForm)
-function FormatFreqSV(Hz: Double): string;
-
implementation
// ════════════════════════════════════════════════════════════════════════════
-// Вспомогательные функции — цвет, градиент
+// Вспомогательные функции
// ════════════════════════════════════════════════════════════════════════════
-function WaterfallColorThetis(Level: Integer): LongWord;
-const
- STOPS: array[0..5] of LongWord = (
- $000000, $0A1A48, $0088CC, $E0C020, $D84A12, $FFFFFF);
-var
- Seg, Base: Integer;
- T: Double;
- C0, C1: LongWord;
- R, G, B: Integer;
+function FormatFreqSV(Hz: Double): string;
+var Mhz: Int64; KHz, Rest: Integer;
begin
- Level := EnsureRange(Level, 0, 255);
- Seg := Min(4, Level div 51);
- Base := Seg * 51;
- T := (Level - Base) / 51.0;
- C0 := STOPS[Seg];
- C1 := STOPS[Seg + 1];
- R := Round(((C0 shr 16) and $FF) * (1.0 - T) + ((C1 shr 16) and $FF) * T);
- G := Round(((C0 shr 8) and $FF) * (1.0 - T) + ((C1 shr 8) and $FF) * T);
- B := Round((C0 and $FF) * (1.0 - T) + (C1 and $FF) * T);
- Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B;
-end;
-
-function WaterfallEnhancedColorThetis(ValueDB, LowDB, HighDB: Double): LongWord;
-var
- Overall, Local: Double;
- R, G, B: Integer;
-begin
- if ValueDB <= LowDB then begin Result := $FF000000; Exit; end;
- if ValueDB >= HighDB then begin Result := ($FF shl 24) or (255 shl 16) or (124 shl 8) or 192; Exit; end;
- Overall := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB);
- if Overall < (2.0 / 9.0) then
- begin Local := Overall / (2.0/9.0); R := 0; G := 0; B := Round(Local * 255.0); end
- else if Overall < (3.0 / 9.0) then
- begin Local := (Overall - 2.0/9.0) / (1.0/9.0); R := 0; G := Round(Local*255.0); B := 255; end
- else if Overall < (4.0 / 9.0) then
- begin Local := (Overall - 3.0/9.0) / (1.0/9.0); R := 0; G := 255; B := Round((1.0-Local)*255.0); end
- else if Overall < (5.0 / 9.0) then
- begin Local := (Overall - 4.0/9.0) / (1.0/9.0); R := Round(Local*255.0); G := 255; B := 0; end
- else if Overall < (7.0 / 9.0) then
- begin Local := (Overall - 5.0/9.0) / (2.0/9.0); R := 255; G := Round((1.0-Local)*255.0); B := 0; end
- else if Overall < (8.0 / 9.0) then
- begin Local := (Overall - 7.0/9.0) / (1.0/9.0); R := 255; G := 0; B := Round(Local*255.0); end
- else
- begin
- Local := (Overall - 8.0/9.0) / (1.0/9.0);
- R := Round((0.75 + 0.25 * (1.0 - Local)) * 255.0);
- G := Round(Local * 255.0 * 0.5);
- B := 255;
- end;
- R := EnsureRange(R, 0, 255);
- G := EnsureRange(G, 0, 255);
- B := EnsureRange(B, 0, 255);
- Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B;
-end;
-
-// Цветовая схема водопада для светлой темы.
-// Шум растворяется в фоне панели; сигналы: синий → cyan → зелёный → жёлтый → оранжевый → красный.
-function WaterfallLightTheme(ValueDB, LowDB, HighDB: Double): LongWord;
-const
- NSTOPS = 8;
- // panel steel blue cyan green yell orange red
- SR: array[0..NSTOPS-1] of Integer = (224, 160, 20, 0, 0, 220, 255, 255);
- SG: array[0..NSTOPS-1] of Integer = (230, 185, 80, 185, 200, 210, 90, 20);
- SB: array[0..NSTOPS-1] of Integer = (232, 210, 200, 210, 80, 0, 0, 20);
-var
- T: Double;
- Seg: Integer;
- R, G, B: Integer;
-begin
- if ValueDB <= LowDB then begin Result := $FFE0E6E8; Exit; end;
- if ValueDB >= HighDB then begin Result := ($FF shl 24) or (SR[NSTOPS-1] shl 16) or (SG[NSTOPS-1] shl 8) or SB[NSTOPS-1]; Exit; end;
- T := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB) * (NSTOPS - 1);
- Seg := Min(NSTOPS - 2, Trunc(T));
- T := T - Seg;
- R := EnsureRange(Round(SR[Seg] * (1.0 - T) + SR[Seg+1] * T), 0, 255);
- G := EnsureRange(Round(SG[Seg] * (1.0 - T) + SG[Seg+1] * T), 0, 255);
- B := EnsureRange(Round(SB[Seg] * (1.0 - T) + SB[Seg+1] * T), 0, 255);
- Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B;
+ Mhz := Trunc(Hz / 1000000);
+ KHz := Trunc((Hz - Mhz * 1000000) / 1000);
+ Rest := Trunc(Hz) mod 1000;
+ Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
end;
procedure InitRawDesc32(var Desc: TRawImageDescription; AWidth, AHeight: Integer);
@@ -389,15 +287,6 @@ begin
C.Pen.Style := psSolid;
end;
-function FormatFreqSV(Hz: Double): string;
-var Mhz: Int64; KHz, Rest: Integer;
-begin
- Mhz := Trunc(Hz / 1000000);
- KHz := Trunc((Hz - Mhz * 1000000) / 1000);
- Rest := Trunc(Hz) mod 1000;
- Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
-end;
-
// ════════════════════════════════════════════════════════════════════════════
// TSpectrumView
// ════════════════════════════════════════════════════════════════════════════
@@ -406,36 +295,20 @@ constructor TSpectrumView.Create;
begin
inherited Create;
FSpectrumBitmap := TBitmap.Create;
- // pf32bit нужен для попиксельного alpha-blend в BlendBand (TX/RX-полосы фильтра).
FSpectrumBitmap.PixelFormat := pf32bit;
- FWaterfallBitmap := TBitmap.Create;
- FWfBitmap := TBitmap.Create;
FGridBitmap := TBitmap.Create;
- FRulerBitmap := TBitmap.Create;
FSpecGradBmp := TBitmap.Create;
- FSmBitmap := nil;
FSpecGradImg := nil;
- FWfIntfImg := nil;
- FWfBitmapW := 0; FWfBitmapH := 0;
FGridBitmapW := 0; FGridBitmapH := 0;
FFMGridLastCenter := -1.0; FFMGridLastStepHz := -1.0; FFMGridLastSpan := -1.0;
FFMGridStepHz := 0.0;
FSpPtsLen := 0;
- FRulerLastFreq := -1.0; FRulerLastVfo := -1.0;
- // Водопад AGC defaults
- FWfManualHigh := -80.0;
- FWfManualLow := -130.0;
- FWfAGCOffset := 0.0;
- FWfHigh := -80.0;
- FWfLow := -130.0;
- FWfAGCEnabled := True;
- FWfNFEnabled := False;
// Spectrum display defaults
FSpecRefLevel := -20.0;
FSpecRange := 110.0;
FSpecGridStep := 10.0;
FSpanHz := 192000;
- // TX overlay defaults — сетка совпадает с RX по умолчанию
+ // TX overlay defaults
FTXMode := False;
FTXFreq := 0.0;
FTXSpanHz := 192000.0;
@@ -443,32 +316,136 @@ begin
FTXOverlay := False;
FADCOverloadVisible := False;
FSpectrumBufCount := 1024;
- FWaterfallBufCount := 1024;
- FWfFrameInterval := 2;
- FWfFrameCounter := 0;
- FWaterfallDirty := True;
- FLastSMeter := -130; FSMeterPeak := -130; FSMeterMin := -130;
- FLastFwdW := 0; FLastSWR := 1;
- FPAMaxPower := 100.0; FTransmitting := False;
+ FSpectrumDirty := True;
FLightTheme := False;
FTheme := DarkTheme;
+ // Sub-views
+ FWaterfall := TWaterfallView.Create;
+ FSMeter := TSMeterView.Create;
+ FRuler := TRulerView.Create;
ResetSpectrumBuf;
end;
destructor TSpectrumView.Destroy;
begin
FSpectrumBitmap.Free;
- FWaterfallBitmap.Free;
- FWfBitmap.Free;
FGridBitmap.Free;
- FRulerBitmap.Free;
FSpecGradBmp.Free;
FreeAndNil(FSpecGradImg);
- FreeAndNil(FWfIntfImg);
- FreeAndNil(FSmBitmap);
+ FWaterfall.Free;
+ FSMeter.Free;
+ FRuler.Free;
inherited;
end;
+// ────────────────────────────────────────────────────────────────────────────
+// Setters with cascade to sub-views
+// ────────────────────────────────────────────────────────────────────────────
+
+procedure TSpectrumView.SetCenterFreq(V: Double);
+begin
+ FCenterFreq := V;
+ FWaterfall.CenterFreq := V;
+ FRuler.CenterFreq := V;
+end;
+
+procedure TSpectrumView.SetSpanHz(V: Double);
+begin
+ FSpanHz := V;
+ FWaterfall.SpanHz := V;
+ FRuler.SpanHz := V;
+end;
+
+procedure TSpectrumView.SetVfoA(V: Double);
+begin
+ FVfoA := V;
+ FRuler.VfoA := V;
+end;
+
+procedure TSpectrumView.SetVfoB(V: Double);
+begin
+ FVfoB := V;
+ FRuler.VfoB := V;
+end;
+
+procedure TSpectrumView.SetActiveVfo(V: Integer);
+begin
+ FActiveVfo := V;
+ FRuler.ActiveVfo := V;
+end;
+
+procedure TSpectrumView.SetTXMode(V: Boolean);
+begin
+ FTXMode := V;
+ FWaterfall.TXMode := V;
+end;
+
+procedure TSpectrumView.SetTXFreq(V: Double);
+begin
+ FTXFreq := V;
+ FWaterfall.TXFreq := V;
+end;
+
+procedure TSpectrumView.SetTXSpanHz(V: Double);
+begin
+ FTXSpanHz := V;
+ FWaterfall.TXSpanHz := V;
+end;
+
+procedure TSpectrumView.SetMarkerActive(V: Boolean);
+begin
+ FMarkerActive := V;
+ FWaterfall.MarkerActive := V;
+end;
+
+procedure TSpectrumView.SetMarkerX(V: Integer);
+begin
+ FMarkerX := V;
+ FWaterfall.MarkerX := V;
+end;
+
+function TSpectrumView.GetWaterfallDirty: Boolean;
+begin
+ Result := FWaterfall.WaterfallDirty;
+end;
+
+procedure TSpectrumView.SetWaterfallDirty(V: Boolean);
+begin
+ FWaterfall.WaterfallDirty := V;
+end;
+
+function TSpectrumView.GetWfAGCEnabled: Boolean; begin Result := FWaterfall.WfAGCEnabled; end;
+procedure TSpectrumView.SetWfAGCEnabled(V: Boolean); begin FWaterfall.WfAGCEnabled := V; end;
+function TSpectrumView.GetWfNFEnabled: Boolean; begin Result := FWaterfall.WfNFEnabled; end;
+procedure TSpectrumView.SetWfNFEnabled(V: Boolean); begin FWaterfall.WfNFEnabled := V; end;
+function TSpectrumView.GetWfManualHigh: Double; begin Result := FWaterfall.WfManualHigh; end;
+procedure TSpectrumView.SetWfManualHigh(V: Double); begin FWaterfall.WfManualHigh := V; end;
+function TSpectrumView.GetWfManualLow: Double; begin Result := FWaterfall.WfManualLow; end;
+procedure TSpectrumView.SetWfManualLow(V: Double); begin FWaterfall.WfManualLow := V; end;
+function TSpectrumView.GetWfAGCOffset: Double; begin Result := FWaterfall.WfAGCOffset; end;
+procedure TSpectrumView.SetWfAGCOffset(V: Double); begin FWaterfall.WfAGCOffset := V; end;
+function TSpectrumView.GetWfFrameInterval: Integer; begin Result := FWaterfall.WfFrameInterval; end;
+procedure TSpectrumView.SetWfFrameInterval(V: Integer); begin FWaterfall.WfFrameInterval := V; end;
+procedure TSpectrumView.SetPbWaterfall(V: TPaintBox); begin FWaterfall.PbWaterfall := V; end;
+
+function TSpectrumView.GetLastSMeter: Double; begin Result := FSMeter.LastSMeter; end;
+procedure TSpectrumView.SetLastSMeter(V: Double); begin FSMeter.LastSMeter := V; end;
+function TSpectrumView.GetSMeterPeak: Double; begin Result := FSMeter.SMeterPeak; end;
+procedure TSpectrumView.SetSMeterPeak(V: Double); begin FSMeter.SMeterPeak := V; end;
+function TSpectrumView.GetSMeterMin: Double; begin Result := FSMeter.SMeterMin; end;
+procedure TSpectrumView.SetSMeterMin(V: Double); begin FSMeter.SMeterMin := V; end;
+function TSpectrumView.GetLastFwdW: Double; begin Result := FSMeter.LastFwdW; end;
+procedure TSpectrumView.SetLastFwdW(V: Double); begin FSMeter.LastFwdW := V; end;
+function TSpectrumView.GetLastSWR: Double; begin Result := FSMeter.LastSWR; end;
+procedure TSpectrumView.SetLastSWR(V: Double); begin FSMeter.LastSWR := V; end;
+function TSpectrumView.GetPAMaxPower: Double; begin Result := FSMeter.PAMaxPower; end;
+procedure TSpectrumView.SetPAMaxPower(V: Double); begin FSMeter.PAMaxPower := V; end;
+function TSpectrumView.GetTransmitting: Boolean; begin Result := FSMeter.Transmitting; end;
+procedure TSpectrumView.SetTransmitting(V: Boolean); begin FSMeter.Transmitting := V; end;
+procedure TSpectrumView.SetPbSMeterRight(V: TPaintBox); begin FSMeter.PbSMeterRight := V; end;
+
+procedure TSpectrumView.SetPbRuler(V: TPaintBox); begin FRuler.PbRuler := V; end;
+
// ────────────────────────────────────────────────────────────────────────────
// Приватные вспомогательные методы
// ────────────────────────────────────────────────────────────────────────────
@@ -489,8 +466,9 @@ begin
if Abs(FFMGridStepHz - V) > 0.5 then
begin
FFMGridStepHz := V;
+ FRuler.FMGridStepHz := V;
InvalidateGridCache;
- InvalidateRulerCache;
+ FRuler.InvalidateRulerCache;
end;
end;
@@ -513,10 +491,6 @@ begin
end;
procedure TSpectrumView.BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
-// Накладывает полупрозрачную вертикальную полосу [X1..X2) высотой H на
-// FSpectrumBitmap. Канал A — сила оверлея 0..255 (255 = полностью непрозрачно).
-// Формула: dst = src*(1-a) + color*a, BGRA-порядок (pf32bit Lazarus default).
-// BeginUpdate(False) — нужен для прямого доступа к raw-пикселям через ScanLine.
var
Y, X, InvA: Integer;
Row: PByte;
@@ -540,7 +514,6 @@ begin
Row[0] := Byte((Alpha * B + InvA * Row[0]) div 255);
Row[1] := Byte((Alpha * G + InvA * Row[1]) div 255);
Row[2] := Byte((Alpha * R + InvA * Row[2]) div 255);
- // Row[3] оставляем — на BitBlt в TPaintBox не влияет.
Inc(Row, 4);
end;
end;
@@ -557,8 +530,6 @@ end;
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
out X1, X2, VfoX: Integer);
-// Возвращает X-координаты левого/правого края полосы фильтра вокруг VfoFreq
-// и саму позицию VFO. Учитывает FMode (LSB/USB/DSB) и FFilterBW.
var
Lo_Hz, Hi_Hz, Half: Double;
begin
@@ -640,17 +611,8 @@ begin
end;
procedure TSpectrumView.SetWaterfallData(const Pixels: array of Single; Count: Integer);
-var i, N: Integer;
begin
- N := Min(Count, 1024);
- for i := 0 to N - 1 do FWaterfallBuf[i] := Pixels[i];
- FWaterfallBufCount := N;
- Inc(FWfFrameCounter);
- if FWfFrameCounter >= Max(1, FWfFrameInterval) then
- begin
- FWfFrameCounter := 0;
- FWaterfallDirty := True;
- end;
+ FWaterfall.SetWaterfallData(Pixels, Count);
end;
// ────────────────────────────────────────────────────────────────────────────
@@ -670,17 +632,12 @@ end;
procedure TSpectrumView.SetWaterfallBitmapSize(W, H: Integer);
begin
- FWaterfallBitmap.SetSize(W, H);
- FWfBitmapW := 0; FWfBitmapH := 0;
- SetLength(FWfPixels, 0);
- FreeAndNil(FWfIntfImg);
+ FWaterfall.SetWaterfallBitmapSize(W, H);
end;
procedure TSpectrumView.SetRulerSize(W, H: Integer);
begin
- if (W <= 0) or (H <= 0) then Exit;
- FRulerBitmap.SetSize(W, H);
- FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; FRulerLastSpan := -1.0;
+ FRuler.SetRulerSize(W, H);
end;
function TSpectrumView.SpectrumBitmapWidth: Integer;
@@ -701,31 +658,29 @@ end;
procedure TSpectrumView.SetTheme(const T: TAppTheme);
begin
FTheme := T;
- FLightTheme := T.BG > TColor($00808080); // светлая тема если фон светлее 50%
- FreeAndNil(FSpecGradImg); // пересчитать градиент
+ FLightTheme := T.BG > TColor($00808080);
+ FreeAndNil(FSpecGradImg);
InvalidateGridCache;
- InvalidateRulerCache;
- FWaterfallDirty := True;
+ FWaterfall.SetTheme(T);
+ FSMeter.SetTheme(T);
+ FRuler.SetTheme(T);
end;
procedure TSpectrumView.InvalidateRulerCache;
begin
- FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; FRulerLastSpan := -1.0;
+ FRuler.InvalidateRulerCache;
end;
procedure TSpectrumView.ResetSpectrumBuf;
var i: Integer;
begin
- for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
- for i := 0 to 1023 do FWaterfallBuf[i] := -130.0;
- FWfHigh := FWfManualHigh; FWfLow := FWfManualLow;
- FWaterfallDirty := True;
+ for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
+ FWaterfall.ResetWfBuf;
end;
procedure TSpectrumView.ResetWfAvgBuf;
begin
- FWfHigh := FWfManualHigh; FWfLow := FWfManualLow;
- FWaterfallDirty := True;
+ FWaterfall.ResetWfAvgBuf;
end;
procedure TSpectrumView.FillDemoSpectrum;
@@ -746,9 +701,7 @@ end;
function TSpectrumView.NeedsRulerRedraw: Boolean;
begin
- Result := (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or
- (Abs(ActiveVfoFreq - FRulerLastVfo) >= 0.5) or
- (Abs(FSpanHz - FRulerLastSpan) >= 1.0);
+ Result := FRuler.NeedsRulerRedraw;
end;
// ────────────────────────────────────────────────────────────────────────────
@@ -849,30 +802,20 @@ begin
LCLIntf.BitBlt(C.Handle, 0, 0, W, H, FGridBitmap.Canvas.Handle, 0, 0, $CC0020);
// ── 2. Полоса фильтра ─────────────────────────────────────────────────────
- // Логика:
- // Не TX → одна полоса на FActiveVfo, штатным цветом FTheme.SpecFilter (solid).
- // TX, не split → одна полоса на FActiveVfo, alpha-blend красный.
- // TX, split → две полосы: RX (FActiveVfo) — alpha-blend зелёный,
- // TX (FTXVfoIndex) — alpha-blend красный.
- // Edges + VFO-cursor рисуются позже на VfoX/X1/X2 — это RX-VFO-полоса
- // (для split-режима TX-полоса рисуется без edges чтобы визуально отличалась).
if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX)
else CalcFilterBandX(FVfoB, W, X1, X2, VfoX);
if FTXOverlay then
begin
if FTXVfoIndex <> FActiveVfo then
begin
- // Split TX: RX-полоса зелёная, TX-полоса (на другом VFO) красная.
if X2 > X1 then BlendBand(X1, X2, H, $30, $C0, $30, 100);
if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
if TXX2 > TXX1 then BlendBand(TXX1, TXX2, H, $E0, $30, $30, 100);
end else
- // Не split: единственная полоса — TX на активном VFO, красная.
if X2 > X1 then BlendBand(X1, X2, H, $E0, $30, $30, 100);
end else
begin
- // Обычный приём — заливка штатным цветом без alpha.
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;
@@ -914,18 +857,12 @@ begin
SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024);
if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then
begin
- // TX-режим: данные приходят @ FTXSpanHz вокруг FTXFreq (TX-анализатор —
- // baseband), а рисуем в координатах RX (FCenterFreq/FSpanHz). Это даёт
- // правильное положение TX-сигнала при CTUN ON и выравнивает сетку, когда
- // RX rate ≠ TX rate (192k).
for i := 0 to W - 1 do
begin
- // freq, соответствующая пикселю i в RX-координатах
SrcF := FCenterFreq - FSpanHz * 0.5 + i * FSpanHz / Max(1, W - 1);
- // смещение от центра TX-окна
Frac := SrcF - FTXFreq;
if (Frac < -FTXSpanHz * 0.5) or (Frac > FTXSpanHz * 0.5) then
- dBv := -200.0 // вне диапазона TX-анализатора
+ dBv := -200.0
else
begin
SrcF := (Frac + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1);
@@ -964,7 +901,6 @@ begin
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12);
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)]);
- // Split TX: рисуем края + cursor для TX-VFO красным, чтобы было видно где идёт передача
if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then
begin
C.Pen.Color := TColor($002030E0); C.Pen.Width := 1; C.Pen.Style := psSolid;
@@ -981,671 +917,17 @@ begin
end;
// ────────────────────────────────────────────────────────────────────────────
-// DrawWaterfall
+// Делегирование к sub-views
// ────────────────────────────────────────────────────────────────────────────
procedure TSpectrumView.DrawWaterfall;
-var
- W, H, X: Integer;
- dB, frac, WatSrcF: Double;
- WatS0, WatS1, V: Integer;
- TargetLow, TargetHigh: Double;
- HistMinDB, HistMaxDB, HistStepDB: Double;
- NoiseFloorDB, SignalTopDB: Double;
- CumCount, LowTargetCount, HighTargetCount, HistIdx: Integer;
- WfHigh, WfLow, InvRange, Step: Double;
- Px: PLongWord;
- Desc: TRawImageDescription;
- Row, Col: Integer; RowPtr: PByte; Src: PLongWord; BPPi: Integer;
- Pal: LongWord; Hist: array[0..191] of Integer; SrcCount: Integer;
-const
- ALPHA_HIGH = 0.10; ALPHA_LOW = 0.08;
- WF_AUTO_OFFSET = -4.0; WF_MIN_RANGE = 48.0; WF_MAX_RANGE = 62.0;
begin
- if FWaterfallBitmap = nil then Exit;
- W := FWaterfallBitmap.Width; H := FWaterfallBitmap.Height;
- if (W <= 0) or (H <= 0) then Exit;
- SrcCount := EnsureRange(FWaterfallBufCount, 2, 1024);
-
- FillChar(Hist, SizeOf(Hist), 0);
- HistMinDB := -170.0; HistMaxDB := 22.0;
- HistStepDB := (HistMaxDB - HistMinDB) / Length(Hist);
- for X := 0 to SrcCount - 1 do
- begin
- HistIdx := EnsureRange(Trunc((FWaterfallBuf[X] - HistMinDB) / HistStepDB), 0, High(Hist));
- Inc(Hist[HistIdx]);
- end;
-
- LowTargetCount := Round(SrcCount * 0.30);
- HighTargetCount := Round(SrcCount * 0.98);
- CumCount := 0; NoiseFloorDB := FWfLow; SignalTopDB := FWfHigh;
- for HistIdx := 0 to High(Hist) do
- begin
- CumCount := CumCount + Hist[HistIdx];
- if CumCount >= LowTargetCount then
- begin NoiseFloorDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; Break; end;
- end;
- CumCount := 0;
- for HistIdx := 0 to High(Hist) do
- begin
- CumCount := CumCount + Hist[HistIdx];
- if CumCount >= HighTargetCount then
- begin SignalTopDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; Break; end;
- end;
-
- if FWfAGCEnabled then
- begin
- TargetLow := NoiseFloorDB + WF_AUTO_OFFSET + FWfAGCOffset;
- if FWfNFEnabled then
- TargetHigh := Max(TargetLow + WF_MIN_RANGE, SignalTopDB + 6.0)
- else
- TargetHigh := TargetLow + 52.0;
- if TargetHigh > TargetLow + WF_MAX_RANGE then TargetHigh := TargetLow + WF_MAX_RANGE;
- FWfLow := FWfLow + ALPHA_LOW * (TargetLow - FWfLow);
- FWfHigh := FWfHigh + ALPHA_HIGH * (TargetHigh - FWfHigh);
- end;
- WfHigh := FWfHigh; WfLow := FWfLow;
- if not FWfAGCEnabled then begin WfHigh := FWfManualHigh; WfLow := FWfManualLow; end;
- if WfHigh < WfLow + 40.0 then WfHigh := WfLow + 40.0;
- if WfHigh > 0.0 then WfHigh := 0.0;
- if WfLow < -160 then WfLow := -160;
- InvRange := 255.0 / (WfHigh - WfLow);
-
- if (FWfBitmapW <> W) or (FWfBitmapH <> H) then
- begin
- FWfBitmapW := W; FWfBitmapH := H;
- SetLength(FWfPixels, W * H);
- // TColor = $00BBGGRR → waterfall pixel = $FF_RR_GG_BB (swap R и B)
- Pal := $FF000000
- or ((LongWord(FTheme.Panel) and $FF) shl 16)
- or (LongWord(FTheme.Panel) and $FF00)
- or ((LongWord(FTheme.Panel) shr 16) and $FF);
- FillDWord(FWfPixels[0], W * H, Pal);
- FWfBitmap.SetSize(W, H);
- FreeAndNil(FWfIntfImg);
- end;
-
- if H > 1 then Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W);
-
- Step := (SrcCount - 1.0) / Max(1, W - 1);
- WatSrcF := 0.0; Px := @FWfPixels[0];
- for X := 0 to W - 1 do
- begin
- if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then
- begin
- // частота для пикселя X в RX-координатах → индекс в TX-буфере
- dB := FCenterFreq - FSpanHz * 0.5 + X * FSpanHz / Max(1, W - 1) - FTXFreq;
- if (dB < -FTXSpanHz * 0.5) or (dB > FTXSpanHz * 0.5) then
- begin
- // Вне TX-окна: копируем пиксель из предыдущей строки (она лежит на
- // расстоянии W в FWfPixels — Move уже сдвинул её ниже). Так старая
- // RX-картинка продолжается без чёрной "дыры", пока TX рисует своё
- // окно в центре.
- if H > 1 then Px^ := FWfPixels[X + W]
- else Px^ := 0;
- Inc(Px);
- WatSrcF := WatSrcF + Step;
- Continue;
- end;
- frac := (dB + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1);
- WatS0 := Trunc(frac);
- if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
- WatS1 := WatS0 + 1;
- frac := frac - WatS0;
- dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
- end else
- begin
- WatS0 := Trunc(WatSrcF);
- if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
- WatS1 := WatS0 + 1;
- frac := WatSrcF - WatS0;
- dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
- end;
- V := Trunc((dB - WfLow) * InvRange);
- if V < 0 then V := 0; if V > 255 then V := 255;
- if FLightTheme then
- Pal := WaterfallLightTheme(dB, WfLow, WfHigh)
- else
- Pal := WaterfallEnhancedColorThetis(dB, WfLow, WfHigh);
- Px^ := Pal; Inc(Px);
- WatSrcF := WatSrcF + Step;
- end;
-
- if FWfIntfImg = nil then
- begin
- FWfIntfImg := TLazIntfImage.Create(W, H);
- InitRawDesc32(Desc, W, H);
- FWfIntfImg.DataDescription := Desc;
- FWfIntfImg.CreateData;
- end;
- BPPi := FWfIntfImg.DataDescription.BitsPerPixel div 8;
- if (BPPi = 4) and (FWfIntfImg.PixelData <> nil) then
- Move(FWfPixels[0], FWfIntfImg.PixelData^, W * H * 4)
- else
- begin
- Src := @FWfPixels[0];
- for Row := 0 to H - 1 do
- begin
- RowPtr := FWfIntfImg.GetDataLineStart(Row);
- if RowPtr = nil then begin Inc(Src, W); Continue; end;
- for Col := 0 to W - 1 do
- begin
- RowPtr[0] := Byte(Src^); RowPtr[1] := Byte(Src^ shr 8);
- RowPtr[2] := Byte(Src^ shr 16);
- if BPPi >= 4 then RowPtr[3] := $FF;
- Inc(Src); Inc(RowPtr, BPPi);
- end;
- end;
- end;
- FWfBitmap.LoadFromIntfImage(FWfIntfImg);
+ FWaterfall.DrawWaterfall;
end;
-// ────────────────────────────────────────────────────────────────────────────
-// DrawRuler
-// ────────────────────────────────────────────────────────────────────────────
-
procedure TSpectrumView.DrawRuler;
-var
- C: TCanvas; W, H, i, X: Integer;
- FreqStart, FreqHz, GridLine, pixPerStep: Double; VfoX: Integer;
- Lbl: string; TW: Integer;
- N, labelMult: Integer;
begin
- if FPbRuler = nil then Exit;
- W := FPbRuler.Width; H := FPbRuler.Height;
- if (W <= 0) or (H <= 0) then Exit;
- if (Abs(FCenterFreq - FRulerLastFreq) < 0.5) and
- (Abs(ActiveVfoFreq - FRulerLastVfo) < 0.5) and
- (Abs(FSpanHz - FRulerLastSpan) < 1.0) then Exit;
- FRulerLastFreq := FCenterFreq; FRulerLastVfo := ActiveVfoFreq; FRulerLastSpan := FSpanHz;
- if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then
- FRulerBitmap.SetSize(W, H);
- C := FRulerBitmap.Canvas;
- 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;
- C.Brush.Style := bsClear;
- FreqStart := FCenterFreq - FSpanHz / 2;
- if FFMGridStepHz > 0 then
- begin
- pixPerStep := W * FFMGridStepHz / FSpanHz;
- if pixPerStep >= 1.0 then
- labelMult := Max(1, Ceil((C.TextWidth('000.000') + 6) / pixPerStep))
- else
- labelMult := MaxInt;
- N := Ceil(FreqStart / FFMGridStepHz);
- GridLine := N * FFMGridStepHz;
- while GridLine <= FCenterFreq + FSpanHz / 2 + 0.5 do
- begin
- X := Round((GridLine - FreqStart) / FSpanHz * W);
- if (X >= 0) and (X < W) then
- begin
- C.Pen.Color := FTheme.RulerBorder;
- if (N mod labelMult) = 0 then
- begin
- C.MoveTo(X, 0); C.LineTo(X, H div 2);
- Lbl := Format('%.3f', [GridLine / 1e6]);
- TW := C.TextWidth(Lbl);
- C.Font.Color := FTheme.RulerText;
- C.TextOut(X - TW div 2, H div 2 - 1, Lbl);
- end else
- begin
- C.MoveTo(X, 0); C.LineTo(X, H div 4);
- end;
- end;
- Inc(N);
- GridLine := GridLine + FFMGridStepHz;
- end;
- end else
- begin
- for i := 0 to 8 do
- begin
- X := ScaleX(i, 8, W);
- 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 := FTheme.RulerText;
- C.TextOut(X - TW div 2, H div 2 - 1, Lbl);
- end;
- end;
- VfoX := Round((ActiveVfoFreq - FCenterFreq + FSpanHz/2) / FSpanHz * W);
- if (VfoX >= 0) and (VfoX < W) then
- begin
- C.Pen.Color := FTheme.RulerVfo; C.Pen.Width := 2;
- C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 1); C.Pen.Width := 1;
- end;
-end;
-
-// ────────────────────────────────────────────────────────────────────────────
-// DrawBarMeter, DrawSMeterWide, DrawSMeterZone
-// ────────────────────────────────────────────────────────────────────────────
-
-procedure TSpectrumView.DrawBarMeter(ACanvas: TCanvas; R: TRect;
- Value, MaxVal: Double; BarColor: TColor);
-var Pct, BarW: Integer;
-begin
- 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))
- else
- Pct := 0;
- BarW := Pct;
- ACanvas.Brush.Color := BarColor; ACanvas.Pen.Color := BarColor;
- ACanvas.FillRect(Rect(R.Left+1, R.Top+1, R.Left+1+BarW, R.Bottom-1));
- ACanvas.Brush.Style := bsClear;
- ACanvas.Pen.Color := SV_CLR_BORDER;
- ACanvas.Rectangle(R);
-end;
-
-procedure TSpectrumView.DrawSMeterWide(ACanvas: TCanvas; R: TRect;
- Value, Peak, MinVal: Double; out ZX1, ZX2, ZY1, ZY2: Integer);
-const
- DB_MIN = -127.0; DB_MAX = -13.0; DB_S9 = -73.0; DB_OVR = -43.0;
- DBM_MARKS: array[0..5] of Double = (-120,-100,-80,-60,-40,-20);
- 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');
- 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;
- i, X, TW: Integer; Lbl: string; SNum, Over: Integer;
- LblDbm, LblS: string;
- TWDbm, LblZoneL, TH10, TH6, MaxTWDbm, MaxTWS, LabelNeed: Integer;
-
- function DBtoX(dB: Double): Integer; inline;
- begin
- Result := BX + Round((dB - DB_MIN) / (DB_MAX - DB_MIN) * BW);
- if Result < BX then Result := BX;
- if Result > BX + BW then Result := BX + BW;
- end;
-
- procedure VLine(X2, Y1, Y2: Integer; C: TColor);
- begin ACanvas.Pen.Color := C; ACanvas.MoveTo(X2, Y1); ACanvas.LineTo(X2, Y2); end;
-
- procedure FillBar(X1, Y1, X2, Y2: Integer; C: TColor);
- begin
- ACanvas.Brush.Color := C; ACanvas.Brush.Style := bsSolid;
- ACanvas.Pen.Style := psClear;
- if X2 > X1 then ACanvas.FillRect(Rect(X1, Y1, X2, Y2));
- ACanvas.Pen.Style := psSolid;
- 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;
-
- // --- Вычисляем строки надписей заранее чтобы измерить ширину ---
- LblDbm := Format('%.1f', [Value]);
- if Value >= -73.0 then
- begin
- Over := Round(Value - (-73.0));
- if Over < 5 then LblS := 'S9'
- else begin Over := ((Over + 5) div 10) * 10; if Over = 0 then Over := 10; LblS := Format('S9+%d', [Over]); end;
- end else if Value <= -121.0 then LblS := 'S1'
- else begin
- LblS := 'S1';
- for SNum := 1 to 8 do
- if Value >= (-121.0 + (SNum - 1) * 6.0) then LblS := 'S' + IntToStr(SNum);
- end;
-
- // Измеряем ширину надписей
- ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Style := [fsBold];
- ACanvas.Font.Size := 10; TWDbm := ACanvas.TextWidth(LblDbm);
- MaxTWDbm := ACanvas.TextWidth('-120.0');
- MaxTWS := ACanvas.TextWidth('S9+60');
- ACanvas.Font.Size := 6; ACanvas.Font.Style := [];
- // Левая информационная зона зависит от реальных метрик шрифта.
- LabelNeed := Max(MaxTWDbm + 2 + ACanvas.TextWidth('dBm '), MaxTWS) + LABEL_PAD * 2;
- LeftInfo := Max(LEFT_INFO_MIN, LabelNeed + 4);
- BX := R.Left + LeftInfo; BW := W - LeftInfo - RIGHT_PAD;
- if BW < 10 then Exit;
- LblZoneL := BX - LabelNeed;
-
- Y_BAR_TOP := R.Top + H * 44 div 100;
- Y_BAR_BOT := R.Top + H * 58 div 100;
- S9X := DBtoX(DB_S9); OvrX := DBtoX(DB_OVR);
-
- 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, 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);
- PeakRight := DBtoX(Peak);
- ZX1 := Max(BX + 1, Min(PeakLeft, PeakRight));
- ZX2 := Min(BX + BW - 1, Max(PeakLeft, PeakRight));
- ZY1 := Y_BAR_TOP + 1; ZY2 := Y_BAR_BOT - 1;
-
- if BarEnd > BX then
- begin
- if BarEnd <= S9X then
- FillBar(BX, Y_BAR_TOP+1, BarEnd, Y_BAR_BOT-1, CLR_BAR_GREEN)
- else begin
- FillBar(BX, Y_BAR_TOP+1, S9X, Y_BAR_BOT-1, CLR_BAR_GREEN);
- FillBar(S9X, Y_BAR_TOP+1, BarEnd, Y_BAR_BOT-1, CLR_BAR_OVER);
- end;
- end;
- if (PeakRight > BX) and (PeakRight <= BX + BW) then
- begin
- ACanvas.Pen.Color := CLR_PEAK_MARKER; ACanvas.Pen.Width := 2;
- ACanvas.MoveTo(PeakRight, Y_BAR_TOP - (Y_BAR_BOT - Y_BAR_TOP));
- ACanvas.LineTo(PeakRight, Y_BAR_BOT + (Y_BAR_BOT - Y_BAR_TOP));
- ACanvas.Pen.Width := 1;
- end;
-
- // Рамка: от левого края зоны надписей до правого края шкалы
- ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR;
- ACanvas.Rectangle(LblZoneL, Y_BAR_TOP, BX + BW, Y_BAR_BOT);
- // разделитель между зоной надписей и шкалой
- ACanvas.Pen.Color := FTheme.SMeterDivider;
- ACanvas.MoveTo(BX, Y_BAR_TOP + 1); ACanvas.LineTo(BX, Y_BAR_BOT - 1);
-
- // --- Шкальные тики и надписи ---
- ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Brush.Style := bsClear;
- for i := 0 to High(DBM_MARKS) do
- begin
- X := DBtoX(DBM_MARKS[i]);
- VLine(X, Y_BAR_TOP - TICK_LONG, Y_BAR_TOP - 1, CLR_TICK_WHITE);
- Lbl := DBM_LABELS[i]; TW := ACanvas.TextWidth(Lbl);
- ACanvas.Font.Color := CLR_SCALE_DBM;
- ACanvas.TextOut(X - TW div 2, Y_BAR_TOP - TICK_LONG - ACanvas.TextHeight(Lbl) - 1, Lbl);
- end;
- i := -125;
- while i < -13 do
- begin
- X := DBtoX(i); VLine(X, Y_BAR_TOP - TICK_MED, Y_BAR_TOP - 1, CLR_TICK_GREEN);
- Inc(i, 5);
- end;
- for i := 0 to High(S_MARKS_DBM) do
- begin
- X := DBtoX(S_MARKS_DBM[i]);
- if S_MARKS_DBM[i] >= DB_S9 then VLine(X, Y_BAR_BOT+1, Y_BAR_BOT+TICK_LONG, CLR_TICK_BLUE)
- else VLine(X, Y_BAR_BOT+1, Y_BAR_BOT+TICK_LONG, CLR_TICK_GREEN);
- Lbl := S_MARKS_LBL[i]; TW := ACanvas.TextWidth(Lbl);
- if S_MARKS_DBM[i] >= DB_S9 then ACanvas.Font.Color := CLR_SCALE_S_BLUE
- else ACanvas.Font.Color := CLR_SCALE_S_GREEN;
- ACanvas.TextOut(X - TW div 2, Y_BAR_BOT + TICK_LONG + 1, Lbl);
- end;
-
- // --- Надписи в левой зоне, левовыровнены к LblZoneL+4 ---
- ACanvas.Brush.Style := bsClear;
- ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; TH10 := ACanvas.TextHeight('0');
- ACanvas.Font.Size := 6; ACanvas.Font.Style := []; TH6 := ACanvas.TextHeight('0');
- // "-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 := 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;
- ACanvas.TextOut(LblZoneL + 4, Y_BAR_BOT + 2, LblS);
-end;
-
-procedure TSpectrumView.DrawSMeterZone(ACanvas: TCanvas; R: TRect;
- X1, X2, Y1, Y2: Integer);
-const ALPHA = 16384;
-var Img: TLazIntfImage; FC: TFPColor; IX, IY: Integer;
-begin
- if (X2 <= X1) or (FSmBitmap = nil) then Exit;
- Img := FSmBitmap.CreateIntfImage;
- try
- for IY := Y1 to Y2 - 1 do
- for IX := X1 to X2 - 1 do
- begin
- FC := Img.Colors[IX, IY];
- FC.Red := Min(65535, FC.Red + ALPHA);
- FC.Green := Min(65535, FC.Green + ALPHA);
- FC.Blue := Min(65535, FC.Blue + ALPHA);
- Img.Colors[IX, IY] := FC;
- end;
- FSmBitmap.LoadFromIntfImage(Img);
- finally Img.Free; end;
-end;
-
-procedure TSpectrumView.DrawTXMeter(ACanvas: TCanvas; R: TRect);
-// Визуально идентичен DrawSMeterWide: те же пропорции, тот же левый блок,
-// те же тики — но центральная полоса разбита на две: мощность (верх) и КСВ (низ).
-const
- SWR_HIGH_THRESH = 2.5;
- SWR_MAX = 10.0;
- LEFT_INFO_MIN = 62; RIGHT_PAD = 8; LABEL_PAD = 4;
- TICK_LONG = 5; TICK_MED = 3;
- PWR_MARKS: array[0..4] of Double = (0.0, 0.25, 0.5, 0.75, 1.0);
- SWR_MARKS_VAL: array[0..3] of Double = (1.0, 2.5, 5.0, 10.0);
- SWR_MARKS_LBL: array[0..3] of string = ('1', '2.5', '5', '10');
-var
- CLR_SMETER_BG, CLR_BAR_GREEN, CLR_BAR_OVER: TColor;
- CLR_TICK_GREEN, CLR_TICK_WHITE, CLR_TICK_BLUE: TColor;
- CLR_LABEL_DBM, CLR_LABEL_S: TColor;
- CLR_SCALE_DBM, CLR_SCALE_S_GREEN, CLR_SCALE_S_BLUE, CLR_BDR: TColor;
- CLR_SWR_HIGH: TColor;
- W, H, BX, BW, LeftInfo: Integer;
- Y_BAR_TOP, Y_BAR_BOT, Y_BAR_MID: Integer;
- PY1, PY2, SY1, SY2: Integer;
- PBarEnd, SBarEnd, S_WarnX: Integer;
- LblPwr, LblSWR: string;
- TWPwr, LblZoneL, TH10, TH6, MaxTWPwr, MaxTWSWR, LabelNeed: Integer;
- i, X, TW: Integer;
- Lbl: string;
- SWRHigh: Boolean;
- PwrPct: Double;
-
- function PwrToX(Pct: Double): Integer; inline;
- begin
- Result := BX + Round(Max(0.0, Min(1.0, Pct)) * BW);
- if Result < BX then Result := BX;
- if Result > BX + BW then Result := BX + BW;
- end;
-
- function SWRToX(SWR: Double): Integer; inline;
- begin
- Result := BX + Round(Max(0.0, Min(1.0, (SWR - 1.0) / (SWR_MAX - 1.0))) * BW);
- if Result < BX then Result := BX;
- if Result > BX + BW then Result := BX + BW;
- end;
-
- procedure VLine(X2, Y1, Y2: Integer; C: TColor);
- begin ACanvas.Pen.Color := C; ACanvas.MoveTo(X2, Y1); ACanvas.LineTo(X2, Y2); end;
-
- procedure FillBar(X1, Y1, X2, Y2: Integer; C: TColor);
- begin
- ACanvas.Brush.Color := C; ACanvas.Brush.Style := bsSolid;
- ACanvas.Pen.Style := psClear;
- if X2 > X1 then ACanvas.FillRect(Rect(X1, Y1, X2, Y2));
- ACanvas.Pen.Style := psSolid;
- end;
-
-begin
- CLR_SMETER_BG := FTheme.SMeterBG;
- CLR_BAR_GREEN := FTheme.SMeterBarGreen;
- CLR_BAR_OVER := FTheme.SMeterBarOver;
- 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;
- CLR_SWR_HIGH := TColor($000000CC);
-
- W := R.Right - R.Left; H := R.Bottom - R.Top;
- if (W < 80) or (H < 16) then Exit;
-
- SWRHigh := FLastSWR > SWR_HIGH_THRESH;
- LblPwr := Format('%.0fW', [FLastFwdW]);
- LblSWR := Format('SWR:%.1f', [FLastSWR]);
-
- // Левая информационная зона — такая же логика как в DrawSMeterWide
- ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Style := [fsBold];
- ACanvas.Font.Size := 10;
- TWPwr := ACanvas.TextWidth(LblPwr);
- MaxTWPwr := ACanvas.TextWidth(Format('%.0fW', [FPAMaxPower]));
- MaxTWSWR := ACanvas.TextWidth('SWR:10.0');
- ACanvas.Font.Size := 6; ACanvas.Font.Style := [];
- LabelNeed := Max(MaxTWPwr, MaxTWSWR) + LABEL_PAD * 2;
- LeftInfo := Max(LEFT_INFO_MIN, LabelNeed + 4);
- BX := R.Left + LeftInfo; BW := W - LeftInfo - RIGHT_PAD;
- if BW < 10 then Exit;
- LblZoneL := BX - LabelNeed;
-
- // Те же вертикальные позиции бара что в DrawSMeterWide (44% / 58%)
- Y_BAR_TOP := R.Top + H * 44 div 100;
- Y_BAR_BOT := R.Top + H * 58 div 100;
- Y_BAR_MID := (Y_BAR_TOP + Y_BAR_BOT) div 2;
-
- // Два суб-бара в той же зоне
- PY1 := Y_BAR_TOP;
- PY2 := Y_BAR_MID - 1; // power bar
- SY1 := Y_BAR_MID + 1;
- SY2 := Y_BAR_BOT; // SWR bar
-
- S_WarnX := SWRToX(SWR_HIGH_THRESH);
-
- // Фон
- ACanvas.Brush.Color := CLR_SMETER_BG; ACanvas.Brush.Style := bsSolid;
- ACanvas.Pen.Style := psClear; ACanvas.FillRect(R); ACanvas.Pen.Style := psSolid;
-
- // Фоновые зоны (как зоны зелёный/красный в S-метре)
- FillBar(BX, PY1, BX + BW, PY2, CLR_BAR_GREEN); // power: весь зелёный фон
- FillBar(BX, SY1, S_WarnX, SY2, CLR_BAR_GREEN); // SWR: OK-зона
- FillBar(S_WarnX, SY1, BX + BW, SY2, CLR_BAR_OVER); // SWR: warn-зона
-
- // Активный заполненный бар мощности
- if FPAMaxPower > 0 then PwrPct := FLastFwdW / FPAMaxPower else PwrPct := 0;
- PBarEnd := PwrToX(PwrPct);
- FillBar(BX, PY1 + 1, PBarEnd, PY2 - 1, SV_CLR_METER_ON);
-
- // Активный заполненный бар КСВ
- SBarEnd := SWRToX(FLastSWR);
- if not SWRHigh then
- FillBar(BX, SY1 + 1, SBarEnd, SY2 - 1, SV_CLR_AMBER)
- else
- begin
- FillBar(BX, SY1 + 1, Min(S_WarnX, SBarEnd), SY2 - 1, SV_CLR_AMBER);
- if SBarEnd > S_WarnX then
- FillBar(S_WarnX, SY1 + 1, SBarEnd, SY2 - 1, CLR_SWR_HIGH);
- end;
-
- // Граница KСВ 2.5
- VLine(S_WarnX, SY1, SY2, FTheme.SMeterDivider);
-
- // Рамки обоих баров + левая зона надписей
- ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR;
- ACanvas.Rectangle(LblZoneL, PY1, BX + BW, PY2);
- if SWRHigh then ACanvas.Pen.Color := CLR_SWR_HIGH
- else ACanvas.Pen.Color := CLR_BDR;
- ACanvas.Rectangle(LblZoneL, SY1, BX + BW, SY2);
-
- // Разделители между зоной надписей и шкалой
- ACanvas.Pen.Color := FTheme.SMeterDivider;
- ACanvas.MoveTo(BX, PY1 + 1); ACanvas.LineTo(BX, PY2 - 1);
- ACanvas.MoveTo(BX, SY1 + 1); ACanvas.LineTo(BX, SY2 - 1);
-
- // --- Тики шкалы ---
- ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Brush.Style := bsClear;
-
- // Мощность: промежуточные тики каждые 5% (не на главных метках кратных 25%)
- i := 5;
- while i < 100 do
- begin
- if (i mod 25) <> 0 then
- begin
- X := PwrToX(i / 100.0);
- VLine(X, PY1 - TICK_MED, PY1 - 1, CLR_TICK_GREEN);
- end;
- Inc(i, 5);
- end;
-
- // Мощность: главные тики с подписями (0%, 25%, 50%, 75%, 100%)
- for i := 0 to 4 do
- begin
- X := PwrToX(PWR_MARKS[i]);
- VLine(X, PY1 - TICK_LONG, PY1 - 1, CLR_TICK_WHITE);
- Lbl := Format('%.0f', [PWR_MARKS[i] * FPAMaxPower]);
- TW := ACanvas.TextWidth(Lbl);
- ACanvas.Font.Color := CLR_SCALE_DBM;
- ACanvas.TextOut(X - TW div 2, PY1 - TICK_LONG - ACanvas.TextHeight(Lbl) - 1, Lbl);
- end;
-
- // КСВ: промежуточные тики на целых значениях 2,3,4,6,7,8,9
- for i := 2 to 9 do
- begin
- if i = 5 then Continue;
- X := SWRToX(i);
- if i >= 3 then VLine(X, SY2 + 1, SY2 + TICK_MED, CLR_TICK_BLUE)
- else VLine(X, SY2 + 1, SY2 + TICK_MED, CLR_TICK_GREEN);
- end;
-
- // КСВ: главные тики с подписями (1, 2.5, 5, 10)
- for i := 0 to 3 do
- begin
- X := SWRToX(SWR_MARKS_VAL[i]);
- if SWR_MARKS_VAL[i] >= SWR_HIGH_THRESH then
- VLine(X, SY2 + 1, SY2 + TICK_LONG, CLR_TICK_BLUE)
- else
- VLine(X, SY2 + 1, SY2 + TICK_LONG, CLR_TICK_GREEN);
- Lbl := SWR_MARKS_LBL[i]; TW := ACanvas.TextWidth(Lbl);
- if SWR_MARKS_VAL[i] >= SWR_HIGH_THRESH then
- begin
- if SWRHigh then ACanvas.Font.Color := CLR_SWR_HIGH
- else ACanvas.Font.Color := CLR_SCALE_S_BLUE;
- end else
- ACanvas.Font.Color := CLR_SCALE_S_GREEN;
- ACanvas.TextOut(X - TW div 2, SY2 + TICK_LONG + 1, Lbl);
- end;
-
- // --- Надписи в левой зоне (те же позиции что в DrawSMeterWide) ---
- ACanvas.Brush.Style := bsClear;
- ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; TH10 := ACanvas.TextHeight('0');
- ACanvas.Font.Size := 6; ACanvas.Font.Style := []; TH6 := ACanvas.TextHeight('0');
-
- // Мощность — над верхним баром, как dBm в S-метре
- ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM;
- ACanvas.TextOut(LblZoneL + 4, PY1 - TH10 - 2, LblPwr);
-
- // КСВ — под нижним баром, как S-значение в S-метре
- ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold];
- if SWRHigh then ACanvas.Font.Color := CLR_SWR_HIGH
- else ACanvas.Font.Color := CLR_LABEL_S;
- ACanvas.TextOut(LblZoneL + 4, SY2 + 2, LblSWR);
-
- // "SWR High" рядом с надписью мощности (как "dBm" в S-метре)
- if SWRHigh then
- begin
- ACanvas.Font.Size := 6; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_SWR_HIGH;
- ACanvas.TextOut(LblZoneL + 4 + TWPwr + 2, PY1 - TH6 - 4, 'SWR High');
- end;
+ FRuler.DrawRuler;
end;
// ────────────────────────────────────────────────────────────────────────────
@@ -1665,68 +947,18 @@ begin
end;
procedure TSpectrumView.PaintWaterfall(Sender: TObject);
-var PB: TPaintBox; W, H: Integer;
begin
- PB := TPaintBox(Sender);
- W := PB.Width; H := PB.Height;
- if (W <= 0) or (H <= 0) then Exit;
- if (FWfBitmap <> nil) and (FWfBitmap.Width = W) and (FWfBitmap.Height = H) then
- PB.Canvas.Draw(0, 0, FWfBitmap)
- else begin
- PB.Canvas.Brush.Color := FTheme.Panel;
- PB.Canvas.FillRect(Rect(0, 0, W, H));
- end;
- if FMarkerActive then DrawMarkerLine(PB.Canvas, W, H);
+ FWaterfall.PaintWaterfall(Sender);
end;
procedure TSpectrumView.PaintRuler(Sender: TObject);
-var PB: TPaintBox;
begin
- PB := TPaintBox(Sender);
- DrawRuler;
- if (FRulerBitmap.Width > 0) and (FRulerBitmap.Height > 0) then
- PB.Canvas.Draw(0, 0, FRulerBitmap);
+ FRuler.PaintRuler(Sender);
end;
procedure TSpectrumView.PaintSMeterRight(Sender: TObject);
-var PB: TPaintBox; W, H, ZX1, ZX2, ZY1, ZY2: Integer;
begin
- PB := TPaintBox(Sender);
- W := PB.Width; H := PB.Height;
- if (W <= 0) or (H <= 0) then Exit;
- if (FSmBitmap = nil) or (FSmBitmap.Width <> W) or (FSmBitmap.Height <> H) then
- begin
- FreeAndNil(FSmBitmap);
- FSmBitmap := TBitmap.Create;
- FSmBitmap.SetSize(W, H);
- end;
- if FTransmitting then
- begin
- DrawTXMeter(FSmBitmap.Canvas, Rect(0, 0, W, H));
- end else
- begin
- DrawSMeterWide(FSmBitmap.Canvas, Rect(0, 0, W, H),
- FLastSMeter, FSMeterPeak, FSMeterMin,
- ZX1, ZX2, ZY1, ZY2);
- DrawSMeterZone(FSmBitmap.Canvas, Rect(0, 0, W, H), ZX1, ZX2, ZY1, ZY2);
- end;
- PB.Canvas.Draw(0, 0, FSmBitmap);
-end;
-
-procedure TSpectrumView.PaintFwdPower(Sender: TObject);
-var PB: TPaintBox;
-begin
- PB := TPaintBox(Sender);
- DrawBarMeter(PB.Canvas, Rect(0, 0, PB.Width, PB.Height),
- FLastFwdW, 150, SV_CLR_METER_ON);
-end;
-
-procedure TSpectrumView.PaintSWR(Sender: TObject);
-var PB: TPaintBox;
-begin
- PB := TPaintBox(Sender);
- DrawBarMeter(PB.Canvas, Rect(0, 0, PB.Width, PB.Height),
- FLastSWR - 1.0, 4.0, SV_CLR_AMBER);
+ FSMeter.PaintSMeterRight(Sender);
end;
end.
diff --git a/WaterfallView.pas b/WaterfallView.pas
new file mode 100644
index 0000000..01e25be
--- /dev/null
+++ b/WaterfallView.pas
@@ -0,0 +1,445 @@
+unit WaterfallView;
+
+{
+ WaterfallView.pas — рендеринг водопада.
+ TWaterfallView — изолированный класс, не зависит от SpectrumView.
+}
+
+{$IFDEF FPC}
+ {$MODE Delphi}
+{$ENDIF}
+
+interface
+
+uses
+ Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
+ IntfGraphics, FPImage, LCLIntf, LCLType, GraphType, AppTheme;
+
+type
+ TWaterfallView = class
+ private
+ FWaterfallBitmap: TBitmap;
+ FWfBitmap: TBitmap;
+ FWfBitmapW: Integer;
+ FWfBitmapH: Integer;
+ FWfIntfImg: TLazIntfImage;
+ FWfPixels: array of LongWord;
+ FWaterfallBuf: array[0..1023] of Single;
+ FWaterfallBufCount: Integer;
+ FWaterfallDirty: Boolean;
+ FWfFrameCounter: Integer;
+ FWfFrameInterval: Integer;
+ FWfAGCEnabled: Boolean;
+ FWfNFEnabled: Boolean;
+ FWfManualHigh: Double;
+ FWfManualLow: Double;
+ FWfAGCOffset: Double;
+ FWfHigh: Double;
+ FWfLow: Double;
+ FCenterFreq: Double;
+ FSpanHz: Double;
+ FTXMode: Boolean;
+ FTXFreq: Double;
+ FTXSpanHz: Double;
+ FMarkerActive: Boolean;
+ FMarkerX: Integer;
+ FTheme: TAppTheme;
+ FLightTheme: Boolean;
+ FPbWaterfall: TPaintBox;
+
+ procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
+ public
+ constructor Create;
+ destructor Destroy; override;
+
+ property CenterFreq: Double read FCenterFreq write FCenterFreq;
+ property SpanHz: Double read FSpanHz write FSpanHz;
+ property TXMode: Boolean read FTXMode write FTXMode;
+ property TXFreq: Double read FTXFreq write FTXFreq;
+ property TXSpanHz: Double read FTXSpanHz write FTXSpanHz;
+ property WfAGCEnabled: Boolean read FWfAGCEnabled write FWfAGCEnabled;
+ property WfNFEnabled: Boolean read FWfNFEnabled write FWfNFEnabled;
+ property WfManualHigh: Double read FWfManualHigh write FWfManualHigh;
+ property WfManualLow: Double read FWfManualLow write FWfManualLow;
+ property WfAGCOffset: Double read FWfAGCOffset write FWfAGCOffset;
+ property WaterfallDirty: Boolean read FWaterfallDirty write FWaterfallDirty;
+ property WfFrameInterval: Integer read FWfFrameInterval write FWfFrameInterval;
+ property MarkerActive: Boolean read FMarkerActive write FMarkerActive;
+ property MarkerX: Integer read FMarkerX write FMarkerX;
+ property PbWaterfall: TPaintBox write FPbWaterfall;
+
+ procedure SetWaterfallData(const Pixels: array of Single; Count: Integer);
+ procedure DrawWaterfall;
+ procedure PaintWaterfall(Sender: TObject);
+ procedure SetWaterfallBitmapSize(W, H: Integer);
+ procedure ResetWfAvgBuf;
+ procedure ResetWfBuf;
+ procedure SetTheme(const T: TAppTheme);
+ end;
+
+implementation
+
+// ════════════════════════════════════════════════════════════════════════════
+// Вспомогательные функции
+// ════════════════════════════════════════════════════════════════════════════
+
+function FormatFreqWF(Hz: Double): string;
+var Mhz: Int64; KHz, Rest: Integer;
+begin
+ Mhz := Trunc(Hz / 1000000);
+ KHz := Trunc((Hz - Mhz * 1000000) / 1000);
+ Rest := Trunc(Hz) mod 1000;
+ Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
+end;
+
+function WaterfallEnhancedColorThetis(ValueDB, LowDB, HighDB: Double): LongWord;
+var
+ Overall, Local: Double;
+ R, G, B: Integer;
+begin
+ if ValueDB <= LowDB then begin Result := $FF000000; Exit; end;
+ if ValueDB >= HighDB then begin Result := ($FF shl 24) or (255 shl 16) or (124 shl 8) or 192; Exit; end;
+ Overall := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB);
+ if Overall < (2.0 / 9.0) then
+ begin Local := Overall / (2.0/9.0); R := 0; G := 0; B := Round(Local * 255.0); end
+ else if Overall < (3.0 / 9.0) then
+ begin Local := (Overall - 2.0/9.0) / (1.0/9.0); R := 0; G := Round(Local*255.0); B := 255; end
+ else if Overall < (4.0 / 9.0) then
+ begin Local := (Overall - 3.0/9.0) / (1.0/9.0); R := 0; G := 255; B := Round((1.0-Local)*255.0); end
+ else if Overall < (5.0 / 9.0) then
+ begin Local := (Overall - 4.0/9.0) / (1.0/9.0); R := Round(Local*255.0); G := 255; B := 0; end
+ else if Overall < (7.0 / 9.0) then
+ begin Local := (Overall - 5.0/9.0) / (2.0/9.0); R := 255; G := Round((1.0-Local)*255.0); B := 0; end
+ else if Overall < (8.0 / 9.0) then
+ begin Local := (Overall - 7.0/9.0) / (1.0/9.0); R := 255; G := 0; B := Round(Local*255.0); end
+ else
+ begin
+ Local := (Overall - 8.0/9.0) / (1.0/9.0);
+ R := Round((0.75 + 0.25 * (1.0 - Local)) * 255.0);
+ G := Round(Local * 255.0 * 0.5);
+ B := 255;
+ end;
+ R := EnsureRange(R, 0, 255);
+ G := EnsureRange(G, 0, 255);
+ B := EnsureRange(B, 0, 255);
+ Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B;
+end;
+
+function WaterfallLightTheme(ValueDB, LowDB, HighDB: Double): LongWord;
+const
+ NSTOPS = 8;
+ SR: array[0..NSTOPS-1] of Integer = (224, 160, 20, 0, 0, 220, 255, 255);
+ SG: array[0..NSTOPS-1] of Integer = (230, 185, 80, 185, 200, 210, 90, 20);
+ SB: array[0..NSTOPS-1] of Integer = (232, 210, 200, 210, 80, 0, 0, 20);
+var
+ T: Double;
+ Seg: Integer;
+ R, G, B: Integer;
+begin
+ if ValueDB <= LowDB then begin Result := $FFE0E6E8; Exit; end;
+ if ValueDB >= HighDB then begin Result := ($FF shl 24) or (SR[NSTOPS-1] shl 16) or (SG[NSTOPS-1] shl 8) or SB[NSTOPS-1]; Exit; end;
+ T := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB) * (NSTOPS - 1);
+ Seg := Min(NSTOPS - 2, Trunc(T));
+ T := T - Seg;
+ R := EnsureRange(Round(SR[Seg] * (1.0 - T) + SR[Seg+1] * T), 0, 255);
+ G := EnsureRange(Round(SG[Seg] * (1.0 - T) + SG[Seg+1] * T), 0, 255);
+ B := EnsureRange(Round(SB[Seg] * (1.0 - T) + SB[Seg+1] * T), 0, 255);
+ Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B;
+end;
+
+procedure InitRawDesc32WF(var Desc: TRawImageDescription; AWidth, AHeight: Integer);
+begin
+ FillChar(Desc, SizeOf(Desc), 0);
+ Desc.Format := ricfRGBA;
+ Desc.Width := AWidth;
+ Desc.Height := AHeight;
+ Desc.Depth := 32;
+ Desc.BitOrder := riboBitsInOrder;
+ Desc.ByteOrder := riboLSBFirst;
+ Desc.LineOrder := riloTopToBottom;
+ Desc.BitsPerPixel := 32;
+ Desc.LineEnd := rileDWordBoundary;
+ Desc.BlueShift := 0; Desc.BluePrec := 8;
+ Desc.GreenShift := 8; Desc.GreenPrec := 8;
+ Desc.RedShift := 16; Desc.RedPrec := 8;
+ Desc.AlphaShift := 24; Desc.AlphaPrec := 8;
+end;
+
+// ════════════════════════════════════════════════════════════════════════════
+// TWaterfallView
+// ════════════════════════════════════════════════════════════════════════════
+
+constructor TWaterfallView.Create;
+begin
+ inherited Create;
+ FWaterfallBitmap := TBitmap.Create;
+ FWfBitmap := TBitmap.Create;
+ FWfIntfImg := nil;
+ FWfBitmapW := 0;
+ FWfBitmapH := 0;
+ FWfManualHigh := -80.0;
+ FWfManualLow := -130.0;
+ FWfAGCOffset := 0.0;
+ FWfHigh := -80.0;
+ FWfLow := -130.0;
+ FWfAGCEnabled := True;
+ FWfNFEnabled := False;
+ FWaterfallBufCount := 1024;
+ FWfFrameInterval := 2;
+ FWfFrameCounter := 0;
+ FWaterfallDirty := True;
+ FTheme := DarkTheme;
+ FLightTheme := False;
+ FSpanHz := 192000;
+ ResetWfBuf;
+end;
+
+destructor TWaterfallView.Destroy;
+begin
+ FWaterfallBitmap.Free;
+ FWfBitmap.Free;
+ FreeAndNil(FWfIntfImg);
+ inherited;
+end;
+
+procedure TWaterfallView.SetTheme(const T: TAppTheme);
+begin
+ FTheme := T;
+ FLightTheme := T.BG > TColor($00808080);
+ FWaterfallDirty := True;
+ // Сброс размера — чтобы следующий кадр перезаполнил фон новой темой
+ FWfBitmapW := 0;
+ FWfBitmapH := 0;
+ SetLength(FWfPixels, 0);
+ FreeAndNil(FWfIntfImg);
+end;
+
+procedure TWaterfallView.ResetWfAvgBuf;
+begin
+ FWfHigh := FWfManualHigh;
+ FWfLow := FWfManualLow;
+ FWaterfallDirty := True;
+end;
+
+procedure TWaterfallView.ResetWfBuf;
+var i: Integer;
+begin
+ for i := 0 to 1023 do FWaterfallBuf[i] := -130.0;
+ FWfHigh := FWfManualHigh;
+ FWfLow := FWfManualLow;
+ FWaterfallDirty := True;
+end;
+
+procedure TWaterfallView.SetWaterfallData(const Pixels: array of Single; Count: Integer);
+var i, N: Integer;
+begin
+ N := Min(Count, 1024);
+ for i := 0 to N - 1 do FWaterfallBuf[i] := Pixels[i];
+ FWaterfallBufCount := N;
+ Inc(FWfFrameCounter);
+ if FWfFrameCounter >= Max(1, FWfFrameInterval) then
+ begin
+ FWfFrameCounter := 0;
+ FWaterfallDirty := True;
+ end;
+end;
+
+procedure TWaterfallView.SetWaterfallBitmapSize(W, H: Integer);
+begin
+ FWaterfallBitmap.SetSize(W, H);
+ FWfBitmapW := 0;
+ FWfBitmapH := 0;
+ SetLength(FWfPixels, 0);
+ FreeAndNil(FWfIntfImg);
+end;
+
+procedure TWaterfallView.DrawMarkerLine(C: TCanvas; W, H: Integer);
+var MX: Integer; MarkerFreq: Double; MarkerLbl: string;
+begin
+ MX := Round(FMarkerX / 1000.0 * W);
+ if (MX < 0) or (MX >= W) then Exit;
+ MarkerFreq := (FCenterFreq - FSpanHz / 2) + FMarkerX / 1000.0 * FSpanHz;
+ MarkerLbl := FormatFreqWF(Round(MarkerFreq));
+ C.Pen.Color := TColor($004444FF);
+ C.Pen.Width := 1; C.Pen.Style := psSolid;
+ C.MoveTo(MX, 0); C.LineTo(MX, H);
+ C.Font.Color := TColor($004444FF);
+ C.Font.Size := 7; C.Font.Name := 'Courier New';
+ if MX + 4 + C.TextWidth(MarkerLbl) < W then
+ C.TextOut(MX + 4, 4, MarkerLbl)
+ else
+ C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl);
+end;
+
+// ────────────────────────────────────────────────────────────────────────────
+// DrawWaterfall
+// ────────────────────────────────────────────────────────────────────────────
+
+procedure TWaterfallView.DrawWaterfall;
+var
+ W, H, X: Integer;
+ dB, frac, WatSrcF: Double;
+ WatS0, WatS1: Integer;
+ TargetLow, TargetHigh: Double;
+ HistMinDB, HistMaxDB, HistStepDB: Double;
+ NoiseFloorDB, SignalTopDB: Double;
+ CumCount, LowTargetCount, HighTargetCount, HistIdx: Integer;
+ WfHigh, WfLow, InvRange, Step: Double;
+ Px: PLongWord;
+ Desc: TRawImageDescription;
+ Row, Col: Integer; RowPtr: PByte; Src: PLongWord; BPPi: Integer;
+ Pal: LongWord; Hist: array[0..191] of Integer; SrcCount: Integer;
+const
+ ALPHA_HIGH = 0.10; ALPHA_LOW = 0.08;
+ WF_AUTO_OFFSET = -4.0; WF_MIN_RANGE = 48.0; WF_MAX_RANGE = 62.0;
+begin
+ if FWaterfallBitmap = nil then Exit;
+ W := FWaterfallBitmap.Width; H := FWaterfallBitmap.Height;
+ if (W <= 0) or (H <= 0) then Exit;
+ SrcCount := EnsureRange(FWaterfallBufCount, 2, 1024);
+
+ FillChar(Hist, SizeOf(Hist), 0);
+ HistMinDB := -170.0; HistMaxDB := 22.0;
+ HistStepDB := (HistMaxDB - HistMinDB) / Length(Hist);
+ for X := 0 to SrcCount - 1 do
+ begin
+ HistIdx := EnsureRange(Trunc((FWaterfallBuf[X] - HistMinDB) / HistStepDB), 0, High(Hist));
+ Inc(Hist[HistIdx]);
+ end;
+
+ LowTargetCount := Round(SrcCount * 0.30);
+ HighTargetCount := Round(SrcCount * 0.98);
+ CumCount := 0; NoiseFloorDB := FWfLow; SignalTopDB := FWfHigh;
+ for HistIdx := 0 to High(Hist) do
+ begin
+ CumCount := CumCount + Hist[HistIdx];
+ if CumCount >= LowTargetCount then
+ begin NoiseFloorDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; Break; end;
+ end;
+ CumCount := 0;
+ for HistIdx := 0 to High(Hist) do
+ begin
+ CumCount := CumCount + Hist[HistIdx];
+ if CumCount >= HighTargetCount then
+ begin SignalTopDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; Break; end;
+ end;
+
+ if FWfAGCEnabled then
+ begin
+ TargetLow := NoiseFloorDB + WF_AUTO_OFFSET + FWfAGCOffset;
+ if FWfNFEnabled then
+ TargetHigh := Max(TargetLow + WF_MIN_RANGE, SignalTopDB + 6.0)
+ else
+ TargetHigh := TargetLow + 52.0;
+ if TargetHigh > TargetLow + WF_MAX_RANGE then TargetHigh := TargetLow + WF_MAX_RANGE;
+ FWfLow := FWfLow + ALPHA_LOW * (TargetLow - FWfLow);
+ FWfHigh := FWfHigh + ALPHA_HIGH * (TargetHigh - FWfHigh);
+ end;
+ WfHigh := FWfHigh; WfLow := FWfLow;
+ if not FWfAGCEnabled then begin WfHigh := FWfManualHigh; WfLow := FWfManualLow; end;
+ if WfHigh < WfLow + 40.0 then WfHigh := WfLow + 40.0;
+ if WfHigh > 0.0 then WfHigh := 0.0;
+ if WfLow < -160 then WfLow := -160;
+ InvRange := 255.0 / (WfHigh - WfLow);
+
+ if (FWfBitmapW <> W) or (FWfBitmapH <> H) then
+ begin
+ FWfBitmapW := W; FWfBitmapH := H;
+ SetLength(FWfPixels, W * H);
+ Pal := $FF000000
+ or ((LongWord(FTheme.Panel) and $FF) shl 16)
+ or (LongWord(FTheme.Panel) and $FF00)
+ or ((LongWord(FTheme.Panel) shr 16) and $FF);
+ FillDWord(FWfPixels[0], W * H, Pal);
+ FWfBitmap.SetSize(W, H);
+ FreeAndNil(FWfIntfImg);
+ end;
+
+ if H > 1 then Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W);
+
+ Step := (SrcCount - 1.0) / Max(1, W - 1);
+ WatSrcF := 0.0; Px := @FWfPixels[0];
+ for X := 0 to W - 1 do
+ begin
+ if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then
+ begin
+ dB := FCenterFreq - FSpanHz * 0.5 + X * FSpanHz / Max(1, W - 1) - FTXFreq;
+ if (dB < -FTXSpanHz * 0.5) or (dB > FTXSpanHz * 0.5) then
+ begin
+ if H > 1 then Px^ := FWfPixels[X + W]
+ else Px^ := 0;
+ Inc(Px);
+ WatSrcF := WatSrcF + Step;
+ Continue;
+ end;
+ frac := (dB + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1);
+ WatS0 := Trunc(frac);
+ if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
+ WatS1 := WatS0 + 1;
+ frac := frac - WatS0;
+ dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
+ end else
+ begin
+ WatS0 := Trunc(WatSrcF);
+ if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
+ WatS1 := WatS0 + 1;
+ frac := WatSrcF - WatS0;
+ dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
+ end;
+ if FLightTheme then
+ Pal := WaterfallLightTheme(dB, WfLow, WfHigh)
+ else
+ Pal := WaterfallEnhancedColorThetis(dB, WfLow, WfHigh);
+ Px^ := Pal; Inc(Px);
+ WatSrcF := WatSrcF + Step;
+ end;
+
+ if FWfIntfImg = nil then
+ begin
+ FWfIntfImg := TLazIntfImage.Create(W, H);
+ InitRawDesc32WF(Desc, W, H);
+ FWfIntfImg.DataDescription := Desc;
+ FWfIntfImg.CreateData;
+ end;
+ BPPi := FWfIntfImg.DataDescription.BitsPerPixel div 8;
+ if (BPPi = 4) and (FWfIntfImg.PixelData <> nil) then
+ Move(FWfPixels[0], FWfIntfImg.PixelData^, W * H * 4)
+ else
+ begin
+ Src := @FWfPixels[0];
+ for Row := 0 to H - 1 do
+ begin
+ RowPtr := FWfIntfImg.GetDataLineStart(Row);
+ if RowPtr = nil then begin Inc(Src, W); Continue; end;
+ for Col := 0 to W - 1 do
+ begin
+ RowPtr[0] := Byte(Src^); RowPtr[1] := Byte(Src^ shr 8);
+ RowPtr[2] := Byte(Src^ shr 16);
+ if BPPi >= 4 then RowPtr[3] := $FF;
+ Inc(Src); Inc(RowPtr, BPPi);
+ end;
+ end;
+ end;
+ FWfBitmap.LoadFromIntfImage(FWfIntfImg);
+end;
+
+// ────────────────────────────────────────────────────────────────────────────
+// PaintWaterfall
+// ────────────────────────────────────────────────────────────────────────────
+
+procedure TWaterfallView.PaintWaterfall(Sender: TObject);
+var PB: TPaintBox; W, H: Integer;
+begin
+ PB := TPaintBox(Sender);
+ W := PB.Width; H := PB.Height;
+ if (W <= 0) or (H <= 0) then Exit;
+ if (FWfBitmap <> nil) and (FWfBitmap.Width = W) and (FWfBitmap.Height = H) then
+ PB.Canvas.Draw(0, 0, FWfBitmap)
+ else begin
+ PB.Canvas.Brush.Color := FTheme.Panel;
+ PB.Canvas.FillRect(Rect(0, 0, W, H));
+ end;
+ if FMarkerActive then DrawMarkerLine(PB.Canvas, W, H);
+end;
+
+end.
diff --git a/ewsdr.lpi b/ewsdr.lpi
index dffb9d6..752b758 100644
--- a/ewsdr.lpi
+++ b/ewsdr.lpi
@@ -167,6 +167,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+