Split SpectrumView into WaterfallView, SMeterView, RulerView modules

TSpectrumView remains the public facade for MainForm; waterfall, S-meter,
and ruler rendering are now in isolated units. Dead code removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 11:16:55 +03:00
co-authored by Claude Sonnet 4.6
parent dede74d0a5
commit d99c19ec9a
5 changed files with 1454 additions and 1006 deletions
+248
View File
@@ -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.