Chapter 7: WebSockets#
Real-Time Without Polling#
Your Delphi application displays a dashboard with live metrics. The user stares at the numbers. Nothing moves. They click refresh. The numbers update. They stare again. They click refresh again.
This is HTTP. You ask, the server answers, the connection closes. If you want fresh data, you ask again. Polling every 5 seconds works, but it is wasteful -- 90% of those requests return the same data. It hammers the server. It wastes bandwidth. And the user still sees data that is up to 5 seconds stale.
WebSocket is the fix. One connection opens. It stays open. The server pushes data the instant it changes. The client pushes messages back. No request/response cycle. No polling interval. No stale data.
TTina4WebSocketClient brings WebSocket to your Delphi FMX applications with RFC 6455 compliance, automatic reconnection, ping/pong keepalive, and event-driven message handling. Drop it on a form, set a URL, connect, and start receiving real-time data.
1. TTina4WebSocketClient Overview#
The WebSocket client handles the full RFC 6455 protocol:
- Full-duplex communication -- send and receive simultaneously over a single TCP connection
- Auto-reconnect -- configurable reconnection with backoff when the connection drops
- Ping/pong keepalive -- automatic heartbeat to detect dead connections
- Text and binary messages -- handle both message types
- Event-driven -- OnConnected, OnMessage, OnDisconnected, OnError callbacks
Component Setup#
Drop a TTina4WebSocketClient on your form from the Tina4 palette, or create it at runtime:
uses Tina4WebSocketClient;โvar WSClient: TTina4WebSocketClient;begin WSClient := TTina4WebSocketClient.Create(Self); WSClient.URL := 'wss://api.example.com/ws';end;2. Basic Connection#
Design-Time Configuration#
Set properties in the Object Inspector:
| Property | Value |
|---|---|
URL | wss://api.example.com/ws |
AutoReconnect | True |
ReconnectInterval | 5000 (ms) |
PingInterval | 30000 (ms) |
Runtime Connection#
procedure TForm1.FormCreate(Sender: TObject);begin Tina4WebSocket1.URL := 'wss://api.example.com/ws'; Tina4WebSocket1.AutoReconnect := True; Tina4WebSocket1.ReconnectInterval := 5000;โ Tina4WebSocket1.OnConnected := WebSocketConnected; Tina4WebSocket1.OnMessage := WebSocketMessage; Tina4WebSocket1.OnDisconnected := WebSocketDisconnected; Tina4WebSocket1.OnError := WebSocketError;โ Tina4WebSocket1.Connect;end;Event Handlers#
procedure TForm1.WebSocketConnected(Sender: TObject);begin TThread.Synchronize(nil, procedure begin LabelStatus.Text := 'Connected'; LabelStatus.TextSettings.FontColor := TAlphaColorRec.Green; end);end;โprocedure TForm1.WebSocketMessage(Sender: TObject; const AMessage: string);begin TThread.Synchronize(nil, procedure begin MemoMessages.Lines.Add(FormatDateTime('hh:nn:ss', Now) + ' ' + AMessage); end);end;โprocedure TForm1.WebSocketDisconnected(Sender: TObject; const ACode: Integer; const AReason: string);begin TThread.Synchronize(nil, procedure begin LabelStatus.Text := 'Disconnected: ' + AReason; LabelStatus.TextSettings.FontColor := TAlphaColorRec.Red; end);end;โprocedure TForm1.WebSocketError(Sender: TObject; const AError: string);begin TThread.Synchronize(nil, procedure begin MemoMessages.Lines.Add('ERROR: ' + AError); end);end;3. Sending Messages#
Send Text#
procedure TForm1.ButtonSendClick(Sender: TObject);begin Tina4WebSocket1.Send(EditMessage.Text); EditMessage.Text := '';end;Send JSON#
procedure TForm1.SendJSON;var Obj: TJSONObject;begin Obj := TJSONObject.Create; try Obj.AddPair('type', 'subscribe'); Obj.AddPair('channel', 'notifications'); Obj.AddPair('userId', TJSONNumber.Create(1001));โ Tina4WebSocket1.Send(Obj.ToString); finally Obj.Free; end;end;Check Connection Before Sending#
procedure TForm1.SafeSend(const AMessage: string);begin if Tina4WebSocket1.IsConnected then Tina4WebSocket1.Send(AMessage) else ShowMessage('Not connected to server');end;4. Auto-Reconnect Behavior#
When the connection drops, TTina4WebSocketClient automatically attempts to reconnect if AutoReconnect is True.
Configuration#
Tina4WebSocket1.AutoReconnect := True;Tina4WebSocket1.ReconnectInterval := 5000; // 5 seconds between attemptsReconnect Flow#
Connected -> Connection Lost -> Wait 5s -> Reconnect Attempt 1 -> Fail -> Wait 5s -> Reconnect Attempt 2 -> Success -> ConnectedHandling Reconnection in Code#
procedure TForm1.WebSocketConnected(Sender: TObject);begin TThread.Synchronize(nil, procedure begin LabelStatus.Text := 'Connected';โ // Re-subscribe to channels after reconnection var Sub := TJSONObject.Create; try Sub.AddPair('type', 'subscribe'); Sub.AddPair('channel', 'updates'); Tina4WebSocket1.Send(Sub.ToString); finally Sub.Free; end; end);end;The OnConnected event fires every time the connection opens -- including after a reconnect. Use it to re-subscribe to channels or re-authenticate.
5. Ping/Pong Keepalive#
WebSocket connections can silently die -- a router drops the connection, a firewall times it out, the server crashes. Without keepalive, your client thinks it is still connected while the connection is actually dead.
TTina4WebSocketClient sends periodic ping frames. If the server does not respond with a pong within a timeout, the connection is considered dead and reconnection begins.
Tina4WebSocket1.PingInterval := 30000; // Send ping every 30 secondsYou do not need to handle pings manually. The component manages the ping/pong protocol internally.
6. Binary vs Text Messages#
WebSocket supports two frame types: text and binary. TTina4WebSocketClient handles both.
Text Messages#
Most WebSocket APIs use text frames with JSON payloads:
procedure TForm1.WebSocketMessage(Sender: TObject; const AMessage: string);var JSON: TJSONObject;begin JSON := StrToJSONObject(AMessage); if Assigned(JSON) then try var MsgType := JSON.GetValue<String>('type', '');โ TThread.Synchronize(nil, procedure begin if MsgType = 'price_update' then UpdatePriceDisplay(JSON) else if MsgType = 'notification' then ShowNotification(JSON) else if MsgType = 'error' then HandleServerError(JSON); end); finally JSON.Free; end;end;Binary Messages#
For binary data (images, files, protocol buffers), use the binary message event:
procedure TForm1.WebSocketBinaryMessage(Sender: TObject; const AData: TBytes);begin TThread.Synchronize(nil, procedure begin // Save binary data to file var Stream := TFileStream.Create('received_data.bin', fmCreate); try Stream.WriteBuffer(AData[0], Length(AData)); finally Stream.Free; end; end);end;7. Disconnecting#
Graceful Close#
procedure TForm1.ButtonDisconnectClick(Sender: TObject);begin Tina4WebSocket1.AutoReconnect := False; // Prevent reconnection Tina4WebSocket1.Disconnect;end;Cleanup on Form Destroy#
procedure TForm1.FormDestroy(Sender: TObject);begin Tina4WebSocket1.AutoReconnect := False; if Tina4WebSocket1.IsConnected then Tina4WebSocket1.Disconnect;end;8. Complete Example: Real-Time Chat Client#
Build a chat application that connects to a WebSocket server, sends and receives messages, and displays them in an HTML renderer with online status.
Form Layout#
Place these components on your form:
TTina4WebSocketClient(Tina4WebSocket1)TTina4HTMLRender(HTMLRender1) -- displays chat messagesTEdit(EditMessage) -- message inputTButton(ButtonSend) -- send buttonTEdit(EditUsername) -- username inputTButton(ButtonConnect) -- connect/disconnectTLabel(LabelStatus) -- connection status
Implementation#
unit ChatForm;โinterfaceโuses System.SysUtils, System.Types, System.Classes, System.JSON, FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, FMX.Edit, FMX.Memo, FMX.Layouts, Tina4WebSocketClient, Tina4HTMLRender, Tina4Core;โtype TFormChat = class(TForm) Tina4WebSocket1: TTina4WebSocketClient; HTMLRender1: TTina4HTMLRender; EditMessage: TEdit; ButtonSend: TButton; EditUsername: TEdit; ButtonConnect: TButton; LabelStatus: TLabel; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure ButtonConnectClick(Sender: TObject); procedure ButtonSendClick(Sender: TObject); procedure EditMessageKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState); private FChatHistory: TStringList; FOnlineUsers: TStringList; FIsConnected: Boolean; procedure OnWSOpen(Sender: TObject); procedure OnWSMessage(Sender: TObject; const AMessage: string); procedure OnWSClose(Sender: TObject; const ACode: Integer; const AReason: string); procedure OnWSError(Sender: TObject; const AError: string); procedure AddChatMessage(const AUser, AMessage, ATime: string; AIsOwn: Boolean); procedure RefreshChatDisplay; procedure UpdateOnlineStatus; end;โvar FormChat: TFormChat;โimplementationโ{$R *.fmx}โprocedure TFormChat.FormCreate(Sender: TObject);begin FChatHistory := TStringList.Create; FOnlineUsers := TStringList.Create; FIsConnected := False;โ Tina4WebSocket1.AutoReconnect := True; Tina4WebSocket1.ReconnectInterval := 3000; Tina4WebSocket1.PingInterval := 25000;โ Tina4WebSocket1.OnConnected := OnWSOpen; Tina4WebSocket1.OnMessage := OnWSMessage; Tina4WebSocket1.OnDisconnected := OnWSClose; Tina4WebSocket1.OnError := OnWSError;โ ButtonSend.Enabled := False; LabelStatus.Text := 'Disconnected';โ // Show empty chat RefreshChatDisplay;end;โprocedure TFormChat.FormDestroy(Sender: TObject);begin Tina4WebSocket1.AutoReconnect := False; if Tina4WebSocket1.IsConnected then Tina4WebSocket1.Disconnect; FChatHistory.Free; FOnlineUsers.Free;end;โprocedure TFormChat.ButtonConnectClick(Sender: TObject);begin if FIsConnected then begin Tina4WebSocket1.AutoReconnect := False; Tina4WebSocket1.Disconnect; end else begin if EditUsername.Text.Trim.IsEmpty then begin ShowMessage('Please enter a username'); Exit; end;โ Tina4WebSocket1.URL := 'wss://chat.example.com/ws'; Tina4WebSocket1.AutoReconnect := True; Tina4WebSocket1.Connect; LabelStatus.Text := 'Connecting...'; end;end;โprocedure TFormChat.OnWSOpen(Sender: TObject);begin TThread.Synchronize(nil, procedure begin FIsConnected := True; LabelStatus.Text := 'Connected'; ButtonConnect.Text := 'Disconnect'; ButtonSend.Enabled := True; EditUsername.Enabled := False;โ // Send join message var JoinMsg := TJSONObject.Create; try JoinMsg.AddPair('type', 'join'); JoinMsg.AddPair('username', EditUsername.Text); Tina4WebSocket1.Send(JoinMsg.ToString); finally JoinMsg.Free; end; end);end;โprocedure TFormChat.OnWSMessage(Sender: TObject; const AMessage: string);begin TThread.Synchronize(nil, procedure var JSON: TJSONObject; begin JSON := StrToJSONObject(AMessage); if not Assigned(JSON) then Exit; try var MsgType := JSON.GetValue<String>('type', '');โ if MsgType = 'chat' then begin var User := JSON.GetValue<String>('username', 'Unknown'); var Text := JSON.GetValue<String>('message', ''); var Time := JSON.GetValue<String>('time', FormatDateTime('hh:nn', Now)); var IsOwn := (User = EditUsername.Text);โ AddChatMessage(User, Text, Time, IsOwn); end else if MsgType = 'user_list' then begin FOnlineUsers.Clear; var Users := JSON.GetValue<TJSONArray>('users'); if Assigned(Users) then for var I := 0 to Users.Count - 1 do FOnlineUsers.Add(Users.Items[I].Value); UpdateOnlineStatus; end else if MsgType = 'user_joined' then begin var User := JSON.GetValue<String>('username', ''); if not FOnlineUsers.Contains(User) then FOnlineUsers.Add(User); AddChatMessage('System', User + ' joined the chat', '', False); UpdateOnlineStatus; end else if MsgType = 'user_left' then begin var User := JSON.GetValue<String>('username', ''); var Idx := FOnlineUsers.IndexOf(User); if Idx >= 0 then FOnlineUsers.Delete(Idx); AddChatMessage('System', User + ' left the chat', '', False); UpdateOnlineStatus; end; finally JSON.Free; end; end);end;โprocedure TFormChat.OnWSClose(Sender: TObject; const ACode: Integer; const AReason: string);begin TThread.Synchronize(nil, procedure begin FIsConnected := False; LabelStatus.Text := 'Disconnected'; ButtonConnect.Text := 'Connect'; ButtonSend.Enabled := False; EditUsername.Enabled := True; FOnlineUsers.Clear; UpdateOnlineStatus; end);end;โprocedure TFormChat.OnWSError(Sender: TObject; const AError: string);begin TThread.Synchronize(nil, procedure begin AddChatMessage('System', 'Error: ' + AError, '', False); end);end;โprocedure TFormChat.ButtonSendClick(Sender: TObject);begin if EditMessage.Text.Trim.IsEmpty then Exit; if not Tina4WebSocket1.IsConnected then Exit;โ var Msg := TJSONObject.Create; try Msg.AddPair('type', 'chat'); Msg.AddPair('message', EditMessage.Text.Trim); Tina4WebSocket1.Send(Msg.ToString); finally Msg.Free; end;โ EditMessage.Text := ''; EditMessage.SetFocus;end;โprocedure TFormChat.EditMessageKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState);begin if Key = vkReturn then ButtonSendClick(Sender);end;โprocedure TFormChat.AddChatMessage(const AUser, AMessage, ATime: string; AIsOwn: Boolean);var Alignment, BgColor, TextColor: string;begin if AIsOwn then begin Alignment := 'right'; BgColor := '#1abc9c'; TextColor := 'white'; end else if AUser = 'System' then begin Alignment := 'center'; BgColor := '#f0f0f0'; TextColor := '#666'; end else begin Alignment := 'left'; BgColor := '#ecf0f1'; TextColor := '#333'; end;โ FChatHistory.Add(Format( '<div style="text-align: %s; margin: 5px 0;">' + ' <div style="display: inline-block; background: %s; color: %s; ' + ' padding: 8px 12px; border-radius: 12px; max-width: 70%%;">' + ' <small><strong>%s</strong> %s</small><br>%s' + ' </div>' + '</div>', [Alignment, BgColor, TextColor, AUser, ATime, AMessage]));โ RefreshChatDisplay;end;โprocedure TFormChat.RefreshChatDisplay;begin HTMLRender1.HTML.Text := '<div style="font-family: Arial, sans-serif; padding: 10px;">' + '<h3 style="color: #2c3e50; border-bottom: 1px solid #eee; padding-bottom: 5px;">Chat</h3>' + FChatHistory.Text + '</div>';end;โprocedure TFormChat.UpdateOnlineStatus;begin // Update status label with online user count if FOnlineUsers.Count > 0 then LabelStatus.Text := Format('Connected (%d online)', [FOnlineUsers.Count]) else if FIsConnected then LabelStatus.Text := 'Connected' else LabelStatus.Text := 'Disconnected';end;โend.9. Complete Example: Live Data Feed#
Build a real-time price feed that connects to a WebSocket server, receives price updates, and displays them with color-coded changes.
Implementation#
unit PriceFeedForm;โinterfaceโuses System.SysUtils, System.Types, System.Classes, System.JSON, System.Generics.Collections, FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, FMX.Layouts, Tina4WebSocketClient, Tina4HTMLRender, Tina4Core;โtype TPriceInfo = record Symbol: string; Price: Double; PrevPrice: Double; Change: Double; ChangePercent: Double; LastUpdate: TDateTime; end;โ TFormPriceFeed = class(TForm) Tina4WebSocket1: TTina4WebSocketClient; HTMLRender1: TTina4HTMLRender; ButtonConnect: TButton; LabelStatus: TLabel; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure ButtonConnectClick(Sender: TObject); private FPrices: TDictionary<string, TPriceInfo>; procedure OnWSOpen(Sender: TObject); procedure OnWSMessage(Sender: TObject; const AMessage: string); procedure OnWSClose(Sender: TObject; const ACode: Integer; const AReason: string); procedure OnWSError(Sender: TObject; const AError: string); procedure UpdatePriceDisplay; function GetChangeColor(AChange: Double): string; function GetChangeArrow(AChange: Double): string; end;โvar FormPriceFeed: TFormPriceFeed;โimplementationโ{$R *.fmx}โprocedure TFormPriceFeed.FormCreate(Sender: TObject);begin FPrices := TDictionary<string, TPriceInfo>.Create;โ Tina4WebSocket1.URL := 'wss://feeds.example.com/prices'; Tina4WebSocket1.AutoReconnect := True; Tina4WebSocket1.ReconnectInterval := 5000; Tina4WebSocket1.PingInterval := 20000;โ Tina4WebSocket1.OnConnected := OnWSOpen; Tina4WebSocket1.OnMessage := OnWSMessage; Tina4WebSocket1.OnDisconnected := OnWSClose; Tina4WebSocket1.OnError := OnWSError;โ LabelStatus.Text := 'Disconnected'; UpdatePriceDisplay;end;โprocedure TFormPriceFeed.FormDestroy(Sender: TObject);begin Tina4WebSocket1.AutoReconnect := False; if Tina4WebSocket1.IsConnected then Tina4WebSocket1.Disconnect; FPrices.Free;end;โprocedure TFormPriceFeed.ButtonConnectClick(Sender: TObject);begin if Tina4WebSocket1.IsConnected then begin Tina4WebSocket1.AutoReconnect := False; Tina4WebSocket1.Disconnect; ButtonConnect.Text := 'Connect'; end else begin Tina4WebSocket1.AutoReconnect := True; Tina4WebSocket1.Connect; LabelStatus.Text := 'Connecting...'; ButtonConnect.Text := 'Disconnect'; end;end;โprocedure TFormPriceFeed.OnWSOpen(Sender: TObject);begin TThread.Synchronize(nil, procedure begin LabelStatus.Text := 'Connected - Receiving prices';โ // Subscribe to price updates var Sub := TJSONObject.Create; try Sub.AddPair('type', 'subscribe'); var Symbols := TJSONArray.Create; Symbols.Add('BTC-USD'); Symbols.Add('ETH-USD'); Symbols.Add('AAPL'); Symbols.Add('GOOGL'); Symbols.Add('MSFT'); Sub.AddPair('symbols', Symbols); Tina4WebSocket1.Send(Sub.ToString); finally Sub.Free; end; end);end;โprocedure TFormPriceFeed.OnWSMessage(Sender: TObject; const AMessage: string);begin TThread.Synchronize(nil, procedure var JSON: TJSONObject; Info: TPriceInfo; begin JSON := StrToJSONObject(AMessage); if not Assigned(JSON) then Exit; try var MsgType := JSON.GetValue<String>('type', '');โ if MsgType = 'price' then begin var Symbol := JSON.GetValue<String>('symbol', ''); var NewPrice := JSON.GetValue<Double>('price', 0);โ if FPrices.TryGetValue(Symbol, Info) then begin Info.PrevPrice := Info.Price; Info.Price := NewPrice; Info.Change := NewPrice - Info.PrevPrice; if Info.PrevPrice > 0 then Info.ChangePercent := (Info.Change / Info.PrevPrice) * 100 else Info.ChangePercent := 0; end else begin Info.Symbol := Symbol; Info.Price := NewPrice; Info.PrevPrice := NewPrice; Info.Change := 0; Info.ChangePercent := 0; end;โ Info.LastUpdate := Now; FPrices.AddOrSetValue(Symbol, Info); UpdatePriceDisplay; end; finally JSON.Free; end; end);end;โprocedure TFormPriceFeed.OnWSClose(Sender: TObject; const ACode: Integer; const AReason: string);begin TThread.Synchronize(nil, procedure begin LabelStatus.Text := 'Disconnected - Reconnecting...'; ButtonConnect.Text := 'Connect'; end);end;โprocedure TFormPriceFeed.OnWSError(Sender: TObject; const AError: string);begin TThread.Synchronize(nil, procedure begin LabelStatus.Text := 'Error: ' + AError; end);end;โfunction TFormPriceFeed.GetChangeColor(AChange: Double): string;begin if AChange > 0 then Result := '#27ae60' else if AChange < 0 then Result := '#e74c3c' else Result := '#666';end;โfunction TFormPriceFeed.GetChangeArrow(AChange: Double): string;begin if AChange > 0 then Result := '▲' // up triangle else if AChange < 0 then Result := '▼' // down triangle else Result := '▬'; // dashend;โprocedure TFormPriceFeed.UpdatePriceDisplay;var HTML: TStringBuilder; Pair: TPair<string, TPriceInfo>;begin HTML := TStringBuilder.Create; try HTML.AppendLine('<div style="font-family: Arial, sans-serif; padding: 15px;">'); HTML.AppendLine('<h2 style="color: #2c3e50;">Live Price Feed</h2>'); HTML.AppendLine('<table style="width: 100%; border-collapse: collapse;">'); HTML.AppendLine('<thead>'); HTML.AppendLine('<tr style="background: #2c3e50; color: white;">'); HTML.AppendLine(' <th style="padding: 10px; text-align: left;">Symbol</th>'); HTML.AppendLine(' <th style="padding: 10px; text-align: right;">Price</th>'); HTML.AppendLine(' <th style="padding: 10px; text-align: right;">Change</th>'); HTML.AppendLine(' <th style="padding: 10px; text-align: right;">%</th>'); HTML.AppendLine(' <th style="padding: 10px; text-align: right;">Updated</th>'); HTML.AppendLine('</tr>'); HTML.AppendLine('</thead>'); HTML.AppendLine('<tbody>');โ for Pair in FPrices do begin var Color := GetChangeColor(Pair.Value.Change); var Arrow := GetChangeArrow(Pair.Value.Change);โ HTML.AppendFormat( '<tr style="border-bottom: 1px solid #eee;">' + ' <td style="padding: 10px; font-weight: bold;">%s</td>' + ' <td style="padding: 10px; text-align: right; font-size: 1.1em;">$%.2f</td>' + ' <td style="padding: 10px; text-align: right; color: %s;">%s %.2f</td>' + ' <td style="padding: 10px; text-align: right; color: %s;">%.2f%%</td>' + ' <td style="padding: 10px; text-align: right; color: #999; font-size: 0.85em;">%s</td>' + '</tr>', [Pair.Value.Symbol, Pair.Value.Price, Color, Arrow, Abs(Pair.Value.Change), Color, Pair.Value.ChangePercent, FormatDateTime('hh:nn:ss', Pair.Value.LastUpdate)]); end;โ HTML.AppendLine('</tbody>'); HTML.AppendLine('</table>');โ if FPrices.Count = 0 then HTML.AppendLine('<p style="color: #999; text-align: center; padding: 40px;">Waiting for price data...</p>');โ HTML.AppendLine('</div>');โ HTMLRender1.HTML.Text := HTML.ToString; finally HTML.Free; end;end;โend.10. Exercise: Notification System#
Build a notification system with these requirements:
- Connect to a WebSocket server at
wss://api.example.com/notifications - Receive notifications as JSON with
id,title,message,type(info, warning, error), andtimestampfields - Display notifications as toast-style messages in an HTML renderer
- Show a badge counter of unread notifications
- Clicking a notification marks it as read (send
{"type": "mark_read", "id": "..."}back to the server) - Notifications fade out after 10 seconds unless marked as read manually
Solution#
unit NotificationForm;โinterfaceโuses System.SysUtils, System.Types, System.Classes, System.JSON, System.Generics.Collections, FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, FMX.Layouts, FMX.Objects, Tina4WebSocketClient, Tina4HTMLRender, Tina4Core;โtype TNotification = record ID: string; Title: string; Message: string; NotifType: string; // info, warning, error Timestamp: TDateTime; IsRead: Boolean; end;โ TFormNotifications = class(TForm) Tina4WebSocket1: TTina4WebSocketClient; HTMLRender1: TTina4HTMLRender; LabelBadge: TLabel; LabelStatus: TLabel; TimerFade: TTimer; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure TimerFadeTimer(Sender: TObject); private FNotifications: TList<TNotification>; procedure OnWSOpen(Sender: TObject); procedure OnWSMessage(Sender: TObject; const AMessage: string); procedure OnWSClose(Sender: TObject; const ACode: Integer; const AReason: string); procedure RefreshDisplay; procedure UpdateBadge; procedure MarkAsRead(const AID: string); function GetTypeColor(const AType: string): string; function GetTypeIcon(const AType: string): string; function UnreadCount: Integer; end;โvar FormNotifications: TFormNotifications;โimplementationโ{$R *.fmx}โprocedure TFormNotifications.FormCreate(Sender: TObject);begin FNotifications := TList<TNotification>.Create;โ Tina4WebSocket1.URL := 'wss://api.example.com/notifications'; Tina4WebSocket1.AutoReconnect := True; Tina4WebSocket1.ReconnectInterval := 5000; Tina4WebSocket1.PingInterval := 30000; Tina4WebSocket1.OnConnected := OnWSOpen; Tina4WebSocket1.OnMessage := OnWSMessage; Tina4WebSocket1.OnDisconnected := OnWSClose;โ // Timer checks for notifications older than 10 seconds TimerFade.Interval := 2000; TimerFade.Enabled := True;โ // Register click handler for mark-as-read HTMLRender1.RegisterObject('Notif', Self);โ Tina4WebSocket1.Connect; LabelStatus.Text := 'Connecting...'; UpdateBadge; RefreshDisplay;end;โprocedure TFormNotifications.FormDestroy(Sender: TObject);begin Tina4WebSocket1.AutoReconnect := False; if Tina4WebSocket1.IsConnected then Tina4WebSocket1.Disconnect; FNotifications.Free;end;โprocedure TFormNotifications.OnWSOpen(Sender: TObject);begin TThread.Synchronize(nil, procedure begin LabelStatus.Text := 'Connected'; end);end;โprocedure TFormNotifications.OnWSMessage(Sender: TObject; const AMessage: string);begin TThread.Synchronize(nil, procedure var JSON: TJSONObject; Notif: TNotification; begin JSON := StrToJSONObject(AMessage); if not Assigned(JSON) then Exit; try var MsgType := JSON.GetValue<String>('type', '');โ if (MsgType = 'notification') or (MsgType = 'info') or (MsgType = 'warning') or (MsgType = 'error') then begin Notif.ID := JSON.GetValue<String>('id', GetGUID); Notif.Title := JSON.GetValue<String>('title', 'Notification'); Notif.Message := JSON.GetValue<String>('message', ''); Notif.NotifType := JSON.GetValue<String>('type', 'info'); Notif.Timestamp := Now; Notif.IsRead := False;โ FNotifications.Insert(0, Notif); // Newest first UpdateBadge; RefreshDisplay; end; finally JSON.Free; end; end);end;โprocedure TFormNotifications.OnWSClose(Sender: TObject; const ACode: Integer; const AReason: string);begin TThread.Synchronize(nil, procedure begin LabelStatus.Text := 'Reconnecting...'; end);end;โprocedure TFormNotifications.TimerFadeTimer(Sender: TObject);var Changed: Boolean; I: Integer;begin Changed := False;โ // Remove unread notifications older than 10 seconds for I := FNotifications.Count - 1 downto 0 do begin var Notif := FNotifications[I]; if (not Notif.IsRead) and (SecondsBetween(Now, Notif.Timestamp) > 10) then begin Notif.IsRead := True; FNotifications[I] := Notif; Changed := True; end; end;โ if Changed then begin UpdateBadge; RefreshDisplay; end;end;โprocedure TFormNotifications.MarkAsRead(const AID: string);var I: Integer;begin for I := 0 to FNotifications.Count - 1 do begin var Notif := FNotifications[I]; if Notif.ID = AID then begin Notif.IsRead := True; FNotifications[I] := Notif;โ // Notify the server if Tina4WebSocket1.IsConnected then begin var Msg := TJSONObject.Create; try Msg.AddPair('type', 'mark_read'); Msg.AddPair('id', AID); Tina4WebSocket1.Send(Msg.ToString); finally Msg.Free; end; end;โ UpdateBadge; RefreshDisplay; Break; end; end;end;โfunction TFormNotifications.UnreadCount: Integer;var I: Integer;begin Result := 0; for I := 0 to FNotifications.Count - 1 do if not FNotifications[I].IsRead then Inc(Result);end;โprocedure TFormNotifications.UpdateBadge;var Count: Integer;begin Count := UnreadCount; if Count > 0 then begin LabelBadge.Text := Count.ToString; LabelBadge.Visible := True; end else LabelBadge.Visible := False;end;โfunction TFormNotifications.GetTypeColor(const AType: string): string;begin if AType = 'error' then Result := '#e74c3c' else if AType = 'warning' then Result := '#f39c12' else Result := '#3498db';end;โfunction TFormNotifications.GetTypeIcon(const AType: string): string;begin if AType = 'error' then Result := '❌' else if AType = 'warning' then Result := '⚠' else Result := 'ℹ';end;โprocedure TFormNotifications.RefreshDisplay;var HTML: TStringBuilder; Notif: TNotification;begin HTML := TStringBuilder.Create; try HTML.AppendLine('<div style="font-family: Arial, sans-serif; padding: 10px;">'); HTML.AppendLine('<h3 style="color: #2c3e50;">Notifications</h3>');โ if FNotifications.Count = 0 then begin HTML.AppendLine('<p style="color: #999; text-align: center; padding: 30px;">No notifications</p>'); end else begin for Notif in FNotifications do begin var Color := GetTypeColor(Notif.NotifType); var Icon := GetTypeIcon(Notif.NotifType); var Opacity: string; if Notif.IsRead then Opacity := '0.5' else Opacity := '1.0';โ HTML.AppendFormat( '<div style="border-left: 4px solid %s; padding: 10px 15px; margin: 8px 0; ' + ' background: white; border-radius: 0 4px 4px 0; opacity: %s; ' + ' box-shadow: 0 1px 3px rgba(0,0,0,0.1); cursor: pointer;" ' + ' onclick="Notif:MarkAsRead(''%s'')">' + ' <div style="display: flex; justify-content: space-between;">' + ' <strong>%s %s</strong>' + ' <small style="color: #999;">%s</small>' + ' </div>' + ' <p style="margin: 5px 0 0; color: #555;">%s</p>' + ' %s' + '</div>', [Color, Opacity, Notif.ID, Icon, Notif.Title, FormatDateTime('hh:nn:ss', Notif.Timestamp), Notif.Message, IfThen(not Notif.IsRead, '<small style="color: ' + Color + ';">Click to mark as read</small>', '')]); end; end;โ HTML.AppendLine('</div>'); HTMLRender1.HTML.Text := HTML.ToString; finally HTML.Free; end;end;โend.Common Gotchas#
Connection lifecycle. The WebSocket connection runs on a background thread. Every event handler fires on that background thread. If you touch any UI control without TThread.Synchronize, your application will crash with access violations, or worse, silently corrupt UI state. Always wrap UI updates:
// Wrong -- will crashprocedure TForm1.OnWSMessage(Sender: TObject; const AMessage: string);begin Label1.Text := AMessage; // Access violation!end;โ// Rightprocedure TForm1.OnWSMessage(Sender: TObject; const AMessage: string);begin TThread.Synchronize(nil, procedure begin Label1.Text := AMessage; end);end;Thread safety with shared data. If your message handler writes to a TList or TDictionary that the UI thread also reads, you need synchronization. The simplest approach is to do everything inside TThread.Synchronize. For high-frequency messages, consider a thread-safe queue.
Reconnect re-subscription. When auto-reconnect opens a new connection, the server does not remember your subscriptions from the previous connection. Always re-subscribe in the OnConnected handler:
procedure TForm1.OnWSOpen(Sender: TObject);begin TThread.Synchronize(nil, procedure begin // This runs on every connect, including reconnects Tina4WebSocket1.Send('{"type": "subscribe", "channel": "updates"}'); end);end;Memory management with TJSONObject. Every TJSONObject or TJSONArray you create from parsing must be freed. In the message handler, parse once, extract what you need, free immediately:
procedure TForm1.OnWSMessage(Sender: TObject; const AMessage: string);var JSON: TJSONObject;begin JSON := StrToJSONObject(AMessage); if not Assigned(JSON) then Exit; try // Extract values here var Name := JSON.GetValue<String>('name', ''); // Use the values... finally JSON.Free; // Always free end;end;Disconnect before destroy. Always disconnect the WebSocket before the form is destroyed. If the background thread tries to fire an event after the form is freed, you get a use-after-free crash:
procedure TForm1.FormDestroy(Sender: TObject);begin Tina4WebSocket1.AutoReconnect := False; // Stop reconnection first if Tina4WebSocket1.IsConnected then Tina4WebSocket1.Disconnect;end;Secure connections (WSS). For wss:// URLs, you need the same OpenSSL DLLs required for HTTPS REST calls. Without them, the connection will fail silently. See the Installation chapter for SSL setup details.