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; 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.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.