unit ChannelsForm; { TChannelsForm — редактор списка каналов (channel memory). Открывается по правой кнопке мыши на кнопке CH в блоке RX левой панели. Список каналов слева (FlatListBox), поля редактирования справа. Все изменения сохраняются немедленно в TChannelStore и на диск. } {$mode objfpc}{$H+} interface uses Classes, SysUtils, Math, Forms, Controls, Graphics, StdCtrls, ExtCtrls, FlatButton, FlatCheckBox, FlatComboBox, FlatEdit, FlatSpinEdit, FlatFloatSpinEdit, FlatListBox, AppTheme, ChannelStore, RadioModes, DpiUtils; const // Копии из MainForm: нужны для выпадающих списков CH_MODE_COUNT = RadioModes.MODE_COUNT; CH_MODE_NAMES: array[0..CH_MODE_COUNT-1] of string = ('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR','FM RAW'); CH_CTCSS_COUNT = 38; CH_CTCSS_NAMES: array[0..CH_CTCSS_COUNT-1] of string = ( '67.0', '71.9', '74.4', '77.0', '79.7', '82.5', '85.4', '88.5', '91.5', '94.8', '97.4', '100.0', '103.5', '107.2', '110.9', '114.8', '118.8', '123.0', '127.3', '131.8', '136.5', '141.3', '146.2', '151.4', '156.7', '162.2', '167.9', '173.8', '179.9', '186.2', '192.8', '203.5', '210.7', '218.1', '225.7', '233.6', '241.8', '250.3'); type TChannelsForm = class(TForm) private // --- Левая панель --- FPanelLeft: TPanel; FBtnAdd: TFlatButton; FBtnDelete: TFlatButton; FBtnUp: TFlatButton; FBtnDown: TFlatButton; FBtnClose: TFlatButton; FList: TFlatListBox; // --- Правая панель (поля редактирования) --- FPanelRight: TPanel; FFooterPanel: TPanel; FCardIdentity: TPanel; FCardRadio: TPanel; FCardRepeater: TPanel; FLeftTitle: TLabel; FLeftHint: TLabel; FRightTitle: TLabel; FRightHint: TLabel; // Labels (для перекраски темой) FLbls: array of TLabel; // Поля FEdGroup: TFlatEdit; FEdName: TFlatEdit; FSpinRXFreq: TFlatFloatSpinEdit; FSpinTXFreq: TFlatFloatSpinEdit; FCmbMode: TFlatComboBox; FBtnRptNone: TFlatButton; FBtnRptMinus: TFlatButton; FBtnRptPlus: TFlatButton; FSpinRptOffset: TFlatFloatSpinEdit; FChkCTCSS: TFlatCheckBox; FCmbCTCSS: TFlatComboBox; FSpinPower: TFlatSpinEdit; // Данные FStore: TChannelStore; FUpdating: Boolean; FOnChanged: TNotifyEvent; FCurrentDefaults: TChannel; // значения по умолчанию для новых каналов FTheme: TAppTheme; procedure BuildUI; procedure StyleBtn(B: TFlatButton; AActive: Boolean = False); function MakeLabel(AParent: TWinControl; const Cap: string; X, Y, W, H: Integer): TLabel; function MakeGroupPanel(const Cap: string; Y, H: Integer): TPanel; procedure LoadChannelToFields(Idx: Integer); procedure SaveFieldsToChannel; procedure RefreshList(KeepIdx: Integer = -1); function SelectedIdx: Integer; // Handlers — список procedure OnListClick(Sender: TObject); procedure OnListDblClick(Sender: TObject); // Handlers — тулбар procedure OnAddClick(Sender: TObject); procedure OnDeleteClick(Sender: TObject); procedure OnUpClick(Sender: TObject); procedure OnDownClick(Sender: TObject); // Handlers — поля procedure OnFieldChange(Sender: TObject); procedure OnRptBtnClick(Sender: TObject); procedure OnCTCSSChkChange(Sender: TObject); procedure OnCTCSSCmbChange(Sender: TObject); // Handlers — форма procedure OnCloseClick(Sender: TObject); procedure OnFormClose(Sender: TObject; var CloseAction: TCloseAction); public constructor Create(AOwner: TComponent; AStore: TChannelStore; AOnChanged: TNotifyEvent); reintroduce; procedure ApplyTheme(const T: TAppTheme); procedure SetCurrentDefaults(const Ch: TChannel); property OnChannelsChanged: TNotifyEvent read FOnChanged write FOnChanged; end; implementation const // Геометрия LPANEL_W = 240; TOOLBAR_Y = 66; TOOLBAR_H = 30; BTN_H = 26; FORM_W = 820; FORM_H = 540; PAGE_MARGIN = 18; // Правая панель LBL_X = 14; LBL_W = 106; CTRL_X = 126; ROW_H = 28; CTRL_H = 24; constructor TChannelsForm.Create(AOwner: TComponent; AStore: TChannelStore; AOnChanged: TNotifyEvent); var WorkArea: TRect; MaxW, MaxH: Integer; begin inherited CreateNew(AOwner); FStore := AStore; FOnChanged := AOnChanged; FUpdating := False; FTheme := DarkTheme; TChannelStore.DefaultChannel(FCurrentDefaults); Scaled := False; Caption := 'Channels'; BorderStyle := bsSizeable; if AOwner is TCustomForm then begin WorkArea := TCustomForm(AOwner).BoundsRect; WorkArea := Screen.MonitorFromRect(WorkArea).WorkareaRect; Position := poOwnerFormCenter; end else begin WorkArea := Screen.PrimaryMonitor.WorkareaRect; Position := poScreenCenter; end; MaxW := WorkArea.Right - WorkArea.Left - DpiScale(48); MaxH := WorkArea.Bottom - WorkArea.Top - DpiScale(48); Width := Min(DpiScale(FORM_W), MaxW); Height := Min(DpiScale(FORM_H), MaxH); Constraints.MinWidth := Min(DpiScale(680), Width); Constraints.MinHeight := Min(DpiScale(530), Height); KeyPreview := True; OnClose := @OnFormClose; BuildUI; ApplyTheme(DarkTheme); RefreshList(0); end; procedure TChannelsForm.BuildUI; var i, BtnW, CardW, Y: Integer; L: TLabel; procedure PlaceRightUnit(AParent: TWinControl; const Cap: string; ATop: Integer); begin L := MakeLabel(AParent, Cap, 0, ATop + 1, 42, CTRL_H); L.SetBounds(AParent.ClientWidth - DpiScale(50), DpiScale(ATop + 1), DpiScale(42), DpiScale(CTRL_H)); L.Anchors := [akTop, akRight]; end; begin SetLength(FLbls, 0); // ----- Левая панель ----- FPanelLeft := TPanel.Create(Self); FPanelLeft.Parent := Self; FPanelLeft.Align := alLeft; FPanelLeft.Width := DpiScale(LPANEL_W); FPanelLeft.BevelOuter := bvNone; FLeftTitle := MakeLabel(FPanelLeft, 'CHANNEL MEMORY', 14, 12, LPANEL_W - 28, 24); FLeftTitle.Font.Size := 9; FLeftTitle.Font.Style := [fsBold]; FLeftTitle.Font.Color := FTheme.Text; FLeftHint := MakeLabel(FPanelLeft, 'Saved frequencies and modes', 14, 37, LPANEL_W - 28, 18); FLeftHint.Font.Size := 8; BtnW := (DpiScale(LPANEL_W - 24) - 3 * DpiScale(6)) div 4; FBtnAdd := TFlatButton.Create(Self); FBtnAdd.Parent := FPanelLeft; FBtnAdd.SetBounds(DpiScale(12), DpiScale(TOOLBAR_Y), BtnW, DpiScale(BTN_H)); FBtnAdd.Caption := 'ADD'; FBtnAdd.OnClick := @OnAddClick; FBtnDelete := TFlatButton.Create(Self); FBtnDelete.Parent := FPanelLeft; FBtnDelete.SetBounds(FBtnAdd.Left + BtnW + DpiScale(6), FBtnAdd.Top, BtnW, FBtnAdd.Height); FBtnDelete.Caption := 'DEL'; FBtnDelete.OnClick := @OnDeleteClick; FBtnUp := TFlatButton.Create(Self); FBtnUp.Parent := FPanelLeft; FBtnUp.SetBounds(FBtnDelete.Left + BtnW + DpiScale(6), FBtnAdd.Top, BtnW, FBtnAdd.Height); FBtnUp.Caption := '↑'; FBtnUp.OnClick := @OnUpClick; FBtnDown := TFlatButton.Create(Self); FBtnDown.Parent := FPanelLeft; FBtnDown.SetBounds(FBtnUp.Left + BtnW + DpiScale(6), FBtnAdd.Top, BtnW, FBtnAdd.Height); FBtnDown.Caption := '↓'; FBtnDown.OnClick := @OnDownClick; FList := TFlatListBox.Create(Self); FList.Parent := FPanelLeft; FList.SetBounds(DpiScale(12), DpiScale(TOOLBAR_Y + TOOLBAR_H + 10), FPanelLeft.ClientWidth - DpiScale(24), FPanelLeft.ClientHeight - DpiScale(TOOLBAR_Y + TOOLBAR_H + 22)); FList.Anchors := [akLeft, akTop, akRight, akBottom]; FList.OnClick := @OnListClick; FList.OnDblClick := @OnListDblClick; // ----- Правая панель ----- FPanelRight := TPanel.Create(Self); FPanelRight.Parent := Self; FPanelRight.Align := alClient; FPanelRight.BevelOuter := bvNone; FFooterPanel := TPanel.Create(Self); FFooterPanel.Parent := FPanelRight; FFooterPanel.Align := alBottom; FFooterPanel.Height := DpiScale(50); FFooterPanel.BevelOuter := bvNone; FBtnClose := TFlatButton.Create(Self); FBtnClose.Parent := FFooterPanel; FBtnClose.Caption := 'Close'; FBtnClose.SetBounds(FFooterPanel.ClientWidth - DpiScale(110), DpiScale(10), DpiScale(92), DpiScale(30)); FBtnClose.Anchors := [akTop, akRight]; FBtnClose.OnClick := @OnCloseClick; FRightTitle := MakeLabel(FPanelRight, 'CHANNEL DETAILS', PAGE_MARGIN, 10, 420, 22); FRightTitle.Font.Size := 9; FRightTitle.Font.Style := [fsBold]; FRightTitle.Font.Color := FTheme.Text; FRightTitle.Anchors := [akLeft, akTop, akRight]; FRightHint := MakeLabel(FPanelRight, 'Changes are saved automatically', PAGE_MARGIN, 31, 420, 18); FRightHint.Font.Size := 8; FRightHint.Anchors := [akLeft, akTop, akRight]; FCardIdentity := MakeGroupPanel('IDENTITY', 56, 90); CardW := FCardIdentity.ClientWidth; Y := 34; MakeLabel(FCardIdentity, 'Group', LBL_X, Y + 1, LBL_W, CTRL_H); FEdGroup := TFlatEdit.Create(Self); FEdGroup.Parent := FCardIdentity; FEdGroup.SetBounds(DpiScale(CTRL_X), DpiScale(Y), CardW - DpiScale(CTRL_X + 14), DpiScale(CTRL_H)); FEdGroup.Anchors := [akLeft, akTop, akRight]; FEdGroup.OnChange := @OnFieldChange; Inc(Y, ROW_H); MakeLabel(FCardIdentity, 'Name', LBL_X, Y + 1, LBL_W, CTRL_H); FEdName := TFlatEdit.Create(Self); FEdName.Parent := FCardIdentity; FEdName.SetBounds(DpiScale(CTRL_X), DpiScale(Y), CardW - DpiScale(CTRL_X + 14), DpiScale(CTRL_H)); FEdName.Anchors := [akLeft, akTop, akRight]; FEdName.OnChange := @OnFieldChange; FCardRadio := MakeGroupPanel('RADIO', 156, 120); CardW := FCardRadio.ClientWidth; Y := 34; MakeLabel(FCardRadio, 'RX frequency', LBL_X, Y + 1, LBL_W, CTRL_H); FSpinRXFreq := TFlatFloatSpinEdit.Create(Self); FSpinRXFreq.Parent := FCardRadio; FSpinRXFreq.SetBounds(DpiScale(CTRL_X), DpiScale(Y), CardW - DpiScale(CTRL_X + 56), DpiScale(CTRL_H)); FSpinRXFreq.Anchors := [akLeft, akTop, akRight]; FSpinRXFreq.MinValue := 0.001; FSpinRXFreq.MaxValue := 2000.0; FSpinRXFreq.Increment := 0.001; FSpinRXFreq.DecimalPlaces := 6; FSpinRXFreq.Value := 145.5; FSpinRXFreq.OnChange := @OnFieldChange; PlaceRightUnit(FCardRadio, 'MHz', Y); Inc(Y, ROW_H); MakeLabel(FCardRadio, 'TX frequency', LBL_X, Y + 1, LBL_W, CTRL_H); FSpinTXFreq := TFlatFloatSpinEdit.Create(Self); FSpinTXFreq.Parent := FCardRadio; FSpinTXFreq.SetBounds(DpiScale(CTRL_X), DpiScale(Y), CardW - DpiScale(CTRL_X + 56), DpiScale(CTRL_H)); FSpinTXFreq.Anchors := [akLeft, akTop, akRight]; FSpinTXFreq.MinValue := 0.001; FSpinTXFreq.MaxValue := 2000.0; FSpinTXFreq.Increment := 0.001; FSpinTXFreq.DecimalPlaces := 6; FSpinTXFreq.Value := 145.5; FSpinTXFreq.OnChange := @OnFieldChange; PlaceRightUnit(FCardRadio, 'MHz', Y); Inc(Y, ROW_H); MakeLabel(FCardRadio, 'Mode', LBL_X, Y + 1, LBL_W, CTRL_H); FCmbMode := TFlatComboBox.Create(Self); FCmbMode.Parent := FCardRadio; FCmbMode.SetBounds(DpiScale(CTRL_X), DpiScale(Y), DpiScale(180), DpiScale(CTRL_H)); for i := 0 to CH_MODE_COUNT - 1 do FCmbMode.Items.Add(CH_MODE_NAMES[i]); FCmbMode.ItemIndex := 5; // FM default FCmbMode.OnChange := @OnFieldChange; FCardRepeater := MakeGroupPanel('REPEATER / TRANSMIT', 286, 148); CardW := FCardRepeater.ClientWidth; Y := 34; MakeLabel(FCardRepeater, 'Direction', LBL_X, Y + 1, LBL_W, CTRL_H); FBtnRptNone := TFlatButton.Create(Self); FBtnRptNone.Parent := FCardRepeater; FBtnRptNone.SetBounds(DpiScale(CTRL_X), DpiScale(Y), DpiScale(72), DpiScale(CTRL_H)); FBtnRptNone.Caption := 'NONE'; FBtnRptNone.Tag := 0; FBtnRptNone.OnClick := @OnRptBtnClick; FBtnRptMinus := TFlatButton.Create(Self); FBtnRptMinus.Parent := FCardRepeater; FBtnRptMinus.SetBounds(DpiScale(CTRL_X + 78), DpiScale(Y), DpiScale(46), DpiScale(CTRL_H)); FBtnRptMinus.Caption := '−'; FBtnRptMinus.Tag := 1; FBtnRptMinus.OnClick := @OnRptBtnClick; FBtnRptPlus := TFlatButton.Create(Self); FBtnRptPlus.Parent := FCardRepeater; FBtnRptPlus.SetBounds(DpiScale(CTRL_X + 130), DpiScale(Y), DpiScale(46), DpiScale(CTRL_H)); FBtnRptPlus.Caption := '+'; FBtnRptPlus.Tag := 2; FBtnRptPlus.OnClick := @OnRptBtnClick; Inc(Y, ROW_H); MakeLabel(FCardRepeater, 'Offset', LBL_X, Y + 1, LBL_W, CTRL_H); FSpinRptOffset := TFlatFloatSpinEdit.Create(Self); FSpinRptOffset.Parent := FCardRepeater; FSpinRptOffset.SetBounds(DpiScale(CTRL_X), DpiScale(Y), CardW - DpiScale(CTRL_X + 56), DpiScale(CTRL_H)); FSpinRptOffset.Anchors := [akLeft, akTop, akRight]; FSpinRptOffset.MinValue := 0.0; FSpinRptOffset.MaxValue := 100.0; FSpinRptOffset.Increment := 0.025; FSpinRptOffset.DecimalPlaces := 3; FSpinRptOffset.Value := 0.6; FSpinRptOffset.OnChange := @OnFieldChange; PlaceRightUnit(FCardRepeater, 'MHz', Y); Inc(Y, ROW_H); MakeLabel(FCardRepeater, 'CTCSS tone', LBL_X, Y + 1, LBL_W, CTRL_H); FChkCTCSS := TFlatCheckBox.Create(Self); FChkCTCSS.Parent := FCardRepeater; FChkCTCSS.SetBounds(DpiScale(CTRL_X), DpiScale(Y), DpiScale(28), DpiScale(CTRL_H)); FChkCTCSS.Caption := ''; FChkCTCSS.Checked := False; FChkCTCSS.OnChange := @OnCTCSSChkChange; FCmbCTCSS := TFlatComboBox.Create(Self); FCmbCTCSS.Parent := FCardRepeater; FCmbCTCSS.SetBounds(DpiScale(CTRL_X + 34), DpiScale(Y), CardW - DpiScale(CTRL_X + 48), DpiScale(CTRL_H)); FCmbCTCSS.Anchors := [akLeft, akTop, akRight]; for i := 0 to CH_CTCSS_COUNT - 1 do FCmbCTCSS.Items.Add(CH_CTCSS_NAMES[i]); FCmbCTCSS.ItemIndex := 0; FCmbCTCSS.OnChange := @OnCTCSSCmbChange; Inc(Y, ROW_H); MakeLabel(FCardRepeater, 'Power', LBL_X, Y + 1, LBL_W, CTRL_H); FSpinPower := TFlatSpinEdit.Create(Self); FSpinPower.Parent := FCardRepeater; FSpinPower.SetBounds(DpiScale(CTRL_X), DpiScale(Y), DpiScale(92), DpiScale(CTRL_H)); FSpinPower.MinValue := 0; FSpinPower.MaxValue := 100; FSpinPower.Value := 100; FSpinPower.OnChange := @OnFieldChange; MakeLabel(FCardRepeater, '%', CTRL_X + 98, Y + 1, 24, CTRL_H); end; function TChannelsForm.MakeLabel(AParent: TWinControl; const Cap: string; X, Y, W, H: Integer): TLabel; var L: TLabel; begin L := TLabel.Create(Self); L.Parent := AParent; L.Caption := Cap; L.AutoSize := False; L.SetBounds(DpiScale(X), DpiScale(Y), DpiScale(W), DpiScale(H)); L.Layout := tlCenter; L.Font.Size := 8; L.Font.Color := FTheme.TextDim; // Сохраняем для последующей перекраски темой SetLength(FLbls, Length(FLbls) + 1); FLbls[High(FLbls)] := L; Result := L; end; function TChannelsForm.MakeGroupPanel(const Cap: string; Y, H: Integer): TPanel; var Title: TLabel; Divider: TPanel; begin Result := TPanel.Create(Self); Result.Parent := FPanelRight; Result.SetBounds(DpiScale(PAGE_MARGIN), DpiScale(Y), FPanelRight.ClientWidth - DpiScale(2 * PAGE_MARGIN), DpiScale(H)); Result.Anchors := [akLeft, akTop, akRight]; Result.BevelOuter := bvNone; Result.Color := FTheme.Panel; Title := MakeLabel(Result, Cap, 12, 2, 300, 24); Title.Font.Size := 8; Title.Font.Style := [fsBold]; Title.Font.Color := FTheme.FreqActive; Divider := TPanel.Create(Self); Divider.Parent := Result; Divider.SetBounds(0, DpiScale(28), Result.ClientWidth, 1); Divider.Anchors := [akLeft, akTop, akRight]; Divider.BevelOuter := bvNone; Divider.Color := FTheme.Border; end; procedure TChannelsForm.StyleBtn(B: TFlatButton; AActive: Boolean); begin // Прямо созданные TFlatButton не проходят через MakeFlatBtn, поэтому // явно даём им тот же системный шрифт, что и остальному интерфейсу. B.Font.Assign(Font); B.Font.Size := 8; B.Font.Style := []; B.Active := AActive; B.ClrNorm := FTheme.BtnNorm; B.ClrActive := FTheme.BtnActive; B.ClrHot := FTheme.BtnHot; B.ClrBorder := IfThen(AActive, FTheme.BtnBorderActive, FTheme.BtnBorderNorm); B.ClrText := FTheme.BtnText; B.ClrTextAct := FTheme.BtnTextActive; B.Invalidate; end; procedure TChannelsForm.ApplyTheme(const T: TAppTheme); var i: Integer; Ch: TChannel; procedure ThemeCard(P: TPanel); var n: Integer; begin if P = nil then Exit; P.Color := T.Panel; for n := 0 to P.ControlCount - 1 do if (P.Controls[n] is TPanel) and (P.Controls[n].Height = 1) then TPanel(P.Controls[n]).Color := T.Border; end; begin FTheme := T; Color := T.BG; FPanelLeft.Color := T.Panel; FPanelRight.Color := T.BG; FFooterPanel.Color := T.BG; ThemeCard(FCardIdentity); ThemeCard(FCardRadio); ThemeCard(FCardRepeater); FList.SetAppTheme(T); StyleBtn(FBtnAdd); StyleBtn(FBtnDelete); StyleBtn(FBtnUp); StyleBtn(FBtnDown); StyleBtn(FBtnClose); // RPT buttons — пересчитываем Active из текущего состояния if SelectedIdx >= 0 then begin Ch := FStore.GetChannel(SelectedIdx); StyleBtn(FBtnRptNone, Ch.RptDir = 0); StyleBtn(FBtnRptMinus, Ch.RptDir = 1); StyleBtn(FBtnRptPlus, Ch.RptDir = 2); end else begin StyleBtn(FBtnRptNone, True); StyleBtn(FBtnRptMinus, False); StyleBtn(FBtnRptPlus, False); end; FEdGroup.SetAppTheme(T); FEdName.SetAppTheme(T); FSpinRXFreq.SetAppTheme(T); FSpinTXFreq.SetAppTheme(T); FCmbMode.SetAppTheme(T); FSpinRptOffset.SetAppTheme(T); FChkCTCSS.SetAppTheme(T); FCmbCTCSS.SetAppTheme(T); FSpinPower.SetAppTheme(T); for i := 0 to High(FLbls) do begin if FLbls[i] = FLeftTitle then FLbls[i].Font.Color := T.Text else if FLbls[i] = FRightTitle then FLbls[i].Font.Color := T.Text else if fsBold in FLbls[i].Font.Style then FLbls[i].Font.Color := T.FreqActive else FLbls[i].Font.Color := T.TextDim; FLbls[i].Transparent := True; end; end; // --------------------------------------------------------------------------- // Список каналов // --------------------------------------------------------------------------- function TChannelsForm.SelectedIdx: Integer; begin Result := FList.ItemIndex; end; procedure TChannelsForm.RefreshList(KeepIdx: Integer); var i, NewIdx: Integer; Ch: TChannel; begin FList.Items.Clear; for i := 0 to FStore.Count - 1 do begin Ch := FStore.GetChannel(i); if Ch.Group <> '' then FList.Items.Add(Ch.Group + ': ' + Ch.Name) else FList.Items.Add(Ch.Name); end; if FStore.Count = 0 then begin LoadChannelToFields(-1); Exit; end; NewIdx := EnsureRange(KeepIdx, 0, FStore.Count - 1); FList.ItemIndex := NewIdx; LoadChannelToFields(NewIdx); end; procedure TChannelsForm.LoadChannelToFields(Idx: Integer); var Ch: TChannel; HasSel: Boolean; begin FUpdating := True; try HasSel := (Idx >= 0) and (Idx < FStore.Count); FEdGroup.Enabled := HasSel; FEdName.Enabled := HasSel; FSpinRXFreq.Enabled := HasSel; FSpinTXFreq.Enabled := HasSel; FCmbMode.Enabled := HasSel; FBtnRptNone.Enabled := HasSel; FBtnRptMinus.Enabled := HasSel; FBtnRptPlus.Enabled := HasSel; FSpinRptOffset.Enabled := HasSel; FChkCTCSS.Enabled := HasSel; FCmbCTCSS.Enabled := HasSel; FSpinPower.Enabled := HasSel; if not HasSel then Exit; Ch := FStore.GetChannel(Idx); FEdGroup.Text := Ch.Group; FEdName.Text := Ch.Name; FSpinRXFreq.Value := Ch.RXFreq / 1000000.0; FSpinTXFreq.Value := Ch.TXFreq / 1000000.0; FCmbMode.ItemIndex := EnsureRange(Ch.Mode, MODE_MIN, MODE_MAX); StyleBtn(FBtnRptNone, Ch.RptDir = 0); StyleBtn(FBtnRptMinus, Ch.RptDir = 1); StyleBtn(FBtnRptPlus, Ch.RptDir = 2); FSpinRptOffset.Value := Ch.RptOffsetHz / 1000000.0; FChkCTCSS.Checked := Ch.CTCSSOn; FCmbCTCSS.ItemIndex := EnsureRange(Ch.CTCSSToneIdx, 0, CH_CTCSS_COUNT - 1); FSpinPower.Value := Ch.Power; finally FUpdating := False; end; end; procedure TChannelsForm.SaveFieldsToChannel; var Idx: Integer; Ch: TChannel; begin if FUpdating then Exit; Idx := SelectedIdx; if (Idx < 0) or (Idx >= FStore.Count) then Exit; Ch := FStore.GetChannel(Idx); Ch.Group := FEdGroup.Text; Ch.Name := FEdName.Text; Ch.RXFreq := FSpinRXFreq.Value * 1000000.0; Ch.TXFreq := FSpinTXFreq.Value * 1000000.0; Ch.Mode := EnsureRange(FCmbMode.ItemIndex, MODE_MIN, MODE_MAX); // RptDir берётся из кнопок (Active) if FBtnRptMinus.Active then Ch.RptDir := 1 else if FBtnRptPlus.Active then Ch.RptDir := 2 else Ch.RptDir := 0; Ch.RptOffsetHz := FSpinRptOffset.Value * 1000000.0; Ch.CTCSSOn := FChkCTCSS.Checked; Ch.CTCSSToneIdx := EnsureRange(FCmbCTCSS.ItemIndex, 0, CH_CTCSS_COUNT - 1); Ch.Power := FSpinPower.Value; FStore.UpdateChannel(Idx, Ch); FStore.Save; // Обновляем имя в списке FUpdating := True; try if Ch.Group <> '' then FList.Items[Idx] := Ch.Group + ': ' + Ch.Name else FList.Items[Idx] := Ch.Name; finally FUpdating := False; end; if Assigned(FOnChanged) then FOnChanged(Self); end; // --------------------------------------------------------------------------- // Обработчики кнопок тулбара // --------------------------------------------------------------------------- procedure TChannelsForm.OnListClick(Sender: TObject); begin if FUpdating then Exit; LoadChannelToFields(FList.ItemIndex); end; procedure TChannelsForm.OnListDblClick(Sender: TObject); begin LoadChannelToFields(FList.ItemIndex); end; procedure TChannelsForm.OnAddClick(Sender: TObject); var Ch: TChannel; NewIdx: Integer; begin Ch := FCurrentDefaults; // берём значения текущего состояния радио FStore.AddChannel(Ch); FStore.Save; NewIdx := FStore.Count - 1; RefreshList(NewIdx); if Assigned(FOnChanged) then FOnChanged(Self); // Фокус на имя для быстрого переименования if FEdName.Enabled then FEdName.SetFocus; FEdName.SelectAll; end; procedure TChannelsForm.OnDeleteClick(Sender: TObject); var Idx: Integer; begin Idx := SelectedIdx; if (Idx < 0) or (Idx >= FStore.Count) then Exit; FStore.DeleteChannel(Idx); FStore.Save; RefreshList(EnsureRange(Idx, 0, FStore.Count - 1)); if Assigned(FOnChanged) then FOnChanged(Self); end; procedure TChannelsForm.OnUpClick(Sender: TObject); var Idx: Integer; begin Idx := SelectedIdx; if Idx < 1 then Exit; FStore.MoveUp(Idx); FStore.Save; RefreshList(Idx - 1); if Assigned(FOnChanged) then FOnChanged(Self); end; procedure TChannelsForm.OnDownClick(Sender: TObject); var Idx: Integer; begin Idx := SelectedIdx; if (Idx < 0) or (Idx >= FStore.Count - 1) then Exit; FStore.MoveDown(Idx); FStore.Save; RefreshList(Idx + 1); if Assigned(FOnChanged) then FOnChanged(Self); end; // --------------------------------------------------------------------------- // Обработчики полей редактирования // --------------------------------------------------------------------------- procedure TChannelsForm.OnFieldChange(Sender: TObject); begin SaveFieldsToChannel; end; procedure TChannelsForm.OnRptBtnClick(Sender: TObject); var Dir: Integer; begin Dir := (Sender as TFlatButton).Tag; StyleBtn(FBtnRptNone, Dir = 0); StyleBtn(FBtnRptMinus, Dir = 1); StyleBtn(FBtnRptPlus, Dir = 2); SaveFieldsToChannel; end; procedure TChannelsForm.OnCTCSSChkChange(Sender: TObject); begin SaveFieldsToChannel; end; procedure TChannelsForm.OnCTCSSCmbChange(Sender: TObject); begin SaveFieldsToChannel; end; procedure TChannelsForm.OnCloseClick(Sender: TObject); begin Close; end; procedure TChannelsForm.OnFormClose(Sender: TObject; var CloseAction: TCloseAction); begin CloseAction := caHide; // скрываем, не уничтожаем — указатель в MainForm остаётся валидным end; procedure TChannelsForm.SetCurrentDefaults(const Ch: TChannel); begin FCurrentDefaults := Ch; end; end.