Files
ewsdr/FreqDisplay.pas
ew8bakandClaude Sonnet 4.6 efc21b92e6 Add configurable VFO frequency range with GHz support
- Settings > Display: new "VFO Display" control to select max frequency
  range (999 MHz / 9.999 GHz / 99.999 GHz), persisted as freq_mhz_digits
- FreqDisplay: extend digit map [0..10] to support up to 11 digit positions;
  keyboard navigation adapts to MinMhzDigits dynamically
- MainForm/LayoutTopVfoBlock: VFO panel width scales automatically with
  selected digit count using dynamic text measurement
- Web VFO: fix broken HTML (class attr not closed before data-s — wheel and
  hover were completely non-functional); rewrite fmtVfo to use variable MHz
  digit count synced from server state; fix missing </span> for .vfo-hz
- WebServer: publish freq_mhz_digits in state JSON; updated from
  ApplyFreqMhzDigits so desktop and web stay in sync
- Fix Int32 overflow on frequencies above ~2.15 GHz: CATEngine.Pad V param
  Int64, FormatFreq/FormatFreqSV Mhz var Int64 (Mhz*1000000 overflowed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 13:13:52 +03:00

426 lines
11 KiB
ObjectPascal

unit FreqDisplay;
{
TFreqDisplay — цифровой дисплей частоты с управлением по разрядам
===================================================================
Отображает частоту в Гц вида 14.201.123
Наводишь мышь на цифру → подсветка разряда.
Колёсико мыши → меняет выделенный разряд (+/- 10^N).
Стрелки Left/Right → переключают активный разряд.
Стрелки Up/Down → меняют активный разряд.
}
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Controls, Graphics, LCLType;
type
TFreqChangeEvent = procedure(Sender: TObject; NewFreq: Int64) of object;
TFreqDisplay = class(TCustomControl)
private
FFrequency: Int64;
FMinFreq: Int64;
FMaxFreq: Int64;
FFontSize: Integer;
FFontName: string;
FColorNormal: TColor;
FColorHover: TColor;
FColorHoverBG: TColor;
FColorDim: TColor;
FMinMhzDigits: Integer;
FDimLeadingZeros: Boolean;
FCenterText: Boolean;
FOnChange: TFreqChangeEvent;
FHoverDigit: Integer; // 0=единицы .. 10=10ГГц, -1=нет
FDigitX: array[0..10] of Integer; // X левого края каждой цифры
FDigitW: Integer;
FCharH: Integer;
procedure SetFrequency(V: Int64);
procedure SetFontSize(V: Integer);
function ClampFreq(V: Int64): Int64;
function DigitAtX(X: Integer): Integer;
function DigitStep(D: Integer): Int64;
procedure ChangeByDigit(D, Delta: Integer);
procedure BuildDigitMap(const S: string; StartX: Integer);
protected
procedure Paint; override;
procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
procedure MouseLeave; override;
function DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
MousePos: TPoint): Boolean; override;
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
procedure Click; override;
public
constructor Create(AOwner: TComponent); override;
property Frequency: Int64 read FFrequency write SetFrequency;
property MinFreq: Int64 read FMinFreq write FMinFreq;
property MaxFreq: Int64 read FMaxFreq write FMaxFreq;
property FontSize: Integer read FFontSize write SetFontSize;
property FontName: string read FFontName write FFontName;
property ColorNormal: TColor read FColorNormal write FColorNormal;
property ColorHover: TColor read FColorHover write FColorHover;
property ColorHoverBG: TColor read FColorHoverBG write FColorHoverBG;
property ColorDim: TColor read FColorDim write FColorDim;
property MinMhzDigits: Integer read FMinMhzDigits write FMinMhzDigits;
property DimLeadingZeros: Boolean read FDimLeadingZeros write FDimLeadingZeros;
property CenterText: Boolean read FCenterText write FCenterText;
property OnChange: TFreqChangeEvent read FOnChange write FOnChange;
end;
implementation
constructor TFreqDisplay.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FFrequency := 14200000;
FMinFreq := 0;
FMaxFreq := 2000000000;
FFontSize := 20;
FFontName := 'Courier New';
FColorNormal := TColor($00E8F0FF);
FColorHover := TColor($0040DDFF);
FColorHoverBG := TColor($00182838);
FColorDim := TColor($00607080);
FMinMhzDigits := 1;
FDimLeadingZeros := False;
FCenterText := True;
FHoverDigit := -1;
FDigitW := 14;
FCharH := 24;
TabStop := True;
Width := 220;
Height := 40;
end;
function TFreqDisplay.ClampFreq(V: Int64): Int64;
begin
if V < FMinFreq then Result := FMinFreq
else if V > FMaxFreq then Result := FMaxFreq
else Result := V;
end;
function TFreqDisplay.DigitStep(D: Integer): Int64;
var
i: Integer;
Result_: Int64;
begin
Result_ := 1;
for i := 0 to D - 1 do
Result_ := Result_ * 10;
Result := Result_;
end;
function TFreqDisplay.DigitAtX(X: Integer): Integer;
var
i: Integer;
begin
Result := -1;
if FDigitW = 0 then Exit;
for i := 0 to 10 do
if (X >= FDigitX[i]) and (X < FDigitX[i] + FDigitW) then
begin
Result := i;
Exit;
end;
end;
// Строим карту FDigitX из строки вида "14.201.123"
// Цифры нумеруются справа налево: digit0=единицы, digit8=сотни млн
procedure TFreqDisplay.BuildDigitMap(const S: string; StartX: Integer);
var
Xs: array[0..13] of Integer; // X каждого символа строки (макс 14 симв)
ci: Integer;
xi: Integer;
dIdx: Integer;
len: Integer;
begin
len := Length(S);
if len > 13 then len := 13;
xi := StartX;
for ci := 0 to len - 1 do
begin
Xs[ci] := xi;
Inc(xi, Canvas.TextWidth(S[ci + 1]));
end;
// Назначаем digit индексы справа налево, пропуская точки
dIdx := 0;
for ci := len - 1 downto 0 do
begin
if S[ci + 1] <> '.' then
begin
if dIdx <= 10 then
FDigitX[dIdx] := Xs[ci];
Inc(dIdx);
end;
end;
end;
procedure TFreqDisplay.Paint;
var
C: TCanvas;
S: string;
Hz: Int64;
Mhz: Integer;
KHz: Integer;
Ones: Integer;
MhzFmt: string;
MhzText: string;
LeadingMhzZeros: Integer;
TotalW: Integer;
StartX: Integer;
ChW, ChH: Integer;
xi: Integer;
ci: Integer;
ch: Char;
dIdx: Integer;
col: TColor;
DiChar: array[0..13] of Integer;
tmpIdx: Integer;
len: Integer;
begin
C := Canvas;
// Фон — прозрачный (наследует от Panel)
C.Brush.Style := bsClear;
C.FillRect(ClientRect);
C.Font.Name := FFontName;
C.Font.Size := FFontSize;
C.Font.Bold := True;
C.Font.Style := [fsBold];
ChW := C.TextWidth('0');
ChH := C.TextHeight('0');
FDigitW := ChW;
FCharH := ChH;
Hz := Abs(FFrequency);
Mhz := Hz div 1000000;
KHz := (Hz div 1000) mod 1000;
Ones := Hz mod 1000;
if FMinMhzDigits > 1 then
MhzFmt := '%.' + IntToStr(FMinMhzDigits) + 'd'
else
MhzFmt := '%d';
MhzText := Format(MhzFmt, [Mhz]);
S := MhzText + Format('.%3.3d.%3.3d', [KHz, Ones]);
LeadingMhzZeros := 0;
while FDimLeadingZeros and
(LeadingMhzZeros < Length(MhzText) - 1) and
(MhzText[LeadingMhzZeros + 1] = '0') do
Inc(LeadingMhzZeros);
TotalW := C.TextWidth(S);
if FCenterText then
StartX := (Width - TotalW) div 2
else
StartX := 2;
if StartX < 2 then StartX := 2;
// Строим карту digit → X
BuildDigitMap(S, StartX);
// Строим обратную карту символ → digit index
len := Length(S);
if len > 13 then len := 13;
tmpIdx := 0;
for ci := len - 1 downto 0 do
begin
if S[ci + 1] <> '.' then
begin
DiChar[ci] := tmpIdx;
Inc(tmpIdx);
end
else
DiChar[ci] := -1;
end;
// Рисуем
xi := StartX;
for ci := 0 to len - 1 do
begin
ch := S[ci + 1];
dIdx := DiChar[ci];
if ch = '.' then
col := FColorDim
else if FDimLeadingZeros and (ci < LeadingMhzZeros) then
col := FColorDim
else if dIdx = FHoverDigit then
col := FColorHover
else
col := FColorNormal;
// Фоновая подсветка активного разряда
if (dIdx >= 0) and (dIdx = FHoverDigit) then
begin
C.Brush.Color := FColorHoverBG;
C.Brush.Style := bsSolid;
C.FillRect(Rect(xi - 1, 2, xi + ChW + 1, Height - 2));
C.Brush.Style := bsClear;
end;
C.Font.Color := col;
C.Brush.Style := bsClear;
C.TextOut(xi, (Height - ChH) div 2, ch);
Inc(xi, C.TextWidth(ch));
end;
// Рамка фокуса
if Focused then
begin
C.Pen.Color := TColor($00004060);
C.Pen.Style := psDot;
C.Pen.Width := 1;
C.Brush.Style := bsClear;
C.Rectangle(1, 1, Width - 2, Height - 2);
C.Pen.Style := psSolid;
end;
end;
procedure TFreqDisplay.ChangeByDigit(D, Delta: Integer);
var
Step: Int64;
Base: Int64;
NewFreq: Int64;
begin
if D < 0 then Exit;
Step := DigitStep(D);
// Нижняя граница текущего разряда (кратная Step)
Base := (FFrequency div Step) * Step;
if Delta > 0 then
begin
// Вверх: от нижней границы + шаг
// 7.129.054 + 100 → base=7.129.000 → 7.129.000 + 100 = 7.129.100
NewFreq := Base + Step * Delta;
end
else
begin
// Вниз:
// Если уже кратна — просто шагаем: 7.129.100 - 100 = 7.129.000
// Если не кратна — возвращаем нижнюю границу: 7.129.054 → 7.129.000
if FFrequency = Base then
NewFreq := Base + Step * Delta // кратна → шагаем
else
NewFreq := Base; // не кратна → снэп к нижней границе
end;
NewFreq := ClampFreq(NewFreq);
if NewFreq <> FFrequency then
begin
FFrequency := NewFreq;
Invalidate;
if Assigned(FOnChange) then
FOnChange(Self, FFrequency);
end;
end;
procedure TFreqDisplay.MouseMove(Shift: TShiftState; X, Y: Integer);
var
d: Integer;
begin
inherited;
d := DigitAtX(X);
if d <> FHoverDigit then
begin
FHoverDigit := d;
Invalidate;
end;
if d >= 0 then Cursor := crHandPoint
else Cursor := crDefault;
end;
procedure TFreqDisplay.MouseLeave;
begin
inherited;
if FHoverDigit <> -1 then
begin
FHoverDigit := -1;
Invalidate;
end;
Cursor := crDefault;
end;
function TFreqDisplay.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
MousePos: TPoint): Boolean;
var
d: Integer;
Delta: Integer;
Local: TPoint;
begin
Result := False;
Local := ScreenToClient(MousePos);
d := FHoverDigit;
if d < 0 then
d := DigitAtX(Local.X);
if d >= 0 then
begin
if WheelDelta > 0 then Delta := 1 else Delta := -1;
ChangeByDigit(d, Delta);
Result := True; // обработали — не передаём дальше
end;
if not Result then
Result := inherited DoMouseWheel(Shift, WheelDelta, MousePos);
end;
procedure TFreqDisplay.KeyDown(var Key: Word; Shift: TShiftState);
var
d: Integer;
begin
d := FHoverDigit;
if d < 0 then d := 3; // по умолчанию — кГц
case Key of
VK_UP: begin ChangeByDigit(d, +1); Key := 0; end;
VK_DOWN: begin ChangeByDigit(d, -1); Key := 0; end;
VK_LEFT: begin
if FHoverDigit < 0 then FHoverDigit := 3;
if FHoverDigit < FMinMhzDigits + 5 then Inc(FHoverDigit);
Invalidate; Key := 0;
end;
VK_RIGHT: begin
if FHoverDigit < 0 then FHoverDigit := 3;
if FHoverDigit > 0 then Dec(FHoverDigit);
Invalidate; Key := 0;
end;
end;
inherited KeyDown(Key, Shift);
end;
procedure TFreqDisplay.Click;
begin
inherited;
SetFocus;
end;
procedure TFreqDisplay.SetFrequency(V: Int64);
begin
V := ClampFreq(V);
if V <> FFrequency then
begin
FFrequency := V;
Invalidate;
end;
end;
procedure TFreqDisplay.SetFontSize(V: Integer);
begin
if V <> FFontSize then
begin
FFontSize := V;
Invalidate;
end;
end;
end.