Files
ewsdr/SMeterView.pas
ew8bakandClaude Opus 5 f99443620a feat(ui): настройки показывают только то, что есть у активного железа
В TBackendCaps восемь флагов заполнялись обоими бэкендами и не читались нигде,
а в ApplyBackendCapsToUI на этом месте стояла расписка «(TODO) гейтинг полей,
которых нет у бэкенда». Из-за этого на AD936x оператору предлагали крутить то,
чего у платы нет, — и хуже всего страница PA: калибровка драйва по диапазонам
там просто ничего не делает (мощность задаётся железной аттенюацией, drive-байта
нет вовсе), но выглядела рабочей.

Теперь гейт идёт по возможностям, а не по виду устройства:
- HasPA — потолок мощности и калибровки драйва (HF + трансвертеры) против
  «TX att @100% drive»; заголовок и подзаголовок страницы следуют содержимому;
- HasHWMic — строка микрофонного разъёма (DUC Specific байт 50). Mic gain
  остаётся: он идёт в WDSP и работает с любым источником, включая звук с ПК,
  поэтому поднимается в освободившуюся строку;
- HasWideband — галки широкополосного АЦП; сам режим принудительно гасится,
  иначе сохранённый в конфиге wideband оставлял бы пустую панель без способа
  её выключить;
- HasDitherRandom — группа ADC целиком, Web Server встаёт на её место.
Вкладка QO-100 убрана на openHPSDR: TRadioController.InQO100 требует IsPluto,
то есть транспондер там недостижим. Симметрично OC Control, которого нет у
AD936x.

Измеритель: TSMeterView.HasPATelemetry. Без моста детектора FLastFwdW/FLastSWR
стоят на значениях инициализации, и «0W / SWR 1.0» на передаче — не показание,
а выдумка. Шкала рисуется пустой: деления и рамка на месте, заливки нет, ватты
на делениях не подписаны (они считаются от Max power, которого у AD936x нет).

★Отдельно — дефект раскладки, который всё это вскрыло. Перекладка страницы
настроек ломается в двух случаях, и оба случались при смене трансивера на живой
программе (Stop/Discover/Connect/Start), а не при старте:

1. Страница ПРОКРУЧЕНА. TScrollBox двигает детей, и SetBounds задаёт координаты
   относительно прокрученного клиента: всё уезжало вверх, а первая группа
   оставалась на месте — под переключателями Profile/EQ/Hardware зияла дыра.
   Лечение — сброс прокрутки первой строкой (так уже делал RelayoutCalTab).
2. Страница СПРЯТАНА. Тогда сброс не доходит до виджета, а сохранённое смещение
   возвращается при показе — и дыра та же. А попадает перекладка почти всегда
   именно на спрятанную страницу: гейты отрабатывают при открытии окна, когда
   активна одна вкладка, а до Transmit/Calibration оператор доходит потом.
   Лечение — перекладка спрятанной страницы взводит флаг и выходит, а SelectPage
   исполняет её в момент показа (RunDueRelayout). Гард стоит внутри каждой
   Relayout*, а не у вызывающих: у RelayoutCalTab их два, и второй
   (LoadXvtrSettings) приходит как раз на спрятанную страницу.

Проверено офскрин-рендером окна настроек в обе стороны (openHPSDR ↔ AD936x) для
Transmit/Power Amplifier/Calibration/Advanced, в том числе на прокрученной и
спрятанной странице. При полном железе координаты совпадают со старыми.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M37eCTZZ9aqppKWkZRxXkP
2026-08-25 10:16:31 +03:00

554 lines
22 KiB
ObjectPascal
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, PlatformUtils;
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;
// Есть ли у железа мост детектора мощности (Caps.HasPA). У AD936x его нет:
// FLastFwdW/FLastSWR так и стоят на значениях инициализации, и «0W / SWR
// 1.0» на передаче — не показание, а выдумка. Шкала в этом случае рисуется
// пустой: деления есть, заливки и цифр нет.
FHasPATelemetry: Boolean;
FTransmitting: Boolean;
FPbSMeterRight: TPaintBox;
// Масштаб канвы: 2 на Retina, 1 на Windows/Linux. Битмап растёт во столько
// же раз, пиксельные константы и шрифты умножаются на него.
FScale: Integer;
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 HasPATelemetry: Boolean read FHasPATelemetry write FHasPATelemetry;
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;
FHasPATelemetry := True;
FTransmitting := False;
FScale := 1;
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;
Sc: Integer; // масштаб канвы, см. FScale
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;
Sc := FScale;
W := R.Right - R.Left; H := R.Bottom - R.Top;
ZX1 := 0; ZX2 := 0; ZY1 := 0; ZY2 := 0;
if (W < 80 * Sc) or (H < 16 * Sc) 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 * Sc; TWDbm := ACanvas.TextWidth(LblDbm);
MaxTWDbm := ACanvas.TextWidth('-120.0');
MaxTWS := ACanvas.TextWidth('S9+60');
ACanvas.Font.Size := 6 * Sc; ACanvas.Font.Style := [];
LabelNeed := Max(MaxTWDbm + 2 * Sc + ACanvas.TextWidth('dBm '), MaxTWS) + LABEL_PAD * 2 * Sc;
LeftInfo := Max(LEFT_INFO_MIN * Sc, LabelNeed + 4 * Sc);
BX := R.Left + LeftInfo; BW := W - LeftInfo - RIGHT_PAD * Sc;
if BW < 10 * Sc 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;
// Перо в физических пикселях: иначе на Retina все риски станут вдвое тоньше.
ACanvas.Pen.Width := Sc;
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 + Sc; ZY2 := Y_BAR_BOT - Sc;
if BarEnd > BX then
begin
if BarEnd <= S9X then
FillBar(BX, Y_BAR_TOP+Sc, BarEnd, Y_BAR_BOT-Sc, CLR_BAR_GREEN)
else begin
FillBar(BX, Y_BAR_TOP+Sc, S9X, Y_BAR_BOT-Sc, CLR_BAR_GREEN);
FillBar(S9X, Y_BAR_TOP+Sc, BarEnd, Y_BAR_BOT-Sc, CLR_BAR_OVER);
end;
end;
if (PeakRight > BX) and (PeakRight <= BX + BW) then
begin
ACanvas.Pen.Color := CLR_PEAK_MARKER; ACanvas.Pen.Width := 2 * Sc;
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 := Sc;
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 + Sc); ACanvas.LineTo(BX, Y_BAR_BOT - Sc);
ACanvas.Font.Size := 6 * Sc; 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 * Sc, Y_BAR_TOP - Sc, 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 * Sc - ACanvas.TextHeight(Lbl) - Sc, Lbl);
end;
i := -125;
while i < -13 do
begin
X := DBtoX(i); VLine(X, Y_BAR_TOP - TICK_MED * Sc, Y_BAR_TOP - Sc, 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+Sc, Y_BAR_BOT+TICK_LONG*Sc, CLR_TICK_BLUE)
else VLine(X, Y_BAR_BOT+Sc, Y_BAR_BOT+TICK_LONG*Sc, 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 * Sc + Sc, Lbl);
end;
ACanvas.Brush.Style := bsClear;
ACanvas.Font.Size := 10 * Sc; ACanvas.Font.Style := [fsBold]; TH10 := ACanvas.TextHeight('0');
ACanvas.Font.Size := 6 * Sc; ACanvas.Font.Style := []; TH6 := ACanvas.TextHeight('0');
ACanvas.Font.Size := 10 * Sc; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM;
ACanvas.TextOut(LblZoneL + 4 * Sc, Y_BAR_TOP - TH10 - 2 * Sc, LblDbm);
ACanvas.Font.Size := 6 * Sc; ACanvas.Font.Style := []; ACanvas.Font.Color := CLR_TICK_BLUE;
ACanvas.TextOut(LblZoneL + 4 * Sc + TWDbm + 2 * Sc, Y_BAR_TOP - TH6 - 4 * Sc, 'dBm');
ACanvas.Font.Size := 10 * Sc; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_S;
ACanvas.TextOut(LblZoneL + 4 * Sc, Y_BAR_BOT + 2 * Sc, 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;
Sc: Integer; // масштаб канвы, см. FScale
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);
Sc := FScale;
W := R.Right - R.Left; H := R.Bottom - R.Top;
if (W < 80 * Sc) or (H < 16 * Sc) then Exit;
SWRHigh := FHasPATelemetry and (FLastSWR > SWR_HIGH_THRESH);
if FHasPATelemetry then
begin
LblPwr := Format('%.0fW', [FLastFwdW]);
LblSWR := Format('SWR:%.1f', [FLastSWR]);
end
else
begin
LblPwr := ''; // измерять нечем — показания не выдумываем
LblSWR := '';
end;
ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Style := [fsBold];
ACanvas.Font.Size := 10 * Sc;
TWPwr := ACanvas.TextWidth(LblPwr);
MaxTWPwr := ACanvas.TextWidth(Format('%.0fW', [FPAMaxPower]));
MaxTWSWR := ACanvas.TextWidth('SWR:10.0');
ACanvas.Font.Size := 6 * Sc; ACanvas.Font.Style := [];
LabelNeed := Max(MaxTWPwr, MaxTWSWR) + LABEL_PAD * 2 * Sc;
LeftInfo := Max(LEFT_INFO_MIN * Sc, LabelNeed + 4 * Sc);
BX := R.Left + LeftInfo; BW := W - LeftInfo - RIGHT_PAD * Sc;
if BW < 10 * Sc 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 - Sc;
SY1 := Y_BAR_MID + Sc;
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;
// Перо в физических пикселях, как и в DrawSMeterWide.
ACanvas.Pen.Width := Sc;
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 FHasPATelemetry then
begin
if FPAMaxPower > 0 then PwrPct := FLastFwdW / FPAMaxPower else PwrPct := 0;
PBarEnd := PwrToX(PwrPct);
FillBar(BX, PY1 + Sc, PBarEnd, PY2 - Sc, SM_CLR_METER_ON);
end;
if FHasPATelemetry then
begin
SBarEnd := SWRToX(FLastSWR);
if not SWRHigh then
FillBar(BX, SY1 + Sc, SBarEnd, SY2 - Sc, SM_CLR_AMBER)
else
begin
FillBar(BX, SY1 + Sc, Min(S_WarnX, SBarEnd), SY2 - Sc, SM_CLR_AMBER);
if SBarEnd > S_WarnX then
FillBar(S_WarnX, SY1 + Sc, SBarEnd, SY2 - Sc, CLR_SWR_HIGH);
end;
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 + Sc); ACanvas.LineTo(BX, PY2 - Sc);
ACanvas.MoveTo(BX, SY1 + Sc); ACanvas.LineTo(BX, SY2 - Sc);
ACanvas.Font.Size := 6 * Sc; 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 * Sc, PY1 - Sc, 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 * Sc, PY1 - Sc, CLR_TICK_WHITE);
// Ватты на делениях считаются от Max power, а он есть только там, где есть
// детектор. Без него оставляем голые деления: подписать их нечем.
if not FHasPATelemetry then Continue;
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 * Sc - ACanvas.TextHeight(Lbl) - Sc, Lbl);
end;
for i := 2 to 9 do
begin
if i = 5 then Continue;
X := SWRToX(i);
if i >= 3 then VLine(X, SY2 + Sc, SY2 + TICK_MED * Sc, CLR_TICK_BLUE)
else VLine(X, SY2 + Sc, SY2 + TICK_MED * Sc, 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 + Sc, SY2 + TICK_LONG * Sc, CLR_TICK_BLUE)
else
VLine(X, SY2 + Sc, SY2 + TICK_LONG * Sc, 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 * Sc + Sc, Lbl);
end;
ACanvas.Brush.Style := bsClear;
ACanvas.Font.Size := 10 * Sc; ACanvas.Font.Style := [fsBold]; TH10 := ACanvas.TextHeight('0');
ACanvas.Font.Size := 6 * Sc; ACanvas.Font.Style := []; TH6 := ACanvas.TextHeight('0');
ACanvas.Font.Size := 10 * Sc; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM;
ACanvas.TextOut(LblZoneL + 4 * Sc, PY1 - TH10 - 2 * Sc, LblPwr);
ACanvas.Font.Size := 10 * Sc; ACanvas.Font.Style := [fsBold];
if SWRHigh then ACanvas.Font.Color := CLR_SWR_HIGH
else ACanvas.Font.Color := CLR_LABEL_S;
ACanvas.TextOut(LblZoneL + 4 * Sc, SY2 + 2 * Sc, LblSWR);
if SWRHigh then
begin
ACanvas.Font.Size := 6 * Sc; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_SWR_HIGH;
ACanvas.TextOut(LblZoneL + 4 * Sc + TWPwr + 2 * Sc, PY1 - TH6 - 4 * Sc, 'SWR High');
end;
end;
// ────────────────────────────────────────────────────────────────────────────
// PaintSMeterRight
// ────────────────────────────────────────────────────────────────────────────
procedure TSMeterView.PaintSMeterRight(Sender: TObject);
var PB: TPaintBox; W, H, BmpW, BmpH, ZX1, ZX2, ZY1, ZY2: Integer;
begin
PB := TPaintBox(Sender);
W := PB.Width; H := PB.Height;
if (W <= 0) or (H <= 0) then Exit;
// Битмап держим в физических пикселях канвы, иначе Cocoa растянет его вдвое
// и картинка замылится. На Windows/Linux FScale=1 и всё как было.
FScale := GetControlScale(PB);
BmpW := W * FScale; BmpH := H * FScale;
if (FSmBitmap = nil) or (FSmBitmap.Width <> BmpW) or (FSmBitmap.Height <> BmpH) then
begin
FreeAndNil(FSmBitmap);
FSmBitmap := TBitmap.Create;
FSmBitmap.SetSize(BmpW, BmpH);
end;
if FTransmitting then
begin
DrawTXMeter(FSmBitmap.Canvas, Rect(0, 0, BmpW, BmpH));
end else
begin
DrawSMeterWide(FSmBitmap.Canvas, Rect(0, 0, BmpW, BmpH),
FLastSMeter, FSMeterPeak, FSMeterMin,
ZX1, ZX2, ZY1, ZY2);
DrawSMeterZone(FSmBitmap.Canvas, Rect(0, 0, BmpW, BmpH), ZX1, ZX2, ZY1, ZY2);
end;
if FScale = 1 then
PB.Canvas.Draw(0, 0, FSmBitmap)
else
PB.Canvas.StretchDraw(Rect(0, 0, W, H), FSmBitmap);
end;
end.