Files
Plancia/Console/uPlanciaElements.pas
T
f.bittiandClaude Opus 5 4a01a2ec88 Plancia configurabile su Modbus RTU
Due programmi che condividono uModbusRTU e uGauge:

- Console/PlanciaConsole: plancia nautica descritta da file XML, con
  modalita' plancia e modalita' configurazione. Comandi, spie, selettori,
  strumenti e allarmi sonori; il bus gira in un thread suo perche' la
  finestra non si fermi mai.
- ProjectPlancia: il programma di prova piu' vecchio, usato per collaudare
  i canali.

Le plance sono in Console/*.xml, la documentazione in Console/LEGGIMI.md.
Esclusi dal versionamento i compilati (.exe, .dcu), i file dell'IDE e un
audio da 17 MB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:56:34 +02:00

2393 lines
71 KiB
ObjectPascal

unit uPlanciaElements;
{
Elementi che si possono piazzare sulla plancia.
Un solo controllo, TPlanciaElement, copre tutti i tipi: cambia solo il
disegno e la reazione al mouse. Cosi' selezione, spostamento,
ridimensionamento, zoom e serializzazione sono scritti una volta sola.
Il controllo NON possiede la propria TElementDef: la lista delle definizioni
vive in TPlanciaConfig, che le crea e le distrugge. Non possiede nemmeno le
immagini, che stanno nella TImageLibrary condivisa.
Coordinate: TElementDef contiene sempre coordinate LOGICHE (zoom 100%).
Il controllo le moltiplica per Scale quando si posiziona e quando disegna,
e divide per Scale i movimenti del mouse. Cosi' lo zoom non sporca il file
di configurazione.
}
interface
uses
Winapi.Windows, Winapi.GDIPAPI, Winapi.GDIPOBJ,
System.SysUtils, System.Classes, System.Types, System.Math,
System.Generics.Collections,
Vcl.Controls, Vcl.Graphics, Vcl.ExtCtrls, uGauge, uImageLib;
type
TElementKind = (ekButton, ekSwitch, ekLamp, ekGauge, ekImage, ekDisplay,
ekRotary, ekLabel);
const
// Identificativi usati nell'XML: non vanno cambiati senza migrare i file.
ELEMENT_IDS: array[TElementKind] of string =
('button', 'switch', 'lamp', 'gauge', 'image', 'display', 'rotary',
'label');
ELEMENT_NAMES: array[TElementKind] of string =
('Pulsante', 'Interruttore', 'Spia', 'Gauge', 'Immagine', 'Display',
'Selettore', 'Testo');
ELEMENT_HINTS: array[TElementKind] of string =
('Chiude il canale solo mentre e'' premuto',
'Chiude il canale e resta premuto',
'Spia di un ingresso digitale (sola lettura)',
'Colonna analogica con scala e soglie',
'Grafica decorativa, nessun canale',
'Display numerico a sette segmenti (sola lettura)',
'Selettore rotativo a 2 o 3 posizioni',
'Scritta serigrafata, nessun canale');
ELEMENT_DEF_W: array[TElementKind] of Integer =
(120, 120, 90, 130, 160, 250, 150, 160);
ELEMENT_DEF_H: array[TElementKind] of Integer =
(48, 48, 64, 200, 100, 140, 170, 30);
// Colori di stato condivisi da tutti gli elementi.
CLR_ELEM_ON = TColor($0050AF4C);
CLR_ELEM_ON_DARK = TColor($00307F2C);
CLR_LAMP_OFF = TColor($00606060);
CLR_LAMP_ON = TColor($0040E040);
/// Lente di un pulsante tondo non illuminato: grigio-azzurro chiaro.
CLR_KEY_FACE = TColor($00D8D0C8);
// Display: rosso acceso e rosso quasi spento, come i sette segmenti veri.
CLR_DISPLAY_BG = TColor($00101010);
CLR_SEG_ON = TColor($001E28FF);
CLR_SEG_OFF = TColor($00181830);
// Selettore rotativo: ghiera chiara, manopola scura, leva blu.
CLR_KNOB_RING = TColor($00C8C8C8);
CLR_KNOB_EDGE = TColor($00808080);
CLR_KNOB_BODY = TColor($00323232);
CLR_KNOB_LEVER = TColor($00A86E30);
// Ghiera di un comando tondo acceso: due tonalita' di blu, la piu' chiara
// contro la lente e la piu' carica all'orlo esterno. Letta dall'occhio come
// una luce che viene da dentro, invece che come un anello ridipinto.
CLR_KNOB_RING_ON = TColor($00FFF0AF);
CLR_KNOB_RING_ON_EDGE = TColor($00FABE5A);
/// Filo scuro all'orlo: senza, la ghiera accesa sbava sul fondo del pannello.
CLR_KNOB_RING_ON_RIM = TColor($00D2822A);
// Tasto a video spento: vetro quasi nero con un filo grigio chiaro attorno.
CLR_SCREEN_KEY_FACE = TColor($001F1C1A);
CLR_SCREEN_KEY_EDGE = TColor($00968F8A);
// Quadrante a lancetta: ghiera cromata, fondo nero, fascia della scala
// grigia; verde e rosso compaiono solo se le soglie sono impostate.
CLR_DIAL_RING = TColor($00B3ADA8);
CLR_DIAL_FACE = TColor($000D0C0B);
CLR_DIAL_BAND = TColor($00443E3A);
CLR_DIAL_OK = TColor($004BB43C);
CLR_DIAL_ALARM = TColor($00323CD2);
CLR_DIAL_TICK = TColor($00E6E6E6);
CLR_DIAL_NEEDLE = TColor($00F0F0F0);
CLR_DIAL_VALUE = TColor($0064E146);
// Modalita' notturna. In navigazione al buio la pupilla resta dilatata solo
// se non la si abbaglia: il pannello scende a un fondo quasi nero e ogni
// colore viene abbassato alla stessa frazione, cosi' i rapporti fra i colori
// restano quelli del giorno e la plancia resta leggibile.
NIGHT_DIM = 0.34;
/// Fondo del pannello di notte: nero appena caldo, non nero assoluto, che
/// sullo schermo sembrerebbe un buco.
CLR_NIGHT_BG = TColor($000C0E12);
/// Inchiostro di tutte le scritte di notte. Ambra: e' il colore che disturba
/// meno la visione notturna, e serigrafie nere sul fondo scuro sparirebbero.
CLR_NIGHT_INK = TColor($003796EB);
MIN_ELEMENT_SIZE = 16;
/// Durata della rotazione della leva di un selettore. Corta: deve dare
/// l'idea del movimento, non far aspettare chi comanda. Il comando parte
/// comunque subito, l'animazione e' solo quello che si vede.
ROTARY_ANIM_MS = 130;
/// Silenzio dell'allarme di una spia, premendola una, due o tre volte.
/// Un minuto per far finire il rumore mentre si va a guardare, dieci per
/// intervenire, un'ora per un guasto noto che si ripara in porto.
MUTE_STEPS_SEC: array[1..3] of Integer = (60, 600, 3600);
/// Passo dell'animazione: ~60 fotogrammi al secondo.
ANIM_TICK_MS = 16;
type
/// Punto di presa del mouse in modalita' configurazione.
TGrabKind = (gkNone, gkBody, gkLeft, gkRight, gkTop, gkBottom,
gkTopLeft, gkTopRight, gkBottomLeft, gkBottomRight);
/// Dove sta l'etichetta rispetto al comando. Sui quadri veri e' quasi
/// sempre serigrafata sotto al pulsante, non scritta sopra.
TCaptionPos = (cpCenter, cpBelow, cpAbove);
/// Forma di pulsanti e interruttori. ksScreen e' il tasto disegnato su un
/// display multifunzione: da acceso si riempie del colore, invece di
/// accendere il bordo, perche' su uno schermo non c'e' una luce dietro.
TKeyShape = (ksAuto, ksRound, ksRect, ksPill, ksScreen);
/// Aspetto del gauge: colonna (livelli, serbatoi) o quadrante a lancetta
/// (strumenti di misura, come sui display di plancia).
TGaugeStyle = (gsBar, gsDial);
/// Cornice attorno a un testo: serve per i marchi serigrafati, che sono
/// una scritta dentro un ovale o un rettangolo stondato.
TFrameKind = (fkNone, fkOval, fkRect, fkRound);
TElementDef = class
public
Kind: TElementKind;
Caption: string;
Slave: Integer;
Channel: Integer;
Left: Integer;
Top: Integer;
Width: Integer;
Height: Integer;
/// Altezza del testo in pixel logici; 0 = font del pannello.
FontSize: Integer;
/// Font del testo; vuoto = quello del pannello.
FontName: string;
/// Spaziatura extra fra le lettere, in pixel logici. I marchi hanno le
/// lettere larghe e senza questo non somigliano.
Spacing: Integer;
CaptionPos: TCaptionPos;
Shape: TKeyShape;
/// Solo ekLabel: cornice attorno alla scritta.
Frame: TFrameKind;
FrameWidth: Integer;
/// Nome nella libreria immagini. Per ekImage vale ImageOff (unica).
ImageOff: string;
ImageOn: string;
// ekGauge e ekDisplay.
RawMin: Integer;
RawMax: Integer;
EngMin: Double;
EngMax: Double;
Units: string;
WarnBelow: Double;
WarnAbove: Double;
// Solo ekGauge.
GaugeStyle: TGaugeStyle;
// Solo ekDisplay.
Digits: Integer;
Decimals: Integer;
// Solo ekRotary.
Positions: Integer;
Legend: string;
/// Bobina del lato destro di un selettore a 3 posizioni. -1 = quella dopo
/// `Channel`, che e' il caso normale; si indica solo quando sul modulo le
/// due bobine non sono contigue.
Channel2: Integer;
/// Selettore a ritorno di molla: tiene la posizione finche' lo si tiene
/// premuto e torna al centro al rilascio. Sui quadri veri e' cosi' il
/// comando di avviamento (STOP - 0 - START).
Momentary: Boolean;
// Solo ekLamp: allarme sonoro quando la spia si accende.
/// Vero se l'accensione deve far suonare qualcosa.
Alarm: Boolean;
/// Nome del file nella cartella "suoni" accanto all'XML; vuoto = nessuno.
Sound: string;
/// Colore acceso: LED della spia, lente del pulsante illuminato.
OnColor: TColor;
/// Colore a riposo. clNone = quello predefinito del tipo. Serve perche'
/// sui quadri veri la lente e' gia' colorata da spenta (STOP rosso,
/// START verde) e si limita a illuminarsi.
OffColor: TColor;
constructor Create(AKind: TElementKind);
/// Colore acceso predefinito del tipo: serve al salvataggio per non
/// scrivere l'attributo quando non e' stato cambiato.
class function DefaultOnColor(AKind: TElementKind): TColor; static;
/// Posizione dell'etichetta con cui nasce il tipo: la spia la porta sotto.
/// Il salvataggio la usa per sapere quando l'attributo va scritto.
class function DefaultCaptionPos(AKind: TElementKind): TCaptionPos; static;
procedure AssignFrom(ASource: TElementDef);
/// Converte il valore grezzo del registro in unita' ingegneristiche.
function RawToEng(ARaw: Word): Double;
function Bounds: TRect;
/// Vero se l'elemento e' agganciato a un modulo: slave 0 vuol dire "non
/// configurato", e un elemento cosi' non si legge e non si comanda.
/// Lo slave 0 sul bus e' l'indirizzo di broadcast, non risponde nessuno:
/// non toglie quindi un indirizzo utile.
function Configured: Boolean;
/// Numero di bobine occupate: il selettore a 3 posizioni ne usa due.
function CoilCount: Integer;
/// Canale della bobina di destra di un selettore a 3 posizioni.
function RightChannel: Integer;
end;
TPlanciaElement = class;
/// AAccepted a False fa tornare l'elemento allo stato precedente: serve
/// quando la scrittura Modbus fallisce.
TElementCommandEvent = procedure(ASender: TPlanciaElement; AOn: Boolean;
var AAccepted: Boolean) of object;
/// APos vale -1, 0 o +1 per i selettori a 3 posizioni; 0 o +1 per quelli a 2.
TElementRotaryEvent = procedure(ASender: TPlanciaElement; APos: Integer;
var AAccepted: Boolean) of object;
TElementEvent = procedure(ASender: TPlanciaElement) of object;
TPlanciaElement = class(TGraphicControl)
private
FDef: TElementDef;
FImages: TImageLibrary;
FEditMode: Boolean;
FSelected: Boolean;
FGridSize: Integer;
FScale: Double;
FState: Boolean;
FNight: Boolean;
FInkColor: TColor;
FRotary: Integer;
/// Posizione della leva come si vede adesso: durante l'animazione e' un
/// valore intermedio fra la posizione di partenza e quella di arrivo.
FShownRotary: Double;
FAnimFrom: Double;
FAnimTo: Double;
FAnimStart: UInt64;
FAnimating: Boolean;
FValue: Double;
FValid: Boolean;
FGrab: TGrabKind;
FStartRect: TRect;
FStartMouse: TPoint;
/// Il mouse ha superato la soglia di trascinamento dopo la pressione.
FDragging: Boolean;
/// Selettore a molla tenuto premuto: la posizione la decide il mouse.
FPressed: Boolean;
/// Allarme sonoro zittito fino a questo momento; 0 = non zittito.
FMuteUntil: UInt64;
/// Il silenzio e' appena finito: l'allarme deve tornare a suonare.
FMuteExpired: Boolean;
/// Quante volte e' stata premuta la spia: 1 = un minuto, 2 = dieci,
/// 3 = un'ora, 4 = si ricomincia con l'allarme attivo.
FMuteStep: Integer;
FOnBeginChange: TElementEvent;
FOnCommand: TElementCommandEvent;
FOnMuteRequest: TElementEvent;
FOnRotary: TElementRotaryEvent;
FOnSelectRequest: TElementEvent;
FOnGeometryChanged: TElementEvent;
procedure SetEditMode(const AValue: Boolean);
procedure SetSelected(const AValue: Boolean);
procedure SetNightMode(const AValue: Boolean);
/// Il colore come va steso adesso: tale e quale di giorno, abbassato di notte.
function Shade(AColor: TColor): TColor;
/// Il colore di una scritta: di notte non si abbassa, si sostituisce,
/// altrimenti un'etichetta nera resterebbe nera su fondo nero.
function Ink(AColor: TColor): TColor;
/// Inchiostro della serigrafia: quello del pannello, nero se non impostato.
function PanelInk: TColor;
procedure SetInkColor(const AValue: TColor);
/// Altezza di un testo che va a capo dentro AWidth.
function TextBlockHeight(const AText: string; AWidth: Integer): Integer;
procedure SetScale(const AValue: Double);
function Command(AOn: Boolean): Boolean;
function RotaryCommand(APos: Integer): Boolean;
/// Avvia (o riaggancia) la rotazione della leva verso FRotary.
procedure StartRotaryAnim;
/// Un fotogramma: False quando l'animazione e' finita.
function AnimStep: Boolean;
/// Angolo della leva in gradi, orario da destra come vuole la GDI.
function LeverAngle: Double;
procedure StepRotary(ADelta: Integer);
/// Porta un selettore a molla direttamente sul lato premuto.
procedure HoldRotary(APos: Integer);
function Sc(AValue: Integer): Integer;
function KeyShape: TKeyShape;
procedure SplitBounds(const ABounds: TRect; out AGraphic, ACaption: TRect);
function HandleRect(AKind: TGrabKind): TRect;
function GrabAt(X, Y: Integer): TGrabKind;
function CurrentPicture: TPicture;
function LogicalParentSize: TPoint;
function DisplayText: string;
procedure ApplyGeometry(const ALogical: TRect);
/// Riempimento attuale di un tasto a video, prima della modalita' notturna.
function ScreenKeyFill: TColor;
procedure PaintKey(const ABounds: TRect);
procedure PaintLamp(const ABounds: TRect);
/// Sbarra sulla spia il cui allarme e' stato zittito.
procedure PaintMuteMark(const ABounds: TRect);
procedure PaintGauge(const ABounds: TRect);
procedure PaintDial(const ABounds: TRect);
procedure PaintDisplay(const ABounds: TRect);
procedure PaintRotary(const ABounds: TRect);
procedure PaintLabel(const ABounds: TRect);
procedure PaintPicture(const ABounds: TRect; APicture: TPicture);
procedure PaintMissingPicture(const ABounds: TRect);
procedure PaintCaption(const ABounds: TRect; AOnImage: Boolean);
procedure PaintEditOverlay(const ABounds: TRect);
protected
procedure Paint; override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer); override;
public
constructor CreateElement(AOwner: TComponent; ADef: TElementDef;
AImages: TImageLibrary);
destructor Destroy; override;
/// Riporta il controllo alla geometria della definizione, applicando lo zoom.
procedure ApplyDef;
/// Valore letto dal registro analogico (ekGauge, ekDisplay).
procedure SetAnalogValue(const AValue: Double);
/// Dato non disponibile: gauge svuotato, spia spenta, display spento.
procedure SetInvalid;
/// Allinea lo stato a quello letto dal campo (spie e interruttori).
procedure SyncState(AOn: Boolean);
/// Vero se l'allarme sonoro di questa spia e' zittito adesso.
function AlarmMuted: Boolean;
/// Secondi che mancano alla fine del silenzio; 0 se non e' zittita.
function MuteSecondsLeft: Integer;
/// Una pressione in piu': torna i secondi di silenzio decisi, 0 se la
/// pressione ha riacceso l'allarme.
function PressAlarmMute: Integer;
/// Toglie il silenzio senza passare dalle pressioni (cambio plancia,
/// uscita dalla modalita' plancia).
procedure ClearAlarmMute;
/// Vero una volta sola, quando il silenzio e' finito: chi lo chiede sa
/// che l'allarme va rifatto sentire.
function TakeMuteExpired: Boolean;
/// Allinea la posizione del selettore a quella letta dal campo.
procedure SyncRotary(APos: Integer);
property Def: TElementDef read FDef;
property EditMode: Boolean read FEditMode write SetEditMode;
property NightMode: Boolean read FNight write SetNightMode;
/// Colore delle scritte sul pannello (etichette esterne, legende, titoli).
/// clNone = nero. Serve alle plance a fondo scuro.
property InkColor: TColor read FInkColor write SetInkColor;
property Selected: Boolean read FSelected write SetSelected;
property GridSize: Integer read FGridSize write FGridSize;
property Scale: Double read FScale write SetScale;
property State: Boolean read FState;
property Rotary: Integer read FRotary;
property OnCommand: TElementCommandEvent read FOnCommand write FOnCommand;
/// La spia e' stata premuta per zittire (o riaccendere) il suo allarme.
property OnMuteRequest: TElementEvent read FOnMuteRequest
write FOnMuteRequest;
property OnRotary: TElementRotaryEvent read FOnRotary write FOnRotary;
property OnSelectRequest: TElementEvent read FOnSelectRequest
write FOnSelectRequest;
property OnGeometryChanged: TElementEvent read FOnGeometryChanged
write FOnGeometryChanged;
/// Un trascinamento sta per cambiare posizione o misure: scatta una volta
/// sola, prima della prima modifica.
property OnBeginChange: TElementEvent read FOnBeginChange
write FOnBeginChange;
published
property Color;
property Font;
property ParentFont;
property Hint;
property ShowHint;
property OnDragOver;
property OnDragDrop;
end;
const
CAPTION_POS_IDS: array[TCaptionPos] of string = ('center', 'below', 'above');
SHAPE_IDS: array[TKeyShape] of string = ('auto', 'round', 'rect', 'pill',
'screen');
GAUGE_STYLE_IDS: array[TGaugeStyle] of string = ('bar', 'dial');
FRAME_IDS: array[TFrameKind] of string = ('none', 'oval', 'rect', 'round');
FRAME_NAMES: array[TFrameKind] of string =
('nessuna', 'ovale', 'rettangolo', 'stondata');
CAPTION_POS_NAMES: array[TCaptionPos] of string =
('sopra il comando', 'sotto', 'in alto');
SHAPE_NAMES: array[TKeyShape] of string =
('automatica', 'tonda', 'squadrata', 'a pillola', 'a video');
function ElementKindFromId(const AId: string; out AKind: TElementKind): Boolean;
function CaptionPosFromId(const AId: string; ADefault: TCaptionPos): TCaptionPos;
function ShapeFromId(const AId: string; ADefault: TKeyShape): TKeyShape;
function GaugeStyleFromId(const AId: string; ADefault: TGaugeStyle): TGaugeStyle;
function FrameFromId(const AId: string; ADefault: TFrameKind): TFrameKind;
/// True per i tipi legati a un canale Modbus.
function ElementHasChannel(AKind: TElementKind): Boolean;
/// True per i tipi che possono usare un'immagine al posto del disegno.
function ElementUsesImages(AKind: TElementKind): Boolean;
/// True per i tipi che leggono un registro analogico.
function ElementIsAnalog(AKind: TElementKind): Boolean;
implementation
const
HANDLE_SIZE = 7;
GRAB_CURSORS: array[TGrabKind] of TCursor = (crDefault, crSizeAll,
crSizeWE, crSizeWE, crSizeNS, crSizeNS,
crSizeNWSE, crSizeNESW, crSizeNESW, crSizeNWSE);
type
/// Un timer solo per tutte le animazioni della plancia: un TTimer per
/// elemento su un quadro da cinquanta comandi sarebbe uno spreco, e
/// l'elemento e' un TGraphicControl, non ha una finestra sua.
/// Nasce al primo elemento che si muove e si ferma quando non ce n'e' piu'.
TElementAnimator = class
private
FTimer: TTimer;
FItems: TList<TPlanciaElement>;
procedure Tick(Sender: TObject);
public
constructor Create;
destructor Destroy; override;
procedure Add(AElement: TPlanciaElement);
procedure Remove(AElement: TPlanciaElement);
end;
var
Animator: TElementAnimator;
const
// Segmenti accesi per cifra: bit 0..6 = a,b,c,d,e,f,g
SEG_DIGITS: array[0..9] of Byte =
($3F, $06, $5B, $4F, $66, $6D, $7D, $07, $7F, $6F);
SEG_MINUS = $40;
function ElementKindFromId(const AId: string; out AKind: TElementKind): Boolean;
var
K: TElementKind;
begin
for K := Low(TElementKind) to High(TElementKind) do
if SameText(AId, ELEMENT_IDS[K]) then
begin
AKind := K;
Exit(True);
end;
AKind := ekButton;
Result := False;
end;
function CaptionPosFromId(const AId: string; ADefault: TCaptionPos): TCaptionPos;
var
P: TCaptionPos;
begin
for P := Low(TCaptionPos) to High(TCaptionPos) do
if SameText(AId, CAPTION_POS_IDS[P]) then
Exit(P);
Result := ADefault;
end;
function ShapeFromId(const AId: string; ADefault: TKeyShape): TKeyShape;
var
S: TKeyShape;
begin
for S := Low(TKeyShape) to High(TKeyShape) do
if SameText(AId, SHAPE_IDS[S]) then
Exit(S);
Result := ADefault;
end;
function GaugeStyleFromId(const AId: string; ADefault: TGaugeStyle): TGaugeStyle;
var
S: TGaugeStyle;
begin
for S := Low(TGaugeStyle) to High(TGaugeStyle) do
if SameText(AId, GAUGE_STYLE_IDS[S]) then
Exit(S);
Result := ADefault;
end;
function FrameFromId(const AId: string; ADefault: TFrameKind): TFrameKind;
var
F: TFrameKind;
begin
for F := Low(TFrameKind) to High(TFrameKind) do
if SameText(AId, FRAME_IDS[F]) then
Exit(F);
Result := ADefault;
end;
function ElementHasChannel(AKind: TElementKind): Boolean;
begin
Result := not (AKind in [ekImage, ekLabel]);
end;
function ElementUsesImages(AKind: TElementKind): Boolean;
begin
Result := AKind in [ekButton, ekSwitch, ekLamp, ekImage];
end;
function ElementIsAnalog(AKind: TElementKind): Boolean;
begin
Result := AKind in [ekGauge, ekDisplay];
end;
/// Colore a meta' strada: AAmount 0 = AFrom, 1 = ATo.
function BlendColor(AFrom, ATo: TColor; const AAmount: Double): TColor;
var
A, B: Longint;
begin
A := ColorToRGB(AFrom);
B := ColorToRGB(ATo);
Result := TColor(RGB(
Round(GetRValue(A) + (GetRValue(B) - GetRValue(A)) * AAmount),
Round(GetGValue(A) + (GetGValue(B) - GetGValue(A)) * AAmount),
Round(GetBValue(A) + (GetBValue(B) - GetBValue(A)) * AAmount)));
end;
/// Luminosita' percepita 0..255: decide se su un fondo va scritto nero o chiaro.
function ColorLuma(AColor: TColor): Integer;
var
C: Longint;
begin
C := ColorToRGB(AColor);
Result := (GetRValue(C) * 299 + GetGValue(C) * 587 + GetBValue(C) * 114)
div 1000;
end;
procedure DrawCenteredText(ACanvas: TCanvas; const ARect: TRect;
const AText: string);
var
Calc, Draw: TRect;
begin
if AText = '' then
Exit;
Calc := ARect;
Winapi.Windows.DrawText(ACanvas.Handle, PChar(AText), Length(AText), Calc,
DT_CENTER or DT_WORDBREAK or DT_CALCRECT);
Draw := ARect;
Draw.Top := ARect.Top +
((ARect.Bottom - ARect.Top) - (Calc.Bottom - Calc.Top)) div 2;
if Draw.Top < ARect.Top then
Draw.Top := ARect.Top;
Winapi.Windows.DrawText(ACanvas.Handle, PChar(AText), Length(AText), Draw,
DT_CENTER or DT_WORDBREAK or DT_END_ELLIPSIS);
end;
/// Disegna una stringa numerica come display a sette segmenti. I segmenti
/// spenti restano visibili in scuro, come sui display veri.
procedure DrawSevenSegment(ACanvas: TCanvas; const ABounds: TRect;
const AText: string; AOnColor, AOffColor: TColor);
var
Cells, I, CellW, CellH, X, Y, T, MidY: Integer;
Ch: Char;
Mask: Byte;
HasDot: Boolean;
procedure Seg(ABit: Integer; const R: TRect);
begin
if (Mask and (1 shl ABit)) <> 0 then
ACanvas.Brush.Color := AOnColor
else
ACanvas.Brush.Color := AOffColor;
ACanvas.FillRect(R);
end;
begin
// Il punto decimale non occupa una cella intera.
Cells := 0;
for I := 1 to Length(AText) do
if AText[I] <> '.' then
Inc(Cells);
if Cells = 0 then
Exit;
CellH := ABounds.Height;
CellW := ABounds.Width div Cells;
if (CellW < 6) or (CellH < 10) then
Exit;
T := Max(2, CellH div 9);
ACanvas.Brush.Style := bsSolid;
X := ABounds.Left + (ABounds.Width - CellW * Cells) div 2;
Y := ABounds.Top;
MidY := Y + CellH div 2;
I := 1;
while I <= Length(AText) do
begin
Ch := AText[I];
if Ch = '.' then
begin
Inc(I);
Continue;
end;
HasDot := (I < Length(AText)) and (AText[I + 1] = '.');
if CharInSet(Ch, ['0'..'9']) then
Mask := SEG_DIGITS[Ord(Ch) - Ord('0')]
else if Ch = '-' then
Mask := SEG_MINUS
else
Mask := 0;
// Larghezza utile della cifra, lasciando spazio fra una cifra e l'altra.
Seg(0, Rect(X + T, Y, X + CellW - T - T, Y + T));
Seg(1, Rect(X + CellW - T - T, Y + T, X + CellW - T, MidY));
Seg(2, Rect(X + CellW - T - T, MidY, X + CellW - T, Y + CellH - T));
Seg(3, Rect(X + T, Y + CellH - T, X + CellW - T - T, Y + CellH));
Seg(4, Rect(X, MidY, X + T, Y + CellH - T));
Seg(5, Rect(X, Y + T, X + T, MidY));
Seg(6, Rect(X + T, MidY - T div 2, X + CellW - T - T, MidY - T div 2 + T));
if HasDot then
begin
ACanvas.Brush.Color := AOnColor;
ACanvas.FillRect(Rect(X + CellW - T, Y + CellH - T,
X + CellW, Y + CellH));
end;
Inc(X, CellW);
Inc(I);
end;
end;
{ TElementAnimator }
constructor TElementAnimator.Create;
begin
inherited Create;
FItems := TList<TPlanciaElement>.Create;
FTimer := TTimer.Create(nil);
FTimer.Interval := ANIM_TICK_MS;
FTimer.Enabled := False;
FTimer.OnTimer := Tick;
end;
destructor TElementAnimator.Destroy;
begin
FTimer.Free;
FItems.Free;
inherited;
end;
procedure TElementAnimator.Add(AElement: TPlanciaElement);
begin
if FItems.IndexOf(AElement) < 0 then
FItems.Add(AElement);
FTimer.Enabled := True;
end;
procedure TElementAnimator.Remove(AElement: TPlanciaElement);
begin
FItems.Remove(AElement);
// Nessuno si muove: il timer si ferma, non gira a vuoto.
FTimer.Enabled := FItems.Count > 0;
end;
procedure TElementAnimator.Tick(Sender: TObject);
var
I: Integer;
begin
// A rovescio: un elemento che finisce si toglie dalla lista.
for I := FItems.Count - 1 downto 0 do
if not FItems[I].AnimStep then
Remove(FItems[I]);
end;
{ TElementDef }
constructor TElementDef.Create(AKind: TElementKind);
begin
inherited Create;
Kind := AKind;
Caption := ELEMENT_NAMES[AKind];
if AKind = ekImage then
Caption := '';
Slave := 1;
Channel := 0;
Left := 0;
Top := 0;
Width := ELEMENT_DEF_W[AKind];
Height := ELEMENT_DEF_H[AKind];
FontSize := 0;
RawMin := 0;
RawMax := 4095;
EngMin := 0;
EngMax := 100;
Units := '%';
WarnBelow := GAUGE_NO_WARN_LO;
WarnAbove := GAUGE_NO_WARN_HI;
GaugeStyle := gsBar;
Digits := 4;
Decimals := 1;
Positions := 2;
Legend := 'OFF < > ON';
Channel2 := -1;
Momentary := False;
Alarm := False;
Sound := '';
Shape := ksAuto;
Frame := fkNone;
FrameWidth := 3;
Spacing := 0;
OnColor := DefaultOnColor(AKind);
OffColor := clNone;
CaptionPos := DefaultCaptionPos(AKind);
end;
class function TElementDef.DefaultCaptionPos(AKind: TElementKind): TCaptionPos;
begin
if AKind = ekLamp then
Result := cpBelow
else
Result := cpCenter;
end;
class function TElementDef.DefaultOnColor(AKind: TElementKind): TColor;
begin
case AKind of
ekLamp: Result := CLR_LAMP_ON;
// Per una scritta "acceso" significa semplicemente il colore dell'inchiostro.
ekLabel: Result := clWindowText;
else
Result := CLR_ELEM_ON;
end;
end;
procedure TElementDef.AssignFrom(ASource: TElementDef);
begin
Kind := ASource.Kind;
Caption := ASource.Caption;
Slave := ASource.Slave;
Channel := ASource.Channel;
Left := ASource.Left;
Top := ASource.Top;
Width := ASource.Width;
Height := ASource.Height;
FontSize := ASource.FontSize;
FontName := ASource.FontName;
Spacing := ASource.Spacing;
CaptionPos := ASource.CaptionPos;
Shape := ASource.Shape;
Frame := ASource.Frame;
FrameWidth := ASource.FrameWidth;
ImageOff := ASource.ImageOff;
ImageOn := ASource.ImageOn;
RawMin := ASource.RawMin;
RawMax := ASource.RawMax;
EngMin := ASource.EngMin;
EngMax := ASource.EngMax;
Units := ASource.Units;
WarnBelow := ASource.WarnBelow;
WarnAbove := ASource.WarnAbove;
GaugeStyle := ASource.GaugeStyle;
Digits := ASource.Digits;
Decimals := ASource.Decimals;
Positions := ASource.Positions;
Legend := ASource.Legend;
Channel2 := ASource.Channel2;
Momentary := ASource.Momentary;
Alarm := ASource.Alarm;
Sound := ASource.Sound;
OnColor := ASource.OnColor;
OffColor := ASource.OffColor;
end;
function TElementDef.RawToEng(ARaw: Word): Double;
begin
if RawMax = RawMin then
Exit(EngMin);
Result := EngMin + (Integer(ARaw) - RawMin) * (EngMax - EngMin) /
(RawMax - RawMin);
end;
function TElementDef.Bounds: TRect;
begin
Result := Rect(Left, Top, Left + Width, Top + Height);
end;
function TElementDef.Configured: Boolean;
begin
Result := ElementHasChannel(Kind) and (Slave >= 1) and (Channel >= 0);
end;
function TElementDef.RightChannel: Integer;
begin
if Channel2 >= 0 then
Result := Channel2
else
Result := Channel + 1;
end;
function TElementDef.CoilCount: Integer;
begin
if (Kind = ekRotary) and (Positions >= 3) then
Result := 2
else
Result := 1;
end;
{ TPlanciaElement }
constructor TPlanciaElement.CreateElement(AOwner: TComponent;
ADef: TElementDef; AImages: TImageLibrary);
begin
inherited Create(AOwner);
FDef := ADef;
FImages := AImages;
FGridSize := 10;
FScale := 1;
FValue := 0;
FValid := False;
FRotary := 0;
FShownRotary := 0;
FInkColor := clNone;
Color := clBtnFace;
ShowHint := True;
ApplyDef;
end;
destructor TPlanciaElement.Destroy;
begin
// Un elemento distrutto a meta' animazione (ricostruzione del pannello,
// annulla) non deve restare nella lista dell'animatore.
if Animator <> nil then
Animator.Remove(Self);
inherited;
end;
procedure TPlanciaElement.StartRotaryAnim;
begin
if FEditMode then
begin
// In configurazione non si comanda niente: la leva sta dove dice la
// definizione, senza animazioni.
FShownRotary := FRotary;
Exit;
end;
FAnimFrom := FShownRotary;
FAnimTo := FRotary;
if SameValue(FAnimFrom, FAnimTo) then
Exit;
FAnimStart := GetTickCount64;
FAnimating := True;
if Animator = nil then
Animator := TElementAnimator.Create;
Animator.Add(Self);
end;
function TPlanciaElement.AnimStep: Boolean;
var
T: Double;
begin
if not FAnimating then
Exit(False);
T := (GetTickCount64 - FAnimStart) / ROTARY_ANIM_MS;
if T >= 1 then
begin
T := 1;
FAnimating := False;
end;
// Partenza e arrivo morbidi: una leva vera non scatta a velocita' costante.
FShownRotary := FAnimFrom + (FAnimTo - FAnimFrom) * (T * T * (3 - 2 * T));
Invalidate;
Result := FAnimating;
end;
function TPlanciaElement.LeverAngle: Double;
begin
// Gradi orari a partire da destra: 180 = sinistra, 270 = in alto,
// 360 = destra. Cosi' la leva passa sempre per l'alto e l'interpolazione
// fra due posizioni e' una rotazione, non un salto.
if FDef.Positions >= 3 then
Result := 270 + FShownRotary * 90
else
Result := 180 + FShownRotary * 180;
end;
function TPlanciaElement.Sc(AValue: Integer): Integer;
begin
Result := Round(AValue * FScale);
if (Result < 1) and (AValue > 0) then
Result := 1;
end;
procedure TPlanciaElement.ApplyDef;
begin
SetBounds(Round(FDef.Left * FScale), Round(FDef.Top * FScale),
Round(FDef.Width * FScale), Round(FDef.Height * FScale));
Invalidate;
end;
procedure TPlanciaElement.SetScale(const AValue: Double);
begin
if (AValue <= 0) or SameValue(FScale, AValue) then
Exit;
FScale := AValue;
ApplyDef;
end;
procedure TPlanciaElement.SetEditMode(const AValue: Boolean);
begin
if FEditMode = AValue then
Exit;
FEditMode := AValue;
if FEditMode then
begin
FState := False;
FValid := False;
FRotary := 0;
FShownRotary := 0;
FAnimating := False;
if Animator <> nil then
Animator.Remove(Self);
end
else
Cursor := crDefault;
Invalidate;
end;
procedure TPlanciaElement.SetSelected(const AValue: Boolean);
begin
if FSelected = AValue then
Exit;
FSelected := AValue;
if not FSelected then
Cursor := crDefault;
Invalidate;
end;
procedure TPlanciaElement.SetAnalogValue(const AValue: Double);
begin
if FValid and (FValue = AValue) then
Exit;
FValue := AValue;
FValid := True;
Invalidate;
end;
procedure TPlanciaElement.SetInvalid;
begin
if not FValid and not FState then
Exit;
FValid := False;
if FDef.Kind = ekLamp then
FState := False;
Invalidate;
end;
function TPlanciaElement.AlarmMuted: Boolean;
begin
Result := (FMuteUntil > 0) and (GetTickCount64 < FMuteUntil);
if (FMuteUntil > 0) and not Result then
begin
// Silenzio scaduto: si riparte da capo, la prossima pressione vale un
// minuto e non un'ora, e l'allarme torna a farsi sentire.
FMuteUntil := 0;
FMuteStep := 0;
FMuteExpired := True;
Invalidate;
end;
end;
function TPlanciaElement.MuteSecondsLeft: Integer;
begin
if not AlarmMuted then
Exit(0);
Result := Integer((FMuteUntil - GetTickCount64 + 999) div 1000);
end;
function TPlanciaElement.PressAlarmMute: Integer;
begin
// Ogni pressione allunga il silenzio; la quarta lo toglie, cosi' non si
// deve aspettare un'ora per riavere l'allarme.
if AlarmMuted then
Inc(FMuteStep)
else
FMuteStep := 1;
if FMuteStep > High(MUTE_STEPS_SEC) then
begin
ClearAlarmMute;
Exit(0);
end;
Result := MUTE_STEPS_SEC[FMuteStep];
FMuteUntil := GetTickCount64 + UInt64(Result) * 1000;
Invalidate;
end;
procedure TPlanciaElement.ClearAlarmMute;
begin
if (FMuteUntil = 0) and (FMuteStep = 0) then
Exit;
// Riattivare a mano vale come un silenzio scaduto: se la condizione c'e'
// ancora, l'allarme torna a suonare.
if FMuteUntil > 0 then
FMuteExpired := True;
FMuteUntil := 0;
FMuteStep := 0;
Invalidate;
end;
function TPlanciaElement.TakeMuteExpired: Boolean;
begin
AlarmMuted; // fa scattare la scadenza se e' il momento
Result := FMuteExpired;
FMuteExpired := False;
end;
procedure TPlanciaElement.SyncState(AOn: Boolean);
begin
if FState = AOn then
Exit;
FState := AOn;
Invalidate;
end;
procedure TPlanciaElement.SetNightMode(const AValue: Boolean);
begin
if FNight = AValue then
Exit;
FNight := AValue;
Invalidate;
end;
function TPlanciaElement.Shade(AColor: TColor): TColor;
var
C: Longint;
begin
if not FNight then
Exit(AColor);
C := ColorToRGB(AColor);
Result := TColor(RGB(
Round(GetRValue(C) * NIGHT_DIM),
Round(GetGValue(C) * NIGHT_DIM),
Round(GetBValue(C) * NIGHT_DIM)));
end;
function TPlanciaElement.Ink(AColor: TColor): TColor;
begin
if FNight then
Result := CLR_NIGHT_INK
else
Result := AColor;
end;
function TPlanciaElement.PanelInk: TColor;
begin
if FInkColor = clNone then
Result := Ink(clWindowText)
else
Result := Ink(FInkColor);
end;
procedure TPlanciaElement.SetInkColor(const AValue: TColor);
begin
if FInkColor = AValue then
Exit;
FInkColor := AValue;
Invalidate;
end;
function TPlanciaElement.TextBlockHeight(const AText: string;
AWidth: Integer): Integer;
var
Calc: TRect;
begin
if AText = '' then
Exit(0);
Calc := Rect(0, 0, Max(1, AWidth), 0);
Winapi.Windows.DrawText(Canvas.Handle, PChar(AText), Length(AText), Calc,
DT_CENTER or DT_WORDBREAK or DT_CALCRECT);
Result := Calc.Height;
end;
procedure TPlanciaElement.SyncRotary(APos: Integer);
begin
// Mentre un selettore a molla e' tenuto premuto la posizione la decide il
// mouse, non il campo: una lettura arrivata in quell'istante lo farebbe
// scattare al centro sotto il dito.
if FPressed then
Exit;
if FRotary = APos then
Exit;
FRotary := APos;
StartRotaryAnim;
Invalidate;
end;
function TPlanciaElement.Command(AOn: Boolean): Boolean;
begin
Result := True;
if Assigned(FOnCommand) then
FOnCommand(Self, AOn, Result);
end;
function TPlanciaElement.RotaryCommand(APos: Integer): Boolean;
begin
Result := True;
if Assigned(FOnRotary) then
FOnRotary(Self, APos, Result);
end;
procedure TPlanciaElement.HoldRotary(APos: Integer);
var
NewPos, OldPos: Integer;
begin
// A due posizioni il lato sinistro e' lo zero, quindi premere a sinistra
// vale come non premere.
NewPos := APos;
if (FDef.Positions < 3) and (NewPos < 0) then
NewPos := 0;
if NewPos = FRotary then
Exit;
OldPos := FRotary;
FRotary := NewPos;
StartRotaryAnim;
Invalidate;
if not RotaryCommand(NewPos) then
begin
FRotary := OldPos;
StartRotaryAnim;
Invalidate;
end;
end;
procedure TPlanciaElement.StepRotary(ADelta: Integer);
var
Lo, Hi, NewPos, OldPos: Integer;
begin
if FDef.Positions >= 3 then
begin
Lo := -1;
Hi := 1;
end
else
begin
Lo := 0;
Hi := 1;
end;
NewPos := FRotary + ADelta;
if NewPos < Lo then
NewPos := Lo;
if NewPos > Hi then
NewPos := Hi;
if NewPos = FRotary then
Exit;
OldPos := FRotary;
FRotary := NewPos;
StartRotaryAnim;
Invalidate;
if not RotaryCommand(NewPos) then
begin
FRotary := OldPos;
StartRotaryAnim;
Invalidate;
end;
end;
{ geometria e mouse }
function TPlanciaElement.LogicalParentSize: TPoint;
begin
if Parent = nil then
Exit(Point(MaxInt, MaxInt));
Result := Point(Round(Parent.ClientWidth / FScale),
Round(Parent.ClientHeight / FScale));
end;
function TPlanciaElement.HandleRect(AKind: TGrabKind): TRect;
var
R: TRect;
H, CX, CY: Integer;
begin
R := ClientRect;
H := HANDLE_SIZE;
CX := (R.Left + R.Right - H) div 2;
CY := (R.Top + R.Bottom - H) div 2;
case AKind of
gkTopLeft: Result := Rect(R.Left, R.Top, R.Left + H, R.Top + H);
gkTop: Result := Rect(CX, R.Top, CX + H, R.Top + H);
gkTopRight: Result := Rect(R.Right - H, R.Top, R.Right, R.Top + H);
gkLeft: Result := Rect(R.Left, CY, R.Left + H, CY + H);
gkRight: Result := Rect(R.Right - H, CY, R.Right, CY + H);
gkBottomLeft: Result := Rect(R.Left, R.Bottom - H, R.Left + H, R.Bottom);
gkBottom: Result := Rect(CX, R.Bottom - H, CX + H, R.Bottom);
gkBottomRight: Result := Rect(R.Right - H, R.Bottom - H, R.Right, R.Bottom);
else
Result := TRect.Empty;
end;
end;
function TPlanciaElement.GrabAt(X, Y: Integer): TGrabKind;
var
K: TGrabKind;
begin
Result := gkNone;
if not (FEditMode and FSelected) then
Exit;
for K := gkLeft to gkBottomRight do
if PtInRect(HandleRect(K), Point(X, Y)) then
Exit(K);
end;
procedure TPlanciaElement.ApplyGeometry(const ALogical: TRect);
begin
if (FDef.Left = ALogical.Left) and (FDef.Top = ALogical.Top) and
(FDef.Width = ALogical.Width) and (FDef.Height = ALogical.Height) then
Exit;
FDef.Left := ALogical.Left;
FDef.Top := ALogical.Top;
FDef.Width := ALogical.Width;
FDef.Height := ALogical.Height;
ApplyDef;
if Assigned(FOnGeometryChanged) then
FOnGeometryChanged(Self);
end;
procedure TPlanciaElement.MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
begin
inherited;
if Button <> mbLeft then
Exit;
if FEditMode then
begin
// La selezione avviene prima della presa: le maniglie esistono solo
// sull'elemento selezionato.
if Assigned(FOnSelectRequest) then
FOnSelectRequest(Self);
FGrab := GrabAt(X, Y);
if FGrab = gkNone then
FGrab := gkBody;
FStartRect := FDef.Bounds;
FStartMouse := ClientToParent(Point(X, Y), Parent);
FDragging := False;
Exit;
end;
// Pulsante e interruttore agiscono alla pressione, senza aspettare il
// rilascio: sul quadro vero il contatto scatta quando si preme il tasto.
if FDef.Kind = ekButton then
begin
FState := True;
Invalidate;
if not Command(True) then
begin
FState := False;
Invalidate;
end;
end
else if FDef.Kind = ekSwitch then
begin
FState := not FState;
Invalidate;
if not Command(FState) then
begin
// Scrittura non andata a segno: si torna allo stato di prima.
FState := not FState;
Invalidate;
end;
end
else if FDef.Kind = ekLamp then
begin
// Premere una spia che sta suonando la zittisce: e' il gesto che viene
// naturale, si preme quello che sta dando fastidio. Una spia spenta, o
// senza allarme, non fa niente.
if FState and FDef.Alarm and Assigned(FOnMuteRequest) then
FOnMuteRequest(Self);
end
else if FDef.Kind = ekRotary then
begin
// Si "gira" la manopola premendo dal lato in cui la si vuole portare.
if not FDef.Momentary then
begin
if X < ClientWidth div 2 then
StepRotary(-1)
else
StepRotary(1);
end
else
begin
// A ritorno di molla si va direttamente sul lato premuto e ci si resta
// solo finche' il tasto e' giu'.
FPressed := True;
if X < ClientWidth div 2 then
HoldRotary(-1)
else
HoldRotary(1);
end;
end;
end;
procedure TPlanciaElement.MouseMove(Shift: TShiftState; X, Y: Integer);
var
P: TPoint;
DX, DY: Integer;
R: TRect;
Lim: TPoint;
function Snap(AValue: Integer): Integer;
begin
if FGridSize > 1 then
Result := Round(AValue / FGridSize) * FGridSize
else
Result := AValue;
end;
begin
inherited;
if not FEditMode then
Exit;
if FGrab = gkNone then
begin
Cursor := GRAB_CURSORS[GrabAt(X, Y)];
Exit;
end;
if not (ssLeft in Shift) then
begin
FGrab := gkNone;
FDragging := False;
Exit;
end;
P := ClientToParent(Point(X, Y), Parent);
// Un click non e' un trascinamento. Senza soglia il minimo tremolio della
// mano spostava l'elemento, e con la griglia attiva bastava anche un
// movimento nullo: la posizione veniva riagganciata al multiplo di 10 e un
// elemento a 245 saltava a 250 solo per essere stato cliccato.
if not FDragging then
begin
if (Abs(P.X - FStartMouse.X) < GetSystemMetrics(SM_CXDRAG)) and
(Abs(P.Y - FStartMouse.Y) < GetSystemMetrics(SM_CYDRAG)) then
Exit;
FDragging := True;
// Prima di toccare la geometria: chi tiene lo storico delle modifiche
// deve poter fotografare lo stato di partenza.
if Assigned(FOnBeginChange) then
FOnBeginChange(Self);
end;
DX := Round((P.X - FStartMouse.X) / FScale);
DY := Round((P.Y - FStartMouse.Y) / FScale);
R := FStartRect;
case FGrab of
gkBody:
begin
// Un asse che non si e' mosso non viene riagganciato alla griglia:
// spostando in orizzontale l'elemento non deve saltare in verticale.
if DX <> 0 then
R.Offset(Snap(R.Left + DX) - R.Left, 0);
if DY <> 0 then
R.Offset(0, Snap(R.Top + DY) - R.Top);
end;
gkLeft:
R.Left := Snap(R.Left + DX);
gkRight:
R.Right := Snap(R.Right + DX);
gkTop:
R.Top := Snap(R.Top + DY);
gkBottom:
R.Bottom := Snap(R.Bottom + DY);
gkTopLeft:
begin
R.Left := Snap(R.Left + DX);
R.Top := Snap(R.Top + DY);
end;
gkTopRight:
begin
R.Right := Snap(R.Right + DX);
R.Top := Snap(R.Top + DY);
end;
gkBottomLeft:
begin
R.Left := Snap(R.Left + DX);
R.Bottom := Snap(R.Bottom + DY);
end;
gkBottomRight:
begin
R.Right := Snap(R.Right + DX);
R.Bottom := Snap(R.Bottom + DY);
end;
end;
// Dimensione minima: si muove il bordo che l'utente sta trascinando.
if R.Right - R.Left < MIN_ELEMENT_SIZE then
if FGrab in [gkLeft, gkTopLeft, gkBottomLeft] then
R.Left := R.Right - MIN_ELEMENT_SIZE
else
R.Right := R.Left + MIN_ELEMENT_SIZE;
if R.Bottom - R.Top < MIN_ELEMENT_SIZE then
if FGrab in [gkTop, gkTopLeft, gkTopRight] then
R.Top := R.Bottom - MIN_ELEMENT_SIZE
else
R.Bottom := R.Top + MIN_ELEMENT_SIZE;
// Dentro il pannello.
Lim := LogicalParentSize;
if R.Left < 0 then
if FGrab = gkBody then
R.Offset(-R.Left, 0)
else
R.Left := 0;
if R.Top < 0 then
if FGrab = gkBody then
R.Offset(0, -R.Top)
else
R.Top := 0;
if R.Right > Lim.X then
if FGrab = gkBody then
R.Offset(Lim.X - R.Right, 0)
else
R.Right := Lim.X;
if R.Bottom > Lim.Y then
if FGrab = gkBody then
R.Offset(0, Lim.Y - R.Bottom)
else
R.Bottom := Lim.Y;
ApplyGeometry(R);
end;
procedure TPlanciaElement.MouseUp(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
begin
inherited;
if Button <> mbLeft then
Exit;
if FEditMode then
begin
FGrab := gkNone;
FDragging := False;
Exit;
end;
// Interruttori e selettori normali hanno gia' agito alla pressione. Il
// rilascio serve a chi e' momentaneo: pulsante e selettore a molla tornano
// sempre a riposo, anche fuori dai bordi e anche se la scrittura di andata
// era fallita, perche' un canale momentaneo non deve mai restare eccitato.
if FDef.Kind = ekButton then
begin
FState := False;
Invalidate;
Command(False);
end
else if (FDef.Kind = ekRotary) and FDef.Momentary then
begin
FPressed := False;
FRotary := 0;
StartRotaryAnim;
Invalidate;
RotaryCommand(0);
end;
end;
{ disegno }
function TPlanciaElement.CurrentPicture: TPicture;
var
N: string;
begin
Result := nil;
if (FImages = nil) or not ElementUsesImages(FDef.Kind) then
Exit;
if FDef.Kind = ekImage then
N := FDef.ImageOff
else if FState and (FDef.ImageOn <> '') then
N := FDef.ImageOn
else
N := FDef.ImageOff;
if N = '' then
Exit;
Result := FImages.Picture(N);
end;
procedure TPlanciaElement.PaintPicture(const ABounds: TRect;
APicture: TPicture);
begin
// Nessun riempimento di sfondo: le PNG con trasparenza devono lasciar
// vedere lo sfondo della plancia.
Canvas.StretchDraw(ABounds, APicture.Graphic);
end;
procedure TPlanciaElement.PaintMissingPicture(const ABounds: TRect);
begin
Canvas.Brush.Style := bsClear;
Canvas.Pen.Style := psDash;
Canvas.Pen.Color := clGray;
Canvas.Rectangle(ABounds);
Canvas.Pen.Style := psSolid;
Canvas.Font.Color := clGray;
Canvas.Font.Style := [];
if FDef.ImageOff = '' then
DrawCenteredText(Canvas, ABounds, '(nessuna immagine)')
else
DrawCenteredText(Canvas, ABounds, FDef.ImageOff + #13'non disponibile');
end;
procedure TPlanciaElement.PaintCaption(const ABounds: TRect; AOnImage: Boolean);
var
R: TRect;
I: Integer;
const
// Scostamenti per il contorno: sopra, sotto, sinistra, destra.
OFS: array[0..3, 0..1] of Integer = ((0, -1), (0, 1), (-1, 0), (1, 0));
begin
if FDef.Caption = '' then
Exit;
Canvas.Brush.Style := bsClear;
// Etichetta fuori dal comando: sta sulla serigrafia del pannello, quindi
// resta nera qualunque sia lo stato.
if FDef.CaptionPos <> cpCenter then
begin
Canvas.Font.Color := PanelInk;
Canvas.Font.Style := [];
DrawCenteredText(Canvas, ABounds, FDef.Caption);
Exit;
end;
if AOnImage then
begin
// Su un'immagine non si sa se il fondo e' chiaro o scuro: testo bianco
// con contorno nero, leggibile in entrambi i casi.
Canvas.Font.Style := [fsBold];
Canvas.Font.Color := clBlack;
for I := 0 to High(OFS) do
begin
R := ABounds;
R.Offset(OFS[I][0], OFS[I][1]);
DrawCenteredText(Canvas, R, FDef.Caption);
end;
Canvas.Font.Color := Ink(clWhite);
DrawCenteredText(Canvas, ABounds, FDef.Caption);
Exit;
end;
// Etichetta dentro al comando: come la lente, non cambia con lo stato. Prima
// diventava bianca e grassetto da acceso, ed era un secondo modo di dire la
// stessa cosa che ora dice la ghiera.
if (FDef.Kind in [ekButton, ekSwitch, ekLamp]) and (KeyShape = ksScreen) then
begin
// Il tasto a video invece si riempie da acceso: la scritta deve stare
// bene sul fondo che ha adesso, nera sul colore chiaro e chiara sul nero.
if ColorLuma(ScreenKeyFill) > 140 then
Canvas.Font.Color := Ink(clBlack)
else
Canvas.Font.Color := Ink(TColor($00F2F2F2));
Canvas.Font.Style := [fsBold];
end
else
begin
Canvas.Font.Color := Ink(clWindowText);
Canvas.Font.Style := [];
end;
DrawCenteredText(Canvas, ABounds, FDef.Caption);
end;
function TPlanciaElement.ScreenKeyFill: TColor;
begin
if FState then
Result := FDef.OnColor
else if FDef.OffColor <> clNone then
Result := FDef.OffColor
else
Result := CLR_SCREEN_KEY_FACE;
end;
function TPlanciaElement.KeyShape: TKeyShape;
begin
Result := FDef.Shape;
if Result <> ksAuto then
Exit;
// Senza indicazione: il pulsante momentaneo e' a pillola, l'interruttore
// squadrato. La forma dice il tipo anche senza leggere l'etichetta.
if FDef.Kind = ekButton then
Result := ksPill
else
Result := ksRect;
end;
procedure TPlanciaElement.PaintKey(const ABounds: TRect);
var
Radius, LedD, Pad, D, CX, CY, Ring, Glow: Integer;
Face: TColor;
Sh: TKeyShape;
begin
Sh := KeyShape;
// La lente non cambia con lo stato: acceso e spento si leggono dalla ghiera.
// Su un quadro vero il vetro e' sempre dello stesso colore, quello che cambia
// e' la luce dietro. Il colore della lente e' `colorOff`; `color` sui comandi
// a tasto non tocca piu' il vetro.
if FDef.OffColor <> clNone then
Face := FDef.OffColor
else if Sh = ksRound then
Face := CLR_KEY_FACE
else
Face := clBtnFace;
Face := Shade(Face);
Canvas.Brush.Style := bsSolid;
Canvas.Pen.Width := 1;
if Sh = ksScreen then
begin
// Tasto di un display multifunzione: spento e' un riquadro scuro con il
// filo chiaro, acceso si riempie del colore `color`. Qui lo stato si legge
// dal riempimento, come sugli schermi veri.
Canvas.Brush.Color := Shade(ScreenKeyFill);
if FState then
Canvas.Pen.Color := Shade(FDef.OnColor)
else
Canvas.Pen.Color := Shade(CLR_SCREEN_KEY_EDGE);
Canvas.Pen.Width := Max(1, Sc(2));
Radius := Sc(10);
Canvas.RoundRect(ABounds.Left + 1, ABounds.Top + 1, ABounds.Right - 1,
ABounds.Bottom - 1, Radius, Radius);
Canvas.Pen.Width := 1;
Exit;
end;
if Sh = ksRound then
begin
// Pulsante illuminato da quadro: ghiera metallica e lente tonda.
D := Min(ABounds.Width, ABounds.Height);
if D < 6 then
D := 6;
CX := (ABounds.Left + ABounds.Right) div 2;
CY := (ABounds.Top + ABounds.Bottom) div 2;
Ring := Round(D * 0.84) div 2;
if FState then
begin
// Acceso: l'orlo esterno porta il blu carico, poi una fascia piu' chiara
// stretta contro la lente. Basta il salto fra le due per dare l'idea del
// riverbero, senza gradienti che a queste dimensioni non si vedrebbero.
Canvas.Brush.Color := Shade(CLR_KNOB_RING_ON_EDGE);
Canvas.Pen.Color := Shade(CLR_KNOB_RING_ON_RIM);
Canvas.Ellipse(CX - D div 2, CY - D div 2, CX + D div 2, CY + D div 2);
Glow := Ring + Max(1, (D div 2 - Ring) div 2);
Canvas.Brush.Color := Shade(CLR_KNOB_RING_ON);
Canvas.Pen.Color := Shade(CLR_KNOB_RING_ON);
Canvas.Ellipse(CX - Glow, CY - Glow, CX + Glow, CY + Glow);
end
else
begin
Canvas.Brush.Color := Shade(CLR_KNOB_RING);
Canvas.Pen.Color := Shade(CLR_KNOB_EDGE);
Canvas.Ellipse(CX - D div 2, CY - D div 2, CX + D div 2, CY + D div 2);
end;
// Filo di separazione fra ghiera e lente: neutro, perche' ormai lo stato
// lo dice la ghiera e un contorno colorato tornerebbe a tingere la lente.
Canvas.Brush.Color := Face;
Canvas.Pen.Color := Shade(CLR_KNOB_EDGE);
Canvas.Ellipse(CX - Ring, CY - Ring, CX + Ring, CY + Ring);
Exit;
end;
// Squadrato e a pillola non hanno ghiera: lo stato sta tutto nel bordo, che
// percio' si ingrossa e prende lo stesso azzurro dei tondi. Con un filo da un
// pixel il comando premuto non si distinguerebbe da fermo.
Canvas.Brush.Color := Face;
if FState then
begin
Canvas.Pen.Color := Shade(CLR_KNOB_RING_ON_EDGE);
Canvas.Pen.Width := Max(2, Sc(2));
end
else
Canvas.Pen.Color := Shade(clGray);
if Sh = ksPill then
Radius := ABounds.Height
else
Radius := Sc(8);
Canvas.RoundRect(ABounds.Left, ABounds.Top, ABounds.Right, ABounds.Bottom,
Radius, Radius);
if (FDef.Kind = ekSwitch) and (FDef.CaptionPos = cpCenter) then
begin
// Spia di stato nell'angolo: l'interruttore resta premuto, serve
// un riscontro visivo anche a colpo d'occhio da lontano.
LedD := Sc(10);
Pad := Sc(6);
// Il bordo squadrato puo' aver lasciato la penna ingrossata.
Canvas.Pen.Width := 1;
if FState then
Canvas.Brush.Color := Shade(CLR_LAMP_ON)
else
Canvas.Brush.Color := Shade(CLR_LAMP_OFF);
Canvas.Pen.Color := Shade(clGray);
Canvas.Ellipse(ABounds.Right - LedD - Pad, ABounds.Top + Pad,
ABounds.Right - Pad, ABounds.Top + Pad + LedD);
end;
end;
procedure TPlanciaElement.PaintLamp(const ABounds: TRect);
var
D, CX, CY, Ring, Hi: Integer;
Lens: TColor;
begin
// Stesso aspetto di un tasto, ma la spia non si preme: e' la lente che si
// accende. Serve per gli allarmi che sul quadro vero sembrano pulsanti.
if FDef.Shape = ksScreen then
begin
PaintKey(ABounds);
Exit;
end;
if FDef.Shape = ksRound then
begin
D := Min(ABounds.Width, ABounds.Height);
if D < 6 then
D := 6;
CX := (ABounds.Left + ABounds.Right) div 2;
CY := (ABounds.Top + ABounds.Bottom) div 2;
Ring := Round(D * 0.84) div 2;
// Ghiera metallica fissa: in una spia lo stato e' tutto nella lente.
Canvas.Brush.Style := bsSolid;
Canvas.Pen.Width := 1;
Canvas.Brush.Color := Shade(CLR_KNOB_RING);
Canvas.Pen.Color := Shade(CLR_KNOB_EDGE);
Canvas.Ellipse(CX - D div 2, CY - D div 2, CX + D div 2, CY + D div 2);
// Spenta la lente resta del suo colore ma scura, come un vetro rosso senza
// luce dietro; accesa prende il colore pieno con un riflesso piu' chiaro.
if FState then
Lens := FDef.OnColor
else if FDef.OffColor <> clNone then
Lens := FDef.OffColor
else
Lens := BlendColor(FDef.OnColor, clBlack, 0.62);
Canvas.Brush.Color := Shade(Lens);
Canvas.Pen.Color := Shade(CLR_KNOB_EDGE);
Canvas.Ellipse(CX - Ring, CY - Ring, CX + Ring, CY + Ring);
if FState then
begin
Hi := Round(Ring * 0.55);
Lens := Shade(BlendColor(FDef.OnColor, clWhite, 0.4));
Canvas.Brush.Color := Lens;
Canvas.Pen.Color := Lens;
Canvas.Ellipse(CX - Hi, CY - Hi - Ring div 6, CX + Hi, CY + Hi - Ring div 6);
end;
Exit;
end;
// Un filo di pannello attorno al LED, ma proporzionato: su una spia da 22
// pixel un margine fisso da 6 si mangiava un quarto del diametro.
D := Min(ABounds.Width, ABounds.Height);
Dec(D, 2 * Min(Sc(3), D div 8));
if D < 6 then
D := 6;
CX := (ABounds.Left + ABounds.Right) div 2;
CY := (ABounds.Top + ABounds.Bottom) div 2;
// La spia invece il colore lo cambia eccome: e' tutto quello che sa fare, e
// segnala un ingresso, non un comando che si e' appena premuto.
Canvas.Brush.Style := bsSolid;
if FState then
Canvas.Brush.Color := Shade(FDef.OnColor)
else if FDef.OffColor <> clNone then
Canvas.Brush.Color := Shade(FDef.OffColor)
else
Canvas.Brush.Color := Shade(CLR_LAMP_OFF);
Canvas.Pen.Color := Shade(clGray);
Canvas.Pen.Width := 1;
Canvas.Ellipse(CX - D div 2, CY - D div 2, CX + D div 2, CY + D div 2);
end;
procedure TPlanciaElement.PaintMuteMark(const ABounds: TRect);
var
D, CX, CY, R: Integer;
begin
// Una sbarra sulla spia: guardando il quadro si deve capire che quel LED
// sta suonando a vuoto, altrimenti si crede che l'allarme sia rientrato.
if not AlarmMuted then
Exit;
D := Min(ABounds.Width, ABounds.Height);
if D < 8 then
Exit;
CX := (ABounds.Left + ABounds.Right) div 2;
CY := (ABounds.Top + ABounds.Bottom) div 2;
R := Round(D * 0.42);
Canvas.Pen.Width := Max(2, Round(D * 0.09));
Canvas.Pen.Color := Shade(clWhite);
Canvas.MoveTo(CX - R, CY + R);
Canvas.LineTo(CX + R, CY - R);
Canvas.Pen.Width := Max(1, Round(D * 0.045));
Canvas.Pen.Color := Shade(clBlack);
Canvas.MoveTo(CX - R, CY + R);
Canvas.LineTo(CX + R, CY - R);
Canvas.Pen.Width := 1;
end;
procedure TPlanciaElement.PaintGauge(const ABounds: TRect);
var
Info: TGaugeInfo;
begin
if FDef.GaugeStyle = gsDial then
begin
PaintDial(ABounds);
Exit;
end;
Info.Title := FDef.Caption;
Info.Units := FDef.Units;
Info.MinValue := FDef.EngMin;
Info.MaxValue := FDef.EngMax;
Info.Value := FValue;
Info.WarnBelow := FDef.WarnBelow;
Info.WarnAbove := FDef.WarnAbove;
Info.TickCount := 5;
Info.Valid := FValid;
Info.BackColor := Color;
Info.NightMode := FNight;
Info.Scale := FScale;
PaintTankGauge(Canvas, ABounds, Info);
end;
procedure TPlanciaElement.PaintDial(const ABounds: TRect);
const
// Scala su 270 gradi, aperta in basso. Gli angoli GDI+ girano in senso
// orario partendo da destra: 135 e' in basso a sinistra.
START_ANG = 135.0;
SWEEP = 270.0;
MAJOR_STEPS = 5;
MINOR_PER_MAJOR = 5;
var
G: TGPGraphics;
Pen: TGPPen;
Brush: TGPSolidBrush;
S, CX, CY, RingW, RFace, Band, RBand, RTick, TickLen, RLabel: Double;
Lo, Hi, A, W, NeedleLen: Double;
I, Steps, TW, TH, BoxW, BoxH: Integer;
Pts: array[0..3] of TGPPointF;
Box: TRect;
Txt: string;
function GP(AColor: TColor): Cardinal;
var
C: Longint;
begin
C := ColorToRGB(Shade(AColor));
Result := MakeColor(255, GetRValue(C), GetGValue(C), GetBValue(C));
end;
function AngleOf(const AValue: Double): Double;
begin
if Hi <= Lo then
Exit(START_ANG);
Result := START_ANG + SWEEP * EnsureRange((AValue - Lo) / (Hi - Lo), 0, 1);
end;
procedure BandArc(const AFrom, ATo: Double; AColor: TColor);
var
P: TGPPen;
A0, A1: Double;
begin
A0 := AngleOf(AFrom);
A1 := AngleOf(ATo);
if A1 - A0 < 0.5 then
Exit;
P := TGPPen.Create(GP(AColor), Band);
try
G.DrawArc(P, CX - RBand, CY - RBand, RBand * 2, RBand * 2, A0, A1 - A0);
finally
P.Free;
end;
end;
begin
// Il quadrante e' tondo: occupa il quadrato piu' grande, centrato in
// orizzontale e appoggiato in alto. Titolo e valore stanno nella bocca
// aperta in basso della scala, come sui display di plancia.
S := Min(ABounds.Width, ABounds.Height);
if S < 40 then
Exit;
CX := (ABounds.Left + ABounds.Right) / 2;
CY := ABounds.Top + S / 2;
RingW := Max(2, S * 0.02);
RFace := S / 2 - RingW / 2 - 1;
Band := Max(3, S * 0.055);
RBand := RFace - RingW / 2 - S * 0.03 - Band / 2;
RTick := RBand - Band / 2 - S * 0.01;
TickLen := S * 0.06;
RLabel := RTick - TickLen - S * 0.065;
Lo := FDef.EngMin;
Hi := FDef.EngMax;
G := TGPGraphics.Create(Canvas.Handle);
try
G.SetSmoothingMode(SmoothingModeAntiAlias);
Brush := TGPSolidBrush.Create(GP(CLR_DIAL_FACE));
try
G.FillEllipse(Brush, CX - RFace, CY - RFace, RFace * 2, RFace * 2);
finally
Brush.Free;
end;
Pen := TGPPen.Create(GP(CLR_DIAL_RING), RingW);
try
G.DrawEllipse(Pen, CX - RFace, CY - RFace, RFace * 2, RFace * 2);
finally
Pen.Free;
end;
// Fascia della scala. Verde e rosso solo se le soglie ci sono: senza
// soglie un quadrante tutto verde direbbe "tutto a posto" senza saperlo.
BandArc(Lo, Hi, CLR_DIAL_BAND);
if (FDef.WarnBelow > GAUGE_NO_WARN_LO) or (FDef.WarnAbove < GAUGE_NO_WARN_HI) then
begin
BandArc(Max(Lo, FDef.WarnBelow), Min(Hi, FDef.WarnAbove), CLR_DIAL_OK);
if FDef.WarnBelow > GAUGE_NO_WARN_LO then
BandArc(Lo, Min(Hi, FDef.WarnBelow), CLR_DIAL_ALARM);
if FDef.WarnAbove < GAUGE_NO_WARN_HI then
BandArc(Max(Lo, FDef.WarnAbove), Hi, CLR_DIAL_ALARM);
end;
// Tacche: lunghe e numerate ogni MAJOR_STEPS, corte in mezzo.
Steps := MAJOR_STEPS * MINOR_PER_MAJOR;
for I := 0 to Steps do
begin
A := DegToRad(START_ANG + SWEEP * I / Steps);
if I mod MINOR_PER_MAJOR = 0 then
begin
W := Max(1.5, S * 0.009);
NeedleLen := TickLen;
end
else
begin
W := Max(1, S * 0.004);
NeedleLen := TickLen * 0.5;
end;
Pen := TGPPen.Create(GP(CLR_DIAL_TICK), W);
try
G.DrawLine(Pen, CX + Cos(A) * RTick, CY + Sin(A) * RTick,
CX + Cos(A) * (RTick - NeedleLen), CY + Sin(A) * (RTick - NeedleLen));
finally
Pen.Free;
end;
end;
// Lancetta: solo con un dato valido. Senza lettura niente lancetta,
// altrimenti a fondo scala sembrerebbe un valore vero.
if FValid and (Hi > Lo) then
begin
A := DegToRad(AngleOf(FValue));
W := Max(2, S * 0.022);
NeedleLen := RTick - S * 0.01;
Pts[0] := MakePoint(CX + Cos(A) * NeedleLen, CY + Sin(A) * NeedleLen);
Pts[1] := MakePoint(CX + Cos(A + Pi / 2) * W, CY + Sin(A + Pi / 2) * W);
Pts[2] := MakePoint(CX - Cos(A) * S * 0.07, CY - Sin(A) * S * 0.07);
Pts[3] := MakePoint(CX + Cos(A - Pi / 2) * W, CY + Sin(A - Pi / 2) * W);
Brush := TGPSolidBrush.Create(GP(CLR_DIAL_NEEDLE));
try
G.FillPolygon(Brush, PGPPointF(@Pts[0]), Length(Pts));
finally
Brush.Free;
end;
end;
Brush := TGPSolidBrush.Create(GP(CLR_DIAL_RING));
try
W := Max(3, S * 0.04);
G.FillEllipse(Brush, CX - W, CY - W, W * 2, W * 2);
finally
Brush.Free;
end;
finally
G.Free;
end;
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := PanelInk;
Canvas.Font.Style := [];
// Titolo: sotto il quadrante se l'elemento e' piu' alto che largo, dove
// non incontra niente; altrimenti dentro, sotto il perno, dove un titolo
// lungo sfiora i numeri di inizio e fondo scala.
if FDef.Caption <> '' then
begin
TW := Canvas.TextWidth(FDef.Caption);
TH := Canvas.TextHeight(FDef.Caption);
if ABounds.Height - S >= TH then
Canvas.TextOut(Round(CX - TW / 2), ABounds.Top + Round(S) +
(ABounds.Height - Round(S) - TH) div 2, FDef.Caption)
else
Canvas.TextOut(Round(CX - TW / 2), Round(CY + S * 0.08), FDef.Caption);
end;
// Numeri della scala, con un font proporzionato al quadrante.
Canvas.Font.Height := -Max(8, Round(S * 0.058));
for I := 0 to MAJOR_STEPS do
begin
A := DegToRad(START_ANG + SWEEP * I / MAJOR_STEPS);
Txt := FormatFloat('0.#', Lo + (Hi - Lo) * I / MAJOR_STEPS);
TW := Canvas.TextWidth(Txt);
TH := Canvas.TextHeight(Txt);
Canvas.TextOut(Round(CX + Cos(A) * RLabel - TW / 2),
Round(CY + Sin(A) * RLabel - TH / 2), Txt);
end;
// Valore in cifre dentro un riquadro, come la lettura digitale dei display.
BoxW := Round(S * 0.44);
BoxH := Round(S * 0.14);
Box := Rect(Round(CX) - BoxW div 2, Round(CY + S * 0.25),
Round(CX) + BoxW div 2, Round(CY + S * 0.25) + BoxH);
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := Shade(clBlack);
Canvas.Pen.Color := Shade(CLR_DIAL_RING);
Canvas.Pen.Width := 1;
Canvas.Rectangle(Box);
Canvas.Brush.Style := bsClear;
if FValid then
begin
if FDef.Decimals > 0 then
Txt := FormatFloat('0.' + StringOfChar('0', FDef.Decimals), FValue)
else
Txt := FormatFloat('0', FValue);
if FDef.Units <> '' then
Txt := Txt + ' ' + FDef.Units;
end
else
Txt := '---';
Canvas.Font.Height := -Max(8, Round(BoxH * 0.7));
Canvas.Font.Style := [fsBold];
if FNight then
Canvas.Font.Color := CLR_NIGHT_INK
else
Canvas.Font.Color := CLR_DIAL_VALUE;
Winapi.Windows.DrawText(Canvas.Handle, PChar(Txt), Length(Txt), Box,
DT_CENTER or DT_VCENTER or DT_SINGLELINE);
end;
function TPlanciaElement.DisplayText: string;
var
Digits: Integer;
begin
Digits := Max(1, FDef.Digits);
if not FValid then
Exit(StringOfChar(' ', Digits));
Result := FormatFloat('0.' + StringOfChar('0', Max(0, FDef.Decimals)),
FValue);
Result := StringReplace(Result, ',', '.', [rfReplaceAll]);
// Allinea a destra sul numero di cifre richiesto, come un display vero.
while Length(StringReplace(Result, '.', '', [rfReplaceAll])) < Digits do
Result := '0' + Result;
end;
procedure TPlanciaElement.PaintDisplay(const ABounds: TRect);
var
LabelH: Integer;
Box, Digits: TRect;
begin
// Nessun riempimento: la fascia dell'etichetta deve lasciar vedere lo
// sfondo del pannello. Il riquadro nero del display e' gia' opaco di suo.
LabelH := 0;
if FDef.Caption <> '' then
LabelH := Canvas.TextHeight('Wg') + Sc(4);
Box := ABounds;
Box.Top := ABounds.Top + LabelH;
if Box.Height < 8 then
Exit;
if LabelH > 0 then
begin
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := PanelInk;
Canvas.Font.Style := [];
DrawCenteredText(Canvas, Rect(ABounds.Left, ABounds.Top, ABounds.Right,
ABounds.Top + LabelH), FDef.Caption);
end;
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := Shade(CLR_DISPLAY_BG);
Canvas.Pen.Color := clBlack;
Canvas.Pen.Width := 1;
Canvas.Rectangle(Box);
Digits := Box;
InflateRect(Digits, -Sc(10), -Sc(10));
if (Digits.Width > 10) and (Digits.Height > 10) then
DrawSevenSegment(Canvas, Digits, DisplayText, Shade(CLR_SEG_ON), Shade(CLR_SEG_OFF));
end;
procedure TPlanciaElement.PaintRotary(const ABounds: TRect);
var
LegendH, CapH, D, CX, CY, R, LeverW: Integer;
Knob: TRect;
Ang: Double;
EndX, EndY: Integer;
begin
// Solo ghiera e manopola sono opache: legenda ed etichetta stanno
// direttamente sullo sfondo del pannello.
// Legende ed etichette lunghe ("SERV.BATT. < 0 > START BATT.") vanno a capo
// invece di essere tagliate, ma senza mangiarsi la manopola.
LegendH := 0;
if FDef.Legend <> '' then
LegendH := Min(TextBlockHeight(FDef.Legend, ABounds.Width),
ABounds.Height div 3) + Sc(3);
CapH := 0;
if FDef.Caption <> '' then
CapH := Min(TextBlockHeight(FDef.Caption, ABounds.Width),
ABounds.Height div 3) + Sc(3);
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := PanelInk;
Canvas.Font.Style := [];
if LegendH > 0 then
DrawCenteredText(Canvas, Rect(ABounds.Left, ABounds.Top, ABounds.Right,
ABounds.Top + LegendH), FDef.Legend);
if CapH > 0 then
DrawCenteredText(Canvas, Rect(ABounds.Left, ABounds.Bottom - CapH,
ABounds.Right, ABounds.Bottom), FDef.Caption);
Knob := Rect(ABounds.Left, ABounds.Top + LegendH, ABounds.Right,
ABounds.Bottom - CapH);
D := Min(Knob.Width, Knob.Height);
if D < 12 then
Exit;
CX := (Knob.Left + Knob.Right) div 2;
CY := (Knob.Top + Knob.Bottom) div 2;
R := D div 2;
// Ghiera
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := Shade(CLR_KNOB_RING);
Canvas.Pen.Color := Shade(CLR_KNOB_EDGE);
Canvas.Ellipse(CX - R, CY - R, CX + R, CY + R);
// Manopola
R := Round(R * 0.78);
Canvas.Brush.Color := Shade(CLR_KNOB_BODY);
Canvas.Pen.Color := clBlack;
Canvas.Ellipse(CX - R, CY - R, CX + R, CY + R);
// Leva: a sinistra, in alto o a destra secondo la posizione.
// La leva segue la posizione mostrata, che durante l.animazione sta fra
// quella di partenza e quella di arrivo.
Ang := DegToRad(LeverAngle);
EndX := CX + Round(Cos(Ang) * R * 0.92);
EndY := CY + Round(Sin(Ang) * R * 0.92);
LeverW := Max(3, Round(R * 0.28));
Canvas.Pen.Color := Shade(CLR_KNOB_LEVER);
Canvas.Pen.Width := LeverW;
Canvas.MoveTo(CX, CY);
Canvas.LineTo(EndX, EndY);
Canvas.Pen.Width := 1;
Canvas.Brush.Color := Shade(CLR_KNOB_LEVER);
Canvas.Pen.Color := Shade(CLR_KNOB_LEVER);
Canvas.Ellipse(EndX - LeverW div 2, EndY - LeverW div 2,
EndX + LeverW div 2, EndY + LeverW div 2);
end;
/// Cornice disegnata con GDI+ invece che con la GDI: serve l'antialiasing,
/// altrimenti un ovale grande viene scalettato e il marchio sembra sporco.
procedure DrawFrameSmooth(ACanvas: TCanvas; const ABounds: TRect;
AKind: TFrameKind; AWidth: Single; AColor: TColor);
var
G: TGPGraphics;
Pen: TGPPen;
Path: TGPGraphicsPath;
RGB: Longint;
X, Y, W, H, R, D: Single;
begin
if (AKind = fkNone) or (ABounds.Width <= 2) or (ABounds.Height <= 2) then
Exit;
if AWidth < 1 then
AWidth := 1;
// Il tratto e' centrato sul contorno: si rientra di meta' spessore per non
// farlo uscire dai bordi dell'elemento.
X := ABounds.Left + AWidth / 2;
Y := ABounds.Top + AWidth / 2;
W := ABounds.Width - AWidth;
H := ABounds.Height - AWidth;
if (W <= 0) or (H <= 0) then
Exit;
RGB := ColorToRGB(AColor);
G := TGPGraphics.Create(ACanvas.Handle);
try
G.SetSmoothingMode(SmoothingModeAntiAlias);
Pen := TGPPen.Create(MakeColor(255, GetRValue(RGB), GetGValue(RGB),
GetBValue(RGB)), AWidth);
try
case AKind of
fkOval:
G.DrawEllipse(Pen, X, Y, W, H);
fkRect:
G.DrawRectangle(Pen, X, Y, W, H);
fkRound:
begin
R := Min(W, H) / 2;
D := R * 2;
Path := TGPGraphicsPath.Create;
try
Path.AddArc(X, Y, D, D, 180, 90);
Path.AddArc(X + W - D, Y, D, D, 270, 90);
Path.AddArc(X + W - D, Y + H - D, D, D, 0, 90);
Path.AddArc(X, Y + H - D, D, D, 90, 90);
Path.CloseFigure;
G.DrawPath(Pen, Path);
finally
Path.Free;
end;
end;
end;
finally
Pen.Free;
end;
finally
G.Free;
end;
end;
procedure TPlanciaElement.PaintLabel(const ABounds: TRect);
var
InkNow: TColor;
begin
// Marchio serigrafato: di notte inchiostro e cornice passano all'ambra,
// altrimenti una scritta nera sparirebbe contro il pannello scuro. Senza un
// colore proprio la scritta prende l'inchiostro del pannello.
if FDef.OnColor = TElementDef.DefaultOnColor(ekLabel) then
InkNow := PanelInk
else
InkNow := Ink(FDef.OnColor);
DrawFrameSmooth(Canvas, ABounds, FDef.Frame, Sc(FDef.FrameWidth), InkNow);
if FDef.Caption = '' then
Exit;
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := InkNow;
Canvas.Font.Style := [];
DrawCenteredText(Canvas, ABounds, FDef.Caption);
end;
procedure TPlanciaElement.PaintEditOverlay(const ABounds: TRect);
var
K: TGrabKind;
begin
Canvas.Brush.Style := bsClear;
Canvas.Pen.Style := psDot;
Canvas.Pen.Width := 1;
if FSelected then
Canvas.Pen.Color := clNavy
else
Canvas.Pen.Color := clSilver;
Canvas.Rectangle(ABounds);
Canvas.Pen.Style := psSolid;
if not FSelected then
Exit;
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clNavy;
Canvas.Pen.Color := clWhite;
for K := gkLeft to gkBottomRight do
Canvas.Rectangle(HandleRect(K));
end;
procedure TPlanciaElement.SplitBounds(const ABounds: TRect;
out AGraphic, ACaption: TRect);
var
CapH: Integer;
Calc: TRect;
begin
AGraphic := ABounds;
ACaption := ABounds;
if FDef.CaptionPos = cpCenter then
begin
InflateRect(ACaption, -Sc(6), -Sc(3));
Exit;
end;
// Senza etichetta non si riserva niente: il disegno, o l'immagine, prende
// tutto l'elemento. Serve alle spie appoggiate su una grafica, che sono
// piccole e con una fascia vuota sotto resterebbero la meta'.
if FDef.Caption = '' then
begin
ACaption.Bottom := ACaption.Top;
Exit;
end;
// Misura il testo davvero: "NAVIGATION LTS" su un pulsante stretto va a
// capo, e con una fascia da una riga sola resterebbe tagliato.
Calc := Rect(0, 0, ABounds.Width, 0);
Winapi.Windows.DrawText(Canvas.Handle, PChar(FDef.Caption),
Length(FDef.Caption), Calc, DT_CENTER or DT_WORDBREAK or DT_CALCRECT);
CapH := Calc.Height + Sc(3);
if CapH > ABounds.Height div 2 then
CapH := ABounds.Height div 2;
if FDef.CaptionPos = cpBelow then
begin
ACaption.Top := ABounds.Bottom - CapH;
AGraphic.Bottom := ABounds.Bottom - CapH;
end
else
begin
ACaption.Bottom := ABounds.Top + CapH;
AGraphic.Top := ABounds.Top + CapH;
end;
end;
procedure TPlanciaElement.Paint;
var
R, GfxR, CapR: TRect;
Pic: TPicture;
begin
R := ClientRect;
Canvas.Font := Font;
if FDef.FontName <> '' then
Canvas.Font.Name := FDef.FontName;
if FDef.FontSize > 0 then
Canvas.Font.Height := -Sc(FDef.FontSize)
else if not SameValue(FScale, 1) then
Canvas.Font.Height := Round(Font.Height * FScale);
// La spaziatura e' una proprieta' del DC, non del font: va rimessa a zero
// in fondo, perche' il canvas e' quello del pannello ed e' condiviso con
// tutti gli altri elementi.
if FDef.Spacing <> 0 then
SetTextCharacterExtra(Canvas.Handle, Sc(FDef.Spacing));
try
SplitBounds(R, GfxR, CapR);
Pic := CurrentPicture;
if FDef.Kind = ekImage then
begin
if Pic <> nil then
PaintPicture(R, Pic)
else if FEditMode then
PaintMissingPicture(R);
end
else if (Pic <> nil) and ElementUsesImages(FDef.Kind) then
begin
PaintPicture(GfxR, Pic);
PaintCaption(CapR, FDef.CaptionPos = cpCenter);
end
else
case FDef.Kind of
ekButton, ekSwitch:
begin
PaintKey(GfxR);
PaintCaption(CapR, False);
end;
ekLamp:
begin
PaintLamp(GfxR);
PaintMuteMark(GfxR);
PaintCaption(CapR, False);
end;
ekGauge: PaintGauge(R);
ekDisplay: PaintDisplay(R);
ekRotary: PaintRotary(R);
ekLabel: PaintLabel(R);
end;
if FEditMode then
PaintEditOverlay(R);
finally
if FDef.Spacing <> 0 then
SetTextCharacterExtra(Canvas.Handle, 0);
end;
end;
initialization
finalization
// Il timer delle animazioni non deve sopravvivere alla chiusura.
FreeAndNil(Animator);
end.