I'm writing a few Service apps in Delphi 10.2 pro, and I want to add a launch-time-controllable parameter to force the service apps into a start-up wait-loop long enough to allow me to click into the "Run\Attach to Process" window (before the app commences the initialization code).
To accomplish this, I want to put a Sleep loop into the TService.OnCreate handler which only gets activated if Winapi.WinSvc.StartService passes an argument which specifies the desired delay length in seconds.
The problem I am having: The values being placed into lpServiceArgVectors (StartService 3rd argument) aren't available in the ParamStr(1) function from within the service. I've read that there is an issue with the VAR parameter passing of this argument, but I think I've got that accounted for in my test app (StartService always returns TRUE).
I just can't get the parameters to be seen in the service, and I need some help to get around this wall.
I've put together a short(ish) self-contained example. The crux of this example is the interaction between TMainWindow.StartService (where the lpServiceArgVectors get assembled and passed) and the ServiceCreate -> CheckStartUpDelayParam procedures in TSimpleServiceDelayTest. The service logs to a text file which displays some diagnostic logging; the log is in descending order so that the newest data is inserted at the top.
There are 3 different menu items to call StartService (to vary the calling args) Note that the logged value of ParamStr(1) is always regardless of which Start Service menu option is selected:
//-------------- SimpleHeartbeatService.dpr --------------
program SimpleHeartbeatService;
uses
Vcl.SvcMgr,
ServiceUnit in 'ServiceUnit.pas' {SimpleServiceDelayTest: TService};
{$R *.RES}
begin
if not Application.DelayInitialize or Application.Installing then
Application.Initialize;
Application.CreateForm(TSimpleServiceDelayTest, SimpleServiceDelayTest);
Application.Run;
end.
//------------------ ServiceUnit.pas -----------------------------
unit ServiceUnit;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.SvcMgr, Vcl.Dialogs;
type
TSimpleServiceDelayTest = class(TService)
procedure ServiceExecute(Sender: TService);
procedure ServiceCreate(Sender: TObject);
procedure ServiceShutdown(Sender: TService);
procedure ServiceStart(Sender: TService; var Started: Boolean);
procedure ServiceStop(Sender: TService; var Stopped: Boolean);
procedure ServiceDestroy(Sender: TObject);
private
PrevHeartbeatStr: String;
ServiceLog: TStringList;
Procedure CheckStartUpDelayParam;
Procedure DriveHeartbeatLogging;
Procedure Log(Const Msg: String);
Function LogFileName: String;
public
function GetServiceController: TServiceController; override;
{ Public declarations }
end;
var
SimpleServiceDelayTest: TSimpleServiceDelayTest;
implementation
{$R *.dfm}
// =============================================================================
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
SimpleServiceDelayTest.Controller(CtrlCode);
end;
// =============================================================================
Procedure TSimpleServiceDelayTest.CheckStartUpDelayParam;
Const
OneSec = 1 / 86400;
Var
DelaySecs: Integer;
TZero: TDateTime;
Begin
Log('CheckStartUpDelayParam');
Log('ParamStr(0)=' + ParamStr(0));
Log('ParamStr(1)=' + ParamStr(1));
// ********** THIS IS THE GOAL OF THIS WHOLE ENDEAVOR: **********
// I want to pause the initialization long enough to attach the
// Delphi debugger (via Run | Attach to Process...)
// I want to pass a command line parameter via the NumArgs/pArgVectors args
// from: Winapi.WinSvc.StartService(Svc, NumArgs, pArgVectors)
// So far, I have not been able to pass arguments this way.
DelaySecs := StrToIntDef(ParamStr(1), 0);
If DelaySecs > 0 Then
Begin
TZero := Now;
While Now - TZero > DelaySecs * OneSec do
Sleep(250);
End;
End;
// =============================================================================
Procedure TSimpleServiceDelayTest.DriveHeartbeatLogging;
Var
HeartbeatStr: String;
begin
HeartbeatStr := FormatDateTime('hh:mm', Now);
If HeartbeatStr <> PrevHeartbeatStr Then
Try
Log('HeartbeatStr = ' + HeartbeatStr);
Finally
PrevHeartbeatStr := HeartbeatStr;
End;
end;
// =============================================================================
function TSimpleServiceDelayTest.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;
// =============================================================================
Procedure TSimpleServiceDelayTest.Log(const Msg: string);
begin
ServiceLog.Insert(0, FormatDateTime('yyyy/mm/dd hh:mm:ss.zzz ', Now) + Msg);
While ServiceLog.Count > 500 do
ServiceLog.Delete(ServiceLog.Count-1);
// Save after every addition; inefficient, but thorough for debugging
ServiceLog.SaveToFile(LogFileName);
end;
// =============================================================================
Function TSimpleServiceDelayTest.LogFileName: String;
Begin
Result := System.SysUtils.ChangeFileExt(ParamStr(0), '.txt');
End;
// =============================================================================
procedure TSimpleServiceDelayTest.ServiceCreate(Sender: TObject);
begin
ServiceLog := TStringList.Create;
If FileExists(LogFileName) Then
ServiceLog.LoadFromFile(LogFileName);
Log('^^^ ServiceCreate ^^^');
CheckStartUpDelayParam;
end;
// =============================================================================
procedure TSimpleServiceDelayTest.ServiceDestroy(Sender: TObject);
begin
PrevHeartbeatStr := '';
ServiceLog.Free;
end;
// =============================================================================
procedure TSimpleServiceDelayTest.ServiceExecute(Sender: TService);
begin
Try
Log('Entering ServiceExecute loop');
While Not Terminated do
Begin
ServiceThread.ProcessRequests(False);
DriveHeartbeatLogging;
// Do other stuff
Sleep(1000);
End;
Log('Exiting due to normal termination');
Except
On E: Exception do
Log('Exiting due to Exception:' + #13#10 + E.Message);
End;
End;
// =============================================================================
procedure TSimpleServiceDelayTest.ServiceShutdown(Sender: TService);
begin
Log('ServiceShutdown');
end;
// =============================================================================
procedure TSimpleServiceDelayTest.ServiceStart(Sender: TService;
var Started: Boolean);
begin
Log('ServiceStart');
Started := True;
end;
// =============================================================================
procedure TSimpleServiceDelayTest.ServiceStop(Sender: TService;
var Stopped: Boolean);
begin
Log('ServiceStop');
Stopped := True;
end;
// =============================================================================
end.
//------------ ServiceUnit.dfm -----------------------
object SimpleServiceDelayTest: TSimpleServiceDelayTest
OldCreateOrder = False
OnCreate = ServiceCreate
OnDestroy = ServiceDestroy
DisplayName = 'Simple Delphi Service (Startup-Delay Test)'
OnExecute = ServiceExecute
OnShutdown = ServiceShutdown
OnStart = ServiceStart
OnStop = ServiceStop
Height = 150
Width = 215
end
Next, a short GUI Service Interface app to (Un)Install, Start/Stop
//------------- SimpleServiceController.dpr ------------
program SimpleServiceController;
uses
Vcl.Forms,
ControllerMainUnit in 'ControllerMainUnit.pas' {MainWindow};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TMainWindow, MainWindow);
Application.Run;
end.
//-------------- ControlerMainUnit.pas ------------------
unit ControllerMainUnit;
interface
uses
System.Classes, System.SysUtils, System.Variants, Vcl.ComCtrls,
Vcl.Controls, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.Forms, Vcl.Graphics, Vcl.Menus,
Vcl.StdCtrls, Winapi.Messages, Winapi.Windows;
type
TMainWindow = class(TForm)
InstallService1: TMenuItem;
MainMenu1: TMainMenu;
Memo1: TMemo;
StartService1: TMenuItem;
StopService1: TMenuItem;
Timer1: TTimer;
UninstallService1: TMenuItem;
StatusBar1: TStatusBar;
StartWithoutDelayMenuItem: TMenuItem;
StartWith10SecondDelay1: TMenuItem;
StartWithXParameter1: TMenuItem;
procedure Timer1Timer(Sender: TObject);
procedure InstallService1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure StartWithoutDelayMenuItemClick(Sender: TObject);
procedure StartWith10SecondDelay1Click(Sender: TObject);
procedure StopService1Click(Sender: TObject);
procedure UninstallService1Click(Sender: TObject);
procedure StartWithXParameter1Click(Sender: TObject);
private
{ Private declarations }
FileTimeLoaded: _FILETIME;
SCMError: Cardinal;
SCMHandle: THandle;
StatusStr: String;
Function CurrentFileTime: _FILETIME;
Function LogFileName: String;
Procedure RelaunchElevatedPrompt;
Function ServiceExePath: String;
Procedure StartService(Const Parameter: String);
Procedure StopService;
public
{ Public declarations }
end;
var
MainWindow: TMainWindow;
implementation
{$R *.dfm}
Uses
System.UITypes, Winapi.ShellAPI, Winapi.WinSvc;
Const
cServiceName = 'SimpleServiceDelayTest';
// =============================================================================
Function AppHasElevatedPrivs: Boolean;
const
TokenElevationType = 18;
TokenElevation = 20;
TokenElevationTypeDefault = 1;
TokenElevationTypeFull = 2;
TokenElevationTypeLimited = 3;
var
token: THandle;
Elevation: DWord;
dwSize: Cardinal;
begin
Try
if OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, token) then
try
if GetTokenInformation(token, TTokenInformationClass(TokenElevation),
#Elevation, SizeOf(Elevation), dwSize) then
Result := Elevation <> 0
else
Result := False;
finally
CloseHandle(token);
end
else
Result := False;
Except
Result := False;
End;
End;
// =============================================================================
Procedure Launch(Exe, Params: String);
Var
Dir: String;
Begin
Dir := ExtractFileDir(Exe);
ShellExecute(0, 'open', PChar(Exe), PChar(Params), PChar(Dir), SW_SHOWNORMAL);
End;
// =============================================================================
Function NowStr: String;
Begin
Result := FormatDateTime('yyyy/mm/dd hh:mm:ss', Now);
End;
// =============================================================================
Procedure LaunchElevated(Const Exe, Params: String);
Var
Dir: String;
Begin
Dir := ExtractFileDir(Exe);
ShellExecute(0, 'runas', PChar(Exe), PChar(Params), PChar(Dir),
SW_SHOWNORMAL);
End;
// =============================================================================
Function TMainWindow.CurrentFileTime;
Var
FAD: TWin32FileAttributeData;
begin
GetFileAttributesEx(PChar(LogFileName), GetFileExInfoStandard, #FAD);
Result := FAD.ftLastWriteTime;
end;
// =============================================================================
procedure TMainWindow.FormCreate(Sender: TObject);
begin
Application.Title := 'SimpleServiceController';
if AppHasElevatedPrivs then
begin
SetLastError(0);
SCMHandle := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS);
SCMError := GetLastError;
end
else
begin
SCMHandle := 0;
SCMError := 0;
end;
end;
// =============================================================================
procedure TMainWindow.InstallService1Click(Sender: TObject);
begin
If AppHasElevatedPrivs Then
Launch(ServiceExePath, '/install')
Else
LaunchElevated(ServiceExePath, '/install');
End;
// =============================================================================
Function TMainWindow.LogFileName: String;
Begin
Result := ExtractFileDir(Application.ExeName) + '\SimpleHeartbeatService.txt';
End;
// =============================================================================
Procedure TMainWindow.RelaunchElevatedPrompt;
Var
Prompt: String;
X, Y: Integer;
Begin
Prompt := 'Elevated privileges required to start/stop service.'#13#10 +
'Re-launch ' + Application.Title + ' with elevated privileges?';
X := Left + 32;
Y := Top + 32;
If MessageDlgPos(Prompt, mtConfirmation, [mbYes, mbNo], 0, X, Y) = mrYes then
Begin
LaunchElevated(Application.ExeName, '');
Close;
End;
End;
// =============================================================================
Function TMainWindow.ServiceExePath;
begin
Result := ExtractFileDir(Application.ExeName) + '\SimpleHeartbeatService.exe';
end;
// =============================================================================
Procedure TMainWindow.StartService(Const Parameter: string);
Var
Result:Boolean;
Svc: THandle;
NumArgs: DWord;
// ********** IS THIS THE CORRECT WAY TO SETUP lpServiceArgVectors ? *********
// learn.microsoft.com/en-us/windows/desktop/api/winsvc/nf-winsvc-startservicea
// ***************************************************************************
ArgVectors: Array [0 .. 1] of PChar;
pArgVectors: LPCWSTR; // To match VAR parameter type in StartService
Begin
Try
If SCMHandle = 0 Then
RelaunchElevatedPrompt
Else
Begin
Svc := OpenService(SCMHandle, PChar(cServiceName), SERVICE_START);
if Svc = 0 then
RaiseLastOSError;
try
// ******************* THIS IS WHERE I AM STYMIED **********************
// StartService reports no errors either way it gets called below,
// but no parameter are detected in the service when
// ArgVectors = 'SimpleServiceDelayTest','10' and NumArgs = 2
// *********************************************************************
If Parameter <> '' Then
Begin
NumArgs := 2;
ArgVectors[0] := PChar(cServiceName);
ArgVectors[1] := PChar(Parameter); // Try 10 second delay
pArgVectors := #ArgVectors;
End
Else
Begin
NumArgs := 0;
ArgVectors[0] := '';
ArgVectors[1] := '';
pArgVectors := Nil;
End;
// NO ERROR, EITHER WAY; BUT PARAMSTR(1) ALWAYS BLANK IN SERVICE
If Parameter = 'X'
Then
// http://codeverge.com/embarcadero.delphi.nativeapi/calling-startservice-with-multip/1067853
Result := Winapi.WinSvc.StartService(Svc, NumArgs, ArgVectors[0])
Else
Result := Winapi.WinSvc.StartService(Svc, NumArgs, pArgVectors);
If Result then
ShowMessage('StartService('''+Parameter+''') returned TRUE')
else
RaiseLastOSError;
finally
CloseServiceHandle(Svc);
end;
End;
except
On E: Exception do
Raise Exception.Create('StartService: ' + E.Message);
end;
end;
// =============================================================================
procedure TMainWindow.StartWith10SecondDelay1Click(Sender: TObject);
begin
StartService('10');
end;
// =============================================================================
procedure TMainWindow.StartWithoutDelayMenuItemClick(Sender: TObject);
begin
StartService('');
end;
procedure TMainWindow.StartWithXParameter1Click(Sender: TObject);
begin
StartService('X');
end;
// =============================================================================
Procedure TMainWindow.StopService;
Const
OneSec = 1 / 86400;
Var
Svc: THandle;
Status: SERVICE_STATUS;
TZero: TDateTime;
begin
Try
If SCMHandle = 0 Then
RelaunchElevatedPrompt
Else
Begin
Svc := OpenService(SCMHandle, PChar(cServiceName), SERVICE_STOP or
SERVICE_QUERY_STATUS);
if Svc = 0 then
RaiseLastOSError
else
Try
if Winapi.WinSvc.ControlService(Svc, SERVICE_CONTROL_STOP, Status)
then
Begin
TZero := Now;
while QueryServiceStatus(Svc, Status) and
(Status.dwCurrentState <> SERVICE_STOPPED) and
(Now - TZero < 5 * OneSec) do
Begin
Application.ProcessMessages;
Sleep(10);
End;
End
Else
Raise Exception.Create('WinSvc.ControlService returned FALSE');
finally
CloseServiceHandle(Svc);
end;
End;
except
On E: Exception do
Raise Exception.Create('StartService: ' + E.Message);
end;
end;
// =============================================================================
procedure TMainWindow.StopService1Click(Sender: TObject);
begin
StopService;
end;
// =============================================================================
procedure TMainWindow.Timer1Timer(Sender: TObject);
begin
Try
If Int64(CurrentFileTime) <> Int64(FileTimeLoaded) Then
Begin
Memo1.Lines.LoadFromFile(LogFileName);
FileTimeLoaded := CurrentFileTime;
StatusStr := ' File loaded # ' + NowStr;
End;
Except
StatusStr := ' Unable to load file # ' + NowStr;
End;
StatusBar1.Panels[0].Text := FormatDateTime('hh:mm:ss ', Now) + StatusStr;
end;
// =============================================================================
procedure TMainWindow.UninstallService1Click(Sender: TObject);
begin
If AppHasElevatedPrivs Then
Launch(ServiceExePath, '/uninstall')
Else
LaunchElevated(ServiceExePath, '/uninstall');
end;
// =============================================================================
end.
//------------------- ControllerMainUnit.dfm ----------------
object MainWindow: TMainWindow
Left = 0
Top = 0
Caption = 'Simple Service Controller'
ClientHeight = 264
ClientWidth = 530
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
Menu = MainMenu1
OldCreateOrder = False
OnCreate = FormCreate
PixelsPerInch = 96
TextHeight = 13
object Memo1: TMemo
Left = 0
Top = 0
Width = 530
Height = 245
Align = alClient
Font.Charset = ANSI_CHARSET
Font.Color = clWindowText
Font.Height = -13
Font.Name = 'Courier New'
Font.Style = []
Lines.Strings = (
'Memo1')
ParentFont = False
ScrollBars = ssBoth
TabOrder = 0
end
object StatusBar1: TStatusBar
Left = 0
Top = 245
Width = 530
Height = 19
Panels = <
item
Width = 50
end>
end
object MainMenu1: TMainMenu
Left = 136
Top = 40
object InstallService1: TMenuItem
Caption = 'Install Service'
OnClick = InstallService1Click
end
object UninstallService1: TMenuItem
Caption = 'Uninstall Service'
OnClick = UninstallService1Click
end
object StartService1: TMenuItem
Caption = 'Start Service'
object StartWithoutDelayMenuItem: TMenuItem
Caption = 'Start Without Delay'
OnClick = StartWithoutDelayMenuItemClick
end
object StartWith10SecondDelay1: TMenuItem
Caption = 'Start With 10 Second Delay'
OnClick = StartWith10SecondDelay1Click
end
object StartWithXParameter1: TMenuItem
Caption = 'Start With "X" Parameter'
OnClick = StartWithXParameter1Click
end
end
object StopService1: TMenuItem
Caption = 'Stop Service'
OnClick = StopService1Click
end
end
object Timer1: TTimer
OnTimer = Timer1Timer
Left = 240
Top = 40
end
end
The TService.OnCreate event is the wrong place to run a delay loop. You need to put it in the TService.OnStart event instead.
The OnCreate event is always called at process startup, regardless of why the process is being run - (un)installation or service start.
The OnStart event is called only when the service is being started by the SCM. That is where you need to process your service start parameters.
The ParamStr() function retrieves the calling process's command-line parameters only, and that is not the correct place to look for service start parameters as they are not passed on the command line. They will be accessible from the TService.Param[] property instead, once the SCM has signaled the service to start.
Try something more like this instead:
Procedure TSimpleServiceDelayTest.CheckStartUpDelayParam;
const
OneSec = 1000;
var
DelaySecs: Integer;
TZero: DWORD;
i, num: Integer;
begin
Log('CheckStartUpDelayParam');
DelaySecs := 0;
for i := 0 to ParamCount-1 do
begin
Log('Param['+IntToStr(i)+']=' + Param[i]);
if DelaySecs = 0 then
begin
if TryStrToInt(Param[i], num) and (num > 0) then
DelaySecs := num;
end;
end;
if DelaySecs > 0 then
begin
TZero := GetTickCount();
repeat
Sleep(250); // NOTE: should not exceed the TService.WaitHint value...
ReportStatus;
until (GetTickCount() - TZero) >= (DelaySecs * OneSec);
end;
end;
...
procedure TSimpleServiceDelayTest.ServiceCreate(Sender: TObject);
begin
ServiceLog := TStringList.Create;
if FileExists(LogFileName) then
ServiceLog.LoadFromFile(LogFileName);
Log('^^^ ServiceCreate ^^^');
// DO NOT call CheckStartUpDelayParam() here!
end;
procedure TSimpleServiceDelayTest.ServiceStart(Sender: TService; var Started: Boolean);
begin
Log('ServiceStart');
// call CheckStartUpDelayParam() here instead!
CheckStartUpDelayParam;
Started := True;
end;
procedure TMainWindow.StartService(Const Parameter: string);
var
Result: Boolean;
Svc: THandle;
ArgVectors: Array [0 .. 1] of PChar;
NumArgs: DWORD;
pArgs: PPChar;
begin
try
if SCMHandle = 0 Then
RelaunchElevatedPrompt
else
begin
Svc := OpenService(SCMHandle, PChar(cServiceName), SERVICE_START);
if Svc = 0 then
RaiseLastOSError;
try
if Parameter <> '' then
begin
NumArgs := 2;
ArgVectors[0] := PChar(cServiceName);
ArgVectors[1] := PChar(Parameter);
pArgs := #ArgVectors[0];
end
else
begin
NumArgs := 0;
pArgs := nil;
end;
if not Winapi.WinSvc.StartService(Svc, NumArgs, pArgs^) then
RaiseLastOSError;
finally
CloseServiceHandle(Svc);
end;
ShowMessage('StartService('''+Parameter+''') returned TRUE')
end;
except
on E: Exception do
begin
raise Exception.Create('StartService: ' + E.Message);
end;
end;
end;
Related
I have an Indy TCPServer that connects a device with several clients.
When device-data arrives, the server sends it to every client.
When client-data arrives, it is sent to the device. (And the device will send it to the server again (echo)).
I only process 2 bytes per cycle.
The above works perfect.
Now i want to process/save that data.
Only when data arrives via the device port, i want to 'Translate' the data.
I need to save the data to a clientdatset.
Then i want to take the bytes apart and compare them with som other info
...
The translated data is also saved in another clientdataset.
unit BusServer;
interface
uses
Winapi.Windows, Winapi.Messages,
System.SysUtils, System.Classes, System.Variants,
Vcl.Graphics, Vcl.Controls, Vcl.SvcMgr, Vcl.Dialogs,
IdContext, IdAntiFreezeBase, IdAntiFreeze, IdBaseComponent, IdComponent,
IdCustomTCPServer, IdTCPServer,
Data.DB, Datasnap.DBClient;
type
TBus_Server = class(TService)
tcpBusDataServer: TIdTCPServer;
IdAntiFreeze1: TIdAntiFreeze;
cdsBusMonitor: TClientDataSet;
cdsBusMonitorNr: TIntegerField;
cdsBusMonitorDate: TStringField;
cdsBusMonitorTime: TStringField;
cdsBusMonitorAad: TIntegerField;
cdsBusMonitorAgr: TIntegerField;
cdsBusMonitorAName: TStringField;
cdsBusMonitorAFct: TStringField;
cdsBusMonitorOrigin: TStringField;
cdsIncoming: TClientDataSet;
cdsMemberState: TClientDataSet;
cdsMemberStateMemberID: TStringField;
cdsMemberStateState: TStringField;
cdsMemberStateDateTime: TDateTimeField;
cdsMemberStateTotaal: TFloatField;
procedure tcpBusDataServerExecute(AContext: TIdContext);
procedure ServiceCreate(Sender: TObject);
private
{ Private declarations }
functionmon: String;
DevicePort: Integer;
ClientPort: Integer;
ClientLSB, ClientMSB: Byte;
DeviceLSB, DeviceMSB: Byte;
FunctionList: TStringList;
procedure TranslateData;
function CodeValue: String;
public
function GetServiceController: TServiceController; override;
{ Public declarations }
end;
var
Bus_Server: TBus_Server;
implementation
uses
CodesiteLogging;
{$R *.dfm}
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
Bus_Server.Controller(CtrlCode);
end;
function TBus_Server.CodeValue: String;
begin
if (cdsIncoming.FieldbyName('MemberType').AsInteger = 11) or
(cdsIncoming.FieldbyName('MemberType').AsInteger = 22)or
(cdsIncoming.FieldbyName('MemberType').AsInteger = 33) then
begin
Result := FunctionMon
end
else
begin
// TODO:
end
end;
function TBus_Server.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;
procedure TBus_Server.ServiceCreate(Sender: TObject);
var
DataSetName: String;
begin
DevicePort := 10001;
ClientPort := 10012;
tcpBusDataServer.Bindings.Clear;
tcpBusDataServer.Bindings.Add.Port := DevicePort;
tcpBusDataServer.Bindings.Add.Port := ClientPort;
tcpBusDataServer.Active := True;
FunctionList := TStringList.Create;
FunctionList.Add('Null');
FunctionList.Add('Reset');
FunctionList.Add('Toggle');
FunctionList.Add('Set');
FunctionList.Add('Misc');
FunctionList.Add('Status');
FunctionList.Add('Timer/Direct');
FunctionList.Add('Value');
FunctionList.Add('Dimmer');
FunctionList.Add('Readout');
FunctionList.Add('Teller');
FunctionList.Add('System');
FunctionList.Add('Settings');
FunctionList.Add('Select');
FunctionList.Add('Data');
FunctionList.Add('Program');
ForceDirectories('c:\Test\');
DataSetName := 'c:\Test\BusMonitor' + FormatDateTime('YYYY-MM-DD', Now) + '.xml';
if cdsBusMonitor.Active then
cdsBusMonitor.Close;
cdsBusMonitor.Filename := DataSetName;
if not System.SysUtils.FileExists(DataSetName) then
begin
cdsBusMonitor.CreateDataSet;
cdsBusMonitor.SaveToFile
end;
DataSetName := 'c:\Test\MemberState' + FormatDateTime('YYYY-MM-DD', Now) + '.xml';
if cdsMemberState.Active then
cdsMemberState.Close;
cdsMemberState.Filename := DataSetName;
if not System.SysUtils.FileExists(DataSetName) then
begin
cdsMemberState.CreateDataSet;
cdsMemberState.SaveToFile
end;
end;
procedure TBus_Server.tcpBusDataServerExecute(AContext: TIdContext);
var
Ctx: TIdContext;
List: TList;
begin
if AContext.Binding.Port = ClientPort then // Client
begin
DeviceLSB := AContext.Connection.IOHandler.ReadByte;
DeviceMSB := AContext.Connection.IOHandler.ReadByte;
List := tcpBusDataServer.Contexts.LockList;
try
for var i := 0 to List.count - 1 do
begin
Ctx := TIdContext(List[I]);
if (Ctx <> AContext) and (Ctx.Binding.Port = DevicePort) then
begin
Ctx.Connection.IOHandler.Write(DeviceLSB);
Ctx.Connection.IOHandler.Write(DeviceMSB);
// Since only 1 has to be written to
Break
end
end
finally
tcpBusDataServer.Contexts.UnlockList
end
end
else
begin
if AContext.Binding.Port = DevicePort then // Device
begin
ClientLSB := AContext.Connection.IOHandler.ReadByte;
ClientMSB := AContext.Connection.IOHandler.ReadByte;
List := tcpBusDataServer.Contexts.LockList;
try
for var i := 0 to List.count - 1 do
begin
Ctx := TIdContext(List[I]);
if (Ctx <> AContext) and (Ctx.Binding.Port = ClientPort) then
begin
Ctx.Connection.IOHandler.Write(ClientLSB);
Ctx.Connection.IOHandler.Write(ClientMSB)
end
end
finally
TIdNotify.NotifyMethod(TranslateData);
tcpBusDataServer.Contexts.UnlockList
end
end
end
end;
procedure TBus_Server.TranslateData;
const {$J+}
LastSave: TDateTime = 0;
type
TProgramState = (psNone,psProgram,psIgnore1,psIgnore2);
const
ProgramState: TProgramState = psNone;
const
ValueMode: Boolean = False;
var
i: Integer;
fct: Integer;
GroupMon: Integer;
AddressMon: Integer;
CorrecteSettings: Boolean;
TmpStr: String;
begin
fct := 0;
// Functie uit MSB halen
if ClientMSB >= 128 then
begin
ClientMSB := ClientMSB - 128;
fct := 8
end;
if ClientMSB >= 64 then
begin
ClientMSB := ClientMSB - 64;
fct := fct + 4
end;
if ClientMSB >= 32 then
begin
ClientMSB := ClientMSB - 32;
fct := fct + 2
end;
if ClientMSB >= 16 then
begin
ClientMSB := ClientMSB - 16;
fct := fct + 1
end;
// Variabelen voor monitor bepalen
functionMon := FunctionList[fct];
if cdsBusMonitor.Active then
begin
cdsBusMonitor.Filtered := False;
cdsBusMonitor.Append;
cdsBusMonitor.FieldByName('Nr').AsInteger := cdsBusMonitor.RecordCount + 1;
cdsBusMonitor.FieldByName('AFct').Asstring := functionMon;
cdsBusMonitor.FieldByName('Aad').AsInteger := ClientLSB;
cdsBusMonitor.FieldByName('Agr').AsInteger := ClientMSB;
cdsBusMonitor.FieldByName('Time').Asstring := TimeToStr(Now);
cdsBusMonitor.FieldByName('Origin').AsString := 'Van de Bus: '{ + UserPeerIP};
cdsBusMonitor.Post;
end;
if ProgramState = psNone then
begin
CodeSite.Send('New situation...');
try
if cdsIncoming.Locate('Group;Address', VarArrayOf([IntToStr(DeviceMsb),IntToStr(DeviceLsb)]), []) then
begin
CodeSite.Send('After locate...');
if cdsMemberState.Locate('MemberID', cdsIncoming.FieldByName('MemberID').AsString, []) then
cdsMemberState.Edit
else
cdsMemberState.Append;
if cdsMemberStateState.AsString = Codevalue then
begin
CodeSite.Send('New state ' + Codevalue + ' already known');
cdsMemberState.Cancel
end
else
begin
CodeSite.Send('New state ' + Codevalue);
cdsMemberStateState.AsString := Codevalue;
if Codevalue.ToLower = 'reset' then
cdsMemberStateTotaal.AsFloat := cdsMemberStateTotaal.AsFloat + (Now - cdsMemberStateDateTime.AsDateTime);
cdsMemberStateDateTime.AsDateTime := Now;
cdsMemberState.Post
end
end
else
CodeSite.SendError('ServerMethodsBServer.cdsIncoming Locate Fail');
except
on E: Exception do
CodeSite.SendException(E);
end
end;
if ((cdsBusMonitor.RecordCount mod 100) = 0) or ((Now - LastSave) > (1/24/60)) then
begin
LastSave := Now;
cdsBusMonitor.MergeChangeLog;
cdsBusMonitor.SaveToFile;
cdsMemberState.MergeChangeLog;
cdsMemberState.SaveToFile
end
end;
In the clientdataset, i occasionally see "wrong data", meaning:
First i have a correct record, followed by a record with the same lsb and a wrong msb.
I split up the msb in the procedure(Translatedata) in a 'high nibble' and a 'low nibble'.
So now i'm trying to find out where this comes from.
As you can see in my code i call the procedure via TIdNotify.NotifyMethod(TranslateData);
Is this te correct way?
Could appending/posting data in a clientdataset (whilst in the serverthread) be a problem?
Is this a timing issue?
Has anyone an idea of what could be wrong?
Your code is not very thread-safe. You are not protecting your data values from concurrent access across thread boundaries, if multiple clients send data to the server at the same time.
Also, you should not be doing all of your service initializations in the OnCreate event, use the OnStart event instead. The OnCreate event is triggered whenever your TService object is created for any reason, which includes not only running the service, but also (un)installing the service.
Try something more like this:
unit BusServer;
interface
uses
Winapi.Windows, Winapi.Messages,
System.SysUtils, System.Classes, System.Variants,
Vcl.Graphics, Vcl.Controls, Vcl.SvcMgr, Vcl.Dialogs,
IdContext, IdBaseComponent, IdComponent,
IdCustomTCPServer, IdTCPServer, IdThreadSafe,
Data.DB, Datasnap.DBClient, System.SyncObjs;
type
TBus_Server = class(TService)
tcpBusDataServer: TIdTCPServer;
cdsBusMonitor: TClientDataSet;
cdsBusMonitorNr: TIntegerField;
cdsBusMonitorDate: TStringField;
cdsBusMonitorTime: TStringField;
cdsBusMonitorAad: TIntegerField;
cdsBusMonitorAgr: TIntegerField;
cdsBusMonitorAName: TStringField;
cdsBusMonitorAFct: TStringField;
cdsBusMonitorOrigin: TStringField;
cdsIncoming: TClientDataSet;
cdsMemberState: TClientDataSet;
cdsMemberStateMemberID: TStringField;
cdsMemberStateState: TStringField;
cdsMemberStateDateTime: TDateTimeField;
cdsMemberStateTotaal: TFloatField;
procedure tcpBusDataServerExecute(AContext: TIdContext);
procedure ServiceStart(Sender: TObject; var Started: Boolean);
procedure ServiceStop(Sender: TObject; var Stopped: Boolean);
procedure ServiceShutdown(Sender: TObject);
private
{ Private declarations }
FunctionMon: TIdThreadSafeString;
DevicePort: Integer;
ClientPort: Integer;
DeviceLSB, DeviceMSB: Byte;
DeviceDataLock: TCriticalSection;
FunctionList: TStringList;
procedure TranslateData(ClientLSB, ClientMSB: Byte);
function CodeValue: String;
public
function GetServiceController: TServiceController; override;
{ Public declarations }
end;
var
Bus_Server: TBus_Server;
implementation
uses
CodesiteLogging;
{$R *.dfm}
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
Bus_Server.Controller(CtrlCode);
end;
function TBus_Server.CodeValue: String;
begin
case cdsIncoming.FieldByName('MemberType').AsInteger of
11, 22, 33: begin
Result := FunctionMon.Value;
end;
else
// TODO
Result := '';
end;
end;
function TBus_Server.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;
procedure TBus_Server.ServiceStart(Sender: TObject; var Started: Boolean);
var
DataSetName: String;
begin
DevicePort := 10001;
ClientPort := 10012;
DeviceDataLock := TCriticalSection.Create;
FunctionMon := TIdThreadSafeString.Create;
FunctionList := TStringList.Create;
FunctionList.Add('Null');
FunctionList.Add('Reset');
FunctionList.Add('Toggle');
FunctionList.Add('Set');
FunctionList.Add('Misc');
FunctionList.Add('Status');
FunctionList.Add('Timer/Direct');
FunctionList.Add('Value');
FunctionList.Add('Dimmer');
FunctionList.Add('Readout');
FunctionList.Add('Teller');
FunctionList.Add('System');
FunctionList.Add('Settings');
FunctionList.Add('Select');
FunctionList.Add('Data');
FunctionList.Add('Program');
ForceDirectories('c:\Test\');
DataSetName := 'c:\Test\BusMonitor' + FormatDateTime('YYYY-MM-DD', Now) + '.xml';
if cdsBusMonitor.Active then
cdsBusMonitor.Close;
cdsBusMonitor.Filename := DataSetName;
if not System.SysUtils.FileExists(DataSetName) then
begin
cdsBusMonitor.CreateDataSet;
cdsBusMonitor.SaveToFile;
end;
DataSetName := 'c:\Test\MemberState' + FormatDateTime('YYYY-MM-DD', Now) + '.xml';
if cdsMemberState.Active then
cdsMemberState.Close;
cdsMemberState.Filename := DataSetName;
if not System.SysUtils.FileExists(DataSetName) then
begin
cdsMemberState.CreateDataSet;
cdsMemberState.SaveToFile;
end;
tcpBusDataServer.Bindings.Clear;
tcpBusDataServer.Bindings.Add.Port := DevicePort;
tcpBusDataServer.Bindings.Add.Port := ClientPort;
tcpBusDataServer.Active := True;
Started := True;
end;
procedure TBus_Server.ServiceStop(Sender: TObject; var Stopped: Boolean);
begin
ServiceShutdown(Sender);
Stopped := True;
end;
procedure TBus_Server.ServiceShutdown(Sender: TObject);
begin
tcpBusDataServer.Active := False;
cdsBusMonitor.Close;
cdsMemberState.Close;
DeviceDataLock.Free;
FunctionMon.Free;
FunctionList.Free;
end;
procedure TBus_Server.tcpBusDataServerExecute(AContext: TIdContext);
var
LSB, MSB: Byte;
List: TList;
Ctx: TIdContext;
begin
LSB := AContext.Connection.IOHandler.ReadByte;
MSB := AContext.Connection.IOHandler.ReadByte;
if AContext.Binding.Port = ClientPort then // Client
begin
DeviceDataLock.Enter;
try
DeviceLSB := LSB;
DeviceMSB := MSB;
finally
DeviceDataLock.Leave;
end;
List := tcpBusDataServer.Contexts.LockList;
try
for var i := 0 to List.count - 1 do
begin
Ctx := TIdContext(List[I]);
if (Ctx <> AContext) and (Ctx.Binding.Port = DevicePort) then
begin
Ctx.Connection.IOHandler.Write(LSB);
Ctx.Connection.IOHandler.Write(MSB);
// Since only 1 has to be written to
Break;
end;
end;
finally
tcpBusDataServer.Contexts.UnlockList;
end;
end
else if AContext.Binding.Port = DevicePort then // Device
begin
List := tcpBusDataServer.Contexts.LockList;
try
for var i := 0 to List.count - 1 do
begin
Ctx := TIdContext(List[I]);
if (Ctx <> AContext) and (Ctx.Binding.Port = ClientPort) then
begin
Ctx.Connection.IOHandler.Write(LSB);
Ctx.Connection.IOHandler.Write(MSB)
end;
end
finally
tcpBusDataServer.Contexts.UnlockList;
TThread.Queue(nil,
procedure
begin
TranslateData(LSB, MSB);
end
);
end;
end;
end;
procedure TBus_Server.TranslateData(ClientLSB, ClientMSB: Byte);
const {$J+}
LastSave: TDateTime = 0;
type
TProgramState = (psNone,psProgram,psIgnore1,psIgnore2);
const
ProgramState: TProgramState = psNone;
const
ValueMode: Boolean = False;
var
i: Integer;
fct: Integer;
GroupMon: Integer;
AddressMon: Integer;
CorrecteSettings: Boolean;
TmpFunc, TmpCodeValue: string;
TmpDeviceLSB, TmpDeviceMSB: Byte;
begin
fct := 0;
// Functie uit MSB halen
if ClientMSB >= 128 then
begin
Dec(ClientMSB, 128);
fct := 8;
end;
if ClientMSB >= 64 then
begin
Dec(ClientMSB, 64);
Inc(fct, 4);
end;
if ClientMSB >= 32 then
begin
Dec(ClientMSB, 32);
Inc(fct, 2);
end;
if ClientMSB >= 16 then
begin
Dec(ClientMSB, 16);
Inc(fct, 1);
end;
// Variabelen voor monitor bepalen
TmpFunc := FunctionList[fct];
FunctionMon.Value := TmpFunc;
if cdsBusMonitor.Active then
begin
cdsBusMonitor.Filtered := False;
cdsBusMonitor.Append;
try
cdsBusMonitor.FieldByName('Nr').AsInteger := cdsBusMonitor.RecordCount + 1;
cdsBusMonitor.FieldByName('AFct').Asstring := TmpFunc;
cdsBusMonitor.FieldByName('Aad').AsInteger := ClientLSB;
cdsBusMonitor.FieldByName('Agr').AsInteger := ClientMSB;
cdsBusMonitor.FieldByName('Time').AsString := TimeToStr(Now);
cdsBusMonitor.FieldByName('Origin').AsString := 'Van de Bus: '{ + UserPeerIP};
cdsBusMonitor.Post;
except
cdsBusMonitor.Cancel;
raise;
end;
end;
if ProgramState = psNone then
begin
CodeSite.Send('New situation...');
try
DeviceDataLock.Enter;
try
TmpDeviceLSB := DeviceLSB;
TmpDeviceMSB := DeviceMSB;
finally
DeviceDataLock.Leave;
end;
if cdsIncoming.Locate('Group;Address', VarArrayOf([IntToStr(TmpDeviceMSB),IntToStr(TmpDeviceLSB)]), []) then
begin
CodeSite.Send('After locate...');
if cdsMemberState.Locate('MemberID', cdsIncoming.FieldByName('MemberID').AsString, []) then
cdsMemberState.Edit
else
cdsMemberState.Append;
try
TmpCodeValue := CodeValue;
if cdsMemberStateState.AsString = TmpCodeValue then
begin
CodeSite.Send('New state ' + TmpCodeValue + ' already known');
cdsMemberState.Cancel;
end
else
begin
CodeSite.Send('New state ' + TmpCodeValue);
cdsMemberStateState.AsString := TmpCodeValue;
if TmpCodeValue = 'Reset' then
cdsMemberStateTotaal.AsFloat := cdsMemberStateTotaal.AsFloat + (Now - cdsMemberStateDateTime.AsDateTime);
cdsMemberStateDateTime.AsDateTime := Now;
cdsMemberState.Post;
end;
except
cdsMemberState.Cancel;
raise;
end;
end
else
CodeSite.SendError('ServerMethodsBServer.cdsIncoming Locate Fail');
except
on E: Exception do
CodeSite.SendException(E);
end
end;
if ((cdsBusMonitor.RecordCount mod 100) = 0) or ((Now - LastSave) > (1/24/60)) then
begin
LastSave := Now;
cdsBusMonitor.MergeChangeLog;
cdsBusMonitor.SaveToFile;
cdsMemberState.MergeChangeLog;
cdsMemberState.SaveToFile;
end;
end;
What is the way to implement Autocomplete or Autosuggest with Delphi/Firemonkey for Windows/Android platforms as well as MacOS and iOS?
Example
When user types text in Google search box - some quick suggestions are shown.
There are lots of implementations for VCL with IAutoComplete, but there are less for FMX. What is needed is - FMX
I've made some research and compiled from different sources what I got below.
I've tested this on XE7/XE8 with Firemonkey. Perfectly runnig on Win32, Android and pretty sure MacOS.
I used to call suggestions within a timer, but the code below comes without a timer. The procedure to call in a timer or a thread is TStyledSuggestEdit.DropDownRecalc.
unit FMX.Edit.Suggest2;
interface
uses
FMX.Edit.Style, FMX.Controls.Presentation, FMX.Controls.Model, FMX.Presentation.Messages, FMX.Edit,
FMX.Controls, FMX.ListBox, System.Classes, System.Types;
const
PM_DROP_DOWN = PM_EDIT_USER + 10;
PM_PRESSENTER = PM_EDIT_USER + 11;
PM_SET_ITEMINDEX = PM_EDIT_USER + 12;
PM_GET_ITEMINDEX = PM_EDIT_USER + 13;
PM_GET_SELECTEDITEM = PM_EDIT_USER + 14;
PM_SET_ITEMCHANGE_EVENT = PM_EDIT_USER + 15;
PM_GET_ITEMS = PM_EDIT_USER + 16;
type
TSelectedItem = record
Text: String;
Data: TObject;
end;
TStyledSuggestEdit = class(TStyledEdit)
private
FItems: TStrings;
FPopup: TPopup;
FListBox: TListBox;
FDropDownCount: Integer;
FOnItemChange: TNotifyEvent;
FItemIndex: integer;
FDontTrack: Boolean;
FLastClickedIndex: Integer;
function _GetIndex: Integer;
procedure _SetIndex(const Value: Integer);
procedure _SetItems(const Value: TStrings);
protected
procedure CheckIfTextMatchesSuggestions; // used to find out if a typed text matches any of suggestions and then do select
function GetListBoxIndexByText(const AText: string): Integer;
procedure OnItemClick(const Sender: TCustomListBox; const Item: TListBoxItem);
procedure PMDropDown(var AMessage: TDispatchMessage); message PM_DROP_DOWN;
procedure MMDataChanged(var AMessage: TDispatchMessageWithValue<TDataRecord>); message MM_DATA_CHANGED;
procedure PMSetSize(var AMessage: TDispatchMessageWithValue<TSizeF>); message PM_SET_SIZE;
procedure PMGetItemIndex(var AMessage: TDispatchMessageWithValue<Integer>); message PM_GET_ITEMINDEX;
procedure PMSetItemIndex(var AMessage: TDispatchMessageWithValue<Integer>); message PM_SET_ITEMINDEX;
procedure PMGetItems(var AMessage: TDispatchMessageWithValue<TStrings>); message PM_GET_ITEMS;
procedure PMGetSelectedItem(var AMEssage: TDispatchMessageWithValue<TSelectedItem>); message PM_GET_SELECTEDITEM;
procedure PMSetItemChangeEvent(var AMessage: TDispatchMessageWithValue<TNotifyEvent>); message PM_SET_ITEMCHANGE_EVENT;
procedure PMPressEnter(var AMessage: TDispatchMessage); message PM_PRESSENTER;
procedure DoChangeTracking; override;
procedure RebuildSuggestionList(AText: String);
procedure RecalculatePopupHeight;
procedure KeyDown(var Key: Word; var KeyChar: Char; Shift: TShiftState); override;
procedure DropDownRecalc(ByText: string; Delay: integer = 100); //Delay parameter is a preparation for calling by a thread or a timer
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function _SelectedItem: TSelectedItem;
property _Items: TStrings read FItems write _SetItems;
property _ItemIndex: Integer read _GetIndex write _SetIndex;
property _OnItemChange: TNotifyEvent read FOnItemChange write FOnItemChange;
end;
TStyleSuggestEditProxy = class(TPresentationProxy)
protected
function CreateReceiver: TObject; override;
end;
TEditSuggestHelper = class helper for TEdit
public type
private
function GetIndex: Integer;
procedure SetIndex(const Value: Integer);
procedure SetOnItemChange(const Value: TNotifyEvent);
function GetItems: TStrings;
public
procedure AssignItems(const S: TStrings);
procedure ForceDropDown;
procedure PressEnter;
function SelectedItem: TSelectedItem;
property OnItemChange: TNotifyEvent write SetOnItemChange;
property ItemIndex: Integer read GetIndex write SetIndex;
property Items: TStrings read GetItems;
end;
implementation
uses
FMX.Presentation.Factory, FMX.Types, System.SysUtils, System.Math, System.Rtti, uDsTimers.FMX, {$IFDEF MSWINDOWS}Winapi.Windows,{$ENDIF}
System.UITypes;
{ TStyleSuggestEditProxy }
function TStyleSuggestEditProxy.CreateReceiver: TObject;
begin
Result := TStyledSuggestEdit.Create(nil);
end;
{ TStyledSuggestEdit }
procedure TStyledSuggestEdit.CheckIfTextMatchesSuggestions;
var I: integer;
begin
if FItemIndex = -1 then
begin
I := self.GetListBoxIndexByText(Edit.Text);
if I <> -1 then
try
OnItemClick(nil, FListBox.ListItems[I]); //try-except: maybe missing items if calling from a timer event or within a thread
FListBox.RemoveObject(FListBox.ListItems[I]);
RecalculatePopupHeight;
except
end;
end;
end;
constructor TStyledSuggestEdit.Create(AOwner: TComponent);
begin
inherited;
FItems := TStringList.Create;
FItemIndex := -1;
FPopup := TPopup.Create(self);
FPopup.Parent := Self;
FPopup.PlacementTarget := Self;
FPopup.Placement := TPlacement.Bottom;
FPopup.Width := Width;
FListBox := TListBox.Create(self);
FListBox.Parent := FPopup;
FListBox.Align := TAlignLayout.Client;
FListBox.OnItemClick := OnItemClick;
FDropDownCount := 5;
FListBox.Width := Self.Width;
FPopup.Width := Self.Width;
FLastClickedIndex := -1;
end;
destructor TStyledSuggestEdit.Destroy;
begin
FPopup := nil;
FListBox := nil;
FItems.Free;
inherited;
end;
procedure TStyledSuggestEdit.DoChangeTracking;
begin
inherited;
if Edit.Text <> _SelectedItem.Text then
FLastClickedIndex := -1;
if not FDontTrack and (FLastClickedIndex = -1) then
begin
_ItemIndex := -1;
DropDownRecalc(Edit.Text);
end;
end;
function TStyledSuggestEdit.GetListBoxIndexByText(const AText: string): Integer;
begin
for Result := 0 to FListBox.Count - 1 do
if FListBox.ListItems[Result].Text.ToLower = AText.ToLower then
Exit;
Result := -1;
end;
function TStyledSuggestEdit._GetIndex: Integer;
begin
Result := FItemIndex;
end;
procedure TStyledSuggestEdit.KeyDown(var Key: Word; var KeyChar: Char; Shift: TShiftState);
begin
inherited;
case Key of
vkReturn:
if FListBox.Selected <> nil then
begin
OnItemClick(FListBox, FListBox.Selected);
end;
vkEscape: FPopup.IsOpen := False;
vkDown: begin
if FListBox.Selected <> nil then
FListBox.ItemIndex := Min(FListBox.Count - 1, FListBox.ItemIndex + 1)
else
if FListBox.Count > 0 then
FListBox.ItemIndex := 0;
end;
vkUp: begin
if FListBox.Selected <> nil then
FListBox.ItemIndex := Max(0, FListBox.ItemIndex - 1);
end;
end;
if Assigned(OnKeyDown) then
OnKeyDown(Edit, Key, KeyChar, Shift);
end;
procedure TStyledSuggestEdit.MMDataChanged(var AMessage: TDispatchMessageWithValue<TDataRecord>);
var
Data: TDataRecord;
begin
Data := AMessage.Value;
if Data.Value.IsType <TStrings> and (Data.Key = 'Suggestions') then
FItems.Assign(Data.Value.AsType<TStrings>)
end;
procedure TStyledSuggestEdit.OnItemClick(const Sender: TCustomListBox; const Item: TListBoxItem);
begin
FLastClickedIndex := Item.Tag;
_ItemIndex := Item.Tag;
FPopup.IsOpen := Sender = nil; // whenever OnItemClick is being called programmatically `Sender` must be passed as `nil`,
Edit.SetFocus; // otherwise considered as real-user-click and should close popup
end;
procedure TStyledSuggestEdit.PMPressEnter(var AMessage: TDispatchMessage);
var K: word; KC: Char;
begin
K := vkReturn;
KC := #13;
KeyDown(K, KC, []);
end;
procedure TStyledSuggestEdit.PMDropDown(var AMessage: TDispatchMessage);
begin
inherited;
DropDownRecalc('',10);
end;
procedure TStyledSuggestEdit.PMGetItemIndex(var AMessage: TDispatchMessageWithValue<Integer>);
begin
AMessage.Value := self._ItemIndex;
end;
procedure TStyledSuggestEdit.PMGetItems(var AMessage: TDispatchMessageWithValue<TStrings>);
begin
AMessage.Value := Self._Items;
end;
procedure TStyledSuggestEdit.PMGetSelectedItem(var AMEssage: TDispatchMessageWithValue<TSelectedItem>);
begin
AMEssage.Value := self._SelectedItem;
end;
procedure TStyledSuggestEdit.PMSetItemChangeEvent(var AMessage: TDispatchMessageWithValue<TNotifyEvent>);
begin
FOnItemChange := AMessage.Value;
end;
procedure TStyledSuggestEdit.PMSetItemIndex(var AMessage: TDispatchMessageWithValue<Integer>);
begin
self._ItemIndex := AMessage.Value;
end;
procedure TStyledSuggestEdit.PMSetSize(var AMessage: TDispatchMessageWithValue<TSizeF>);
begin
inherited;
FPopup.Width := Width;
end;
procedure TStyledSuggestEdit.RebuildSuggestionList(AText: String);
var
i: integer;
Word: string;
begin
FListBox.Clear;
FListBox.BeginUpdate;
AText := AText.ToLower;
try
for i := 0 to FItems.Count - 1 do
if AText.IsEmpty or FItems[i].ToLower.StartsWith(AText) then
begin
FListBox.AddObject(TListBoxItem.Create(FListBox));
FListBox.ListItems[FListBox.Count - 1].Tag := I;
FListBox.ListItems[FListBox.Count - 1].Data := FItems.Objects[i];
FListBox.ListItems[FListBox.Count - 1].Text := FItems[i];
end;
finally
FListBox.EndUpdate;
end;
end;
procedure TStyledSuggestEdit.RecalculatePopupHeight;
begin
if FListBox.Items.Count > 0 then
begin
FPopup.Height := FListBox.ListItems[0].Height * Min(FDropDownCount, FListBox.Items.Count) + FListBox.BorderHeight;
FPopup.PopupFormSize := TSizeF.Create(FPopup.Width, FPopup.Height);
end
else
begin
FPopup.Height := 1; // instead this it's possible to hide FPopup.IsOpen := false;
FPopup.PopupFormSize := TSizeF.Create(FPopup.Width, FPopup.Height);
end;
end;
function TStyledSuggestEdit._SelectedItem: TSelectedItem;
begin
if FItemIndex = -1 then
begin
Result.Text := '';
Result.Data := nil;
end
else
begin
Result.Text := FItems[FItemIndex];
Result.Data := FItems.Objects[FItemIndex];
end;
end;
procedure TStyledSuggestEdit._SetIndex(const Value: Integer);
begin
if (Value >= -1) and (Value < FItems.Count) and (Value <> FItemIndex) then
begin
FDontTrack := true;
FItemIndex := Value;
if (FItemIndex >= 0) and (Edit.Text <> _SelectedItem.Text) then
begin
Edit.Text := _SelectedItem.Text;
Edit.GoToTextEnd;
end;
if Assigned(FOnItemChange) then
FOnItemChange(Edit);
FDontTrack := false;
end;
end;
procedure TStyledSuggestEdit._SetItems(const Value: TStrings);
begin
FItems := Value;
_ItemIndex := -1;
end;
procedure TStyledSuggestEdit.DropDownRecalc(ByText: string; Delay: integer);
begin
// Here is possible to use a timer call or a call in a thread;
if not self.FDontTrack then
begin
Self.RebuildSuggestionList(ByText);
Self.RecalculatePopupHeight;
self.FPopup.IsOpen := self.FListBox.Items.Count > 0;
CheckIfTextMatchesSuggestions;
end;
end;
{ TEditHelper }
procedure TEditSuggestHelper.PressEnter;
begin
if HasPresentationProxy then
PresentationProxy.SendMessage(PM_PRESSENTER);
end;
function TEditSuggestHelper.SelectedItem: TSelectedItem;
begin
if HasPresentationProxy then
PresentationProxy.SendMessageWithResult<TSelectedItem>(PM_GET_SELECTEDITEM, Result);
end;
procedure TEditSuggestHelper.SetIndex(const Value: Integer);
begin
if HasPresentationProxy then
PresentationProxy.SendMessage<Integer>(PM_SET_ITEMINDEX, Value);
end;
procedure TEditSuggestHelper.SetOnItemChange(const Value: TNotifyEvent);
begin
if HasPresentationProxy then
PresentationProxy.SendMessage<TNotifyEvent>(PM_SET_ITEMCHANGE_EVENT, Value);
end;
procedure TEditSuggestHelper.ForceDropDown;
begin
if HasPresentationProxy then
PresentationProxy.SendMessage(PM_DROP_DOWN);
end;
function TEditSuggestHelper.GetIndex: Integer;
begin
if HasPresentationProxy then
PresentationProxy.SendMessageWithResult<Integer>(PM_GET_ITEMINDEX, Result);
end;
function TEditSuggestHelper.GetItems: TStrings;
begin
if HasPresentationProxy then
PresentationProxy.SendMessageWithResult<TStrings>(PM_GET_ITEMS, Result);
end;
procedure TEditSuggestHelper.AssignItems(const S: TStrings);
begin
self.Model.Data['Suggestions'] := TValue.From<TStrings>(S);
end;
initialization
TPresentationProxyFactory.Current.Register('SuggestEditStyle', TStyleSuggestEditProxy);
finalization
TPresentationProxyFactory.Current.Unregister('SuggestEditStyle');
end.
Here is how you use it:
Create Multi-Device application
On a HD form place common TEdit component
Define for TEdit.OnPresentationNameChoosing on Events tab the following:
procedure TForm1.Edit1PresentationNameChoosing(Sender: TObject; var PresenterName: string);
begin
inherited;
PresenterName := 'SuggestEditStyle';
end;
Add items to your sl: TStrings by: sl.AddObject('Name', TIntObj.Create(10));
Assign sl: TStrings to your Edit by: Edit1.AssignItems(sl);
Comment out TStyledSuggestEdit.CheckIfTextMatchesSuggestions in the code if you don't need Autoselect ability while typing.
Test Form1
Form reference
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 325
ClientWidth = 225
FormFactor.Width = 320
FormFactor.Height = 480
FormFactor.Devices = [Desktop]
OnCreate = FormCreate
DesignerMasterStyle = 0
object Edit1: TEdit
Touch.InteractiveGestures = [LongTap, DoubleTap]
Align = Top
TabOrder = 0
OnPresentationNameChoosing = Edit1PresentationNameChoosing
Position.X = 20.000000000000000000
Position.Y = 57.000000000000000000
Margins.Left = 20.000000000000000000
Margins.Right = 20.000000000000000000
Size.Width = 185.000000000000000000
Size.Height = 22.000000000000000000
Size.PlatformDefault = False
object Button2: TButton
Align = Right
Cursor = crArrow
Margins.Left = 1.000000000000000000
Margins.Top = 1.000000000000000000
Margins.Right = 1.000000000000000000
Margins.Bottom = 1.000000000000000000
Position.X = 156.500000000000000000
Position.Y = 0.500000000000000000
Scale.X = 0.500000000000000000
Scale.Y = 0.500000000000000000
Size.Width = 56.000000000000000000
Size.Height = 42.000000000000000000
Size.PlatformDefault = False
StyleLookup = 'arrowdowntoolbutton'
TabOrder = 0
Text = 'Button2'
OnClick = Button2Click
end
end
object Button1: TButton
Align = Top
Margins.Left = 30.000000000000000000
Margins.Top = 10.000000000000000000
Margins.Right = 30.000000000000000000
Position.X = 30.000000000000000000
Position.Y = 89.000000000000000000
Size.Width = 165.000000000000000000
Size.Height = 22.000000000000000000
Size.PlatformDefault = False
TabOrder = 1
Text = 'Set 3rd item'
OnClick = Button1Click
end
object Label1: TLabel
Align = Top
Size.Width = 225.000000000000000000
Size.Height = 57.000000000000000000
Size.PlatformDefault = False
Text = 'Label1'
end
end
Code reference
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Controls.Presentation, FMX.Edit, FMX.Edit.Suggest2, FMX.Layouts, FMX.ListBox,
FMX.StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Edit1: TEdit;
Button2: TButton;
Label1: TLabel;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure esItemChange(Sender: TObject);
procedure Edit1PresentationNameChoosing(Sender: TObject; var PresenterName: string);
procedure Button2Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
sl: TStrings;
implementation
{$R *.fmx}
type
TIntObj = class(TObject)
private
FId: integer;
public
constructor Create(Id: integer); overload;
function Value: integer;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Edit1.ItemIndex := 3; // force choice as if it was combobox behaviour
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
Edit1.ForceDropDown; // add a button inside TEdit and use it as dropdown
end;
procedure TForm1.Edit1PresentationNameChoosing(Sender: TObject; var PresenterName: string);
begin
inherited;
PresenterName := 'SuggestEditStyle';
end;
procedure TForm1.esItemChange(Sender: TObject);
begin
// occurs when ItemIndex is changed
Label1.Text := TEdit(Sender).SelectedItem.Text + LineFeed + 'idx=' + TEdit(Sender).ItemIndex.ToString + LineFeed + 'data=';
if TEdit(Sender).SelectedItem.Data <> nil then
Label1.Text := Label1.Text + TIntObj(TEdit(Sender).SelectedItem.Data).Value.ToString
else
Label1.Text := Label1.Text + 'nil';
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
sl := TStringList.Create;
//sl.AddObject('aaa',10); // Segmentation fault 11 under Android
sl.AddObject('aaa',TIntObj.Create(10));
sl.AddObject('aaabb',TIntObj.Create(20));
sl.AddObject('aaabbbcc',TIntObj.Create(30));
sl.AddObject('aaacc',TIntObj.Create(40));
sl.AddObject('aaafff',TIntObj.Create(50));
sl.AddObject('aaaggg',TIntObj.Create(60));
Edit1.AssignItems(sl);
Edit1.OnItemChange := esItemChange;
end;
{ TIntObject }
constructor TIntObj.Create(Id: integer);
begin
inherited Create;
FId := Id;
end;
function TIntObj.Value: integer;
begin
Result := FId;
end;
end.
Tested Win32 [Windows 7/8] and Android 4.4.4 device [MI3W]
Hope this helps. Any further ideas and suggestions are appreciated.
In the previous answer for Delphi XE10 change line
Result := TStyledSuggestEdit.Create(nil);
to
Result := TStyledSuggestEdit.Create(nil, Model, PresentedControl);
in the function TStyleSuggestEditProxy.CreateReceiver: TObject;
Plus change Data.Key = 'Suggestions' to Data.Key = 'suggestions' in the TStyledSuggestEdit.MMDataChanged
For iOS (I did not check on Android, but should also work) set ControlType of TMemo or TEdit to Platform - this will show T9 autocomplete and check spelling.
I have a problem to sync the GUI of server. I'm using Delphi 2007 and Indy 10.1.5.
This is my case:
Server send to all connected client a hearbit (this is the message send from server --> "REQ|HeartBit")
Client response to server with "I'm alive" (this is the message send from client --> "ANS|USERNAME|I'm alive"
In onExecute procedure of the TIdTCPServer I want to see the answer of the client in a TlistView of server, so I have done like in this Link
When I start my application with two process client connected (that are runs in my PC) and send a hearbit message to clients, I see in the listview of server this situation:
REQ|HeartBit (send to Client1)
REQ|HeartBit (send to Client2)
ANS|Client2|I'm Alive
ANS|Client2|I'm Alive
two response message from Client2 (!?!?)
Where is my mistake?
Sorry for my poor English.
Thanks
The code of server side is this:
type
TLog = class(TIdSync)
private
FMsg : string;
protected
procedure DoSynchronize; override;
public
constructor Create(const AMsg: String);
//class procedure AddMsg(const AMsg: String);
end;
// procedure that add items in listview of server
procedure WriteListLog(aTimeStamp : TDateTime;strMessaggio: String);
implementation
procedure TLog.DoSynchronize;
begin
WriteListLog(Now,FMsg);
end
procedure TForm1.tsExecute(AContext: TIdContext);
var
Ctx: TMyContext;
tmp : String;
sync : Tlog;
begin
Ctx := TMyContext(AContext);
tmp := Ctx.Connection.IOHandler.ReadLn;
sync := Tlog.Create(tmp);
try
sync.FMsg := tmp;
sync.Synchronize;
finally
Sync.Free;
end;
end;
If I add lockList in OnExecute I have this correct sequence of message
REQ|HeartBit (send to Client1)
REQ|HeartBit (send to Client2)
ANS|Client1|I'm Alive
ANS|Client2|I'm Alive
Is it Correct?
procedure TForm1.tsExecute(AContext: TIdContext);
var
Ctx: TMyContext;
tmp : String;
sync : Tlog;
begin
Ctx := TMyContext(AContext);
tmp := Ctx.Connection.IOHandler.ReadLn;
Ctx.FContextList.LockList;
try
sync := Tlog.Create(tmp);
try
sync.FMsg := tmp;
sync.Synchronize;
finally
Sync.Free;
end;
finally
Ctx.FContextList.UnlockList;
end;
end;
Update
In my project, the listView and WriteListLog() is in the unit FLogMsg, not in the same unit of the IdTCSPServer.
This is how is defined the tlistview in dfm
object ListLog: TListView
Left = 0
Top = 0
Width = 737
Height = 189
Align = alClient
Columns = <
item
Caption = 'Data'
Width = 140
end
item
Caption = 'Da'
end
item
Caption = 'A'
end
item
Caption = 'Tipo'
end
item
Caption = 'Messaggio'
Width = 900
end>
ColumnClick = False
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
FlatScrollBars = True
OwnerData = True
ReadOnly = True
ParentFont = False
TabOrder = 0
ViewStyle = vsReport
OnData = ListLogData
end
Code of unit FlogMsg:
type
TTipoMessaggio = (tmSend,tmReceived,tmSystem);
TDataItem = class
private
FDITimeStamp: TDateTime;
FDIRecipient: String;
FDISender: String;
FDITipo: TTipoMessaggio;
FDIMessaggio: String;
public
property DITimeStamp: TDateTime read FDITimeStamp;
property DISender : String read FDISender;
property DIRecipient : String read FDIRecipient;
property DITipo : TTipoMessaggio read FDITipo;
property DIMessaggio: String read FDIMessaggio;
end;
TfrmLog = class(TForm)
ListLog: TListView;
Panel1: TPanel;
procedure FormCreate(Sender: TObject);
procedure ListLogData(Sender: TObject; Item: TListItem);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
FItems: TObjectList;
FActiveItems: TList;
FFilterLogStation: String;
procedure SetFilterLogStation(const Value: String);
public
{ Public declarations }
property FilterLogStation : String read FFilterLogStation write SetFilterLogStation;
end;
procedure WriteListLog(aTimeStamp : TDateTime;
aSender,aRecipient: String;aTipo:TTipoMessaggio;strMessaggio: String);
var
frmLog: TfrmLog;
implementation
{$R *.dfm}
procedure WriteListLog(aTimeStamp : TDateTime;
aSender,aRecipient: String;aTipo:TTipoMessaggio;strMessaggio: String);
var
DataItem: TDataItem;
begin
DataItem := TDataItem.Create;
try
DataItem.FDITimeStamp := aTimeStamp;
DataItem.FDISender := aSender;
DataItem.FDIRecipient := aRecipient;
DataItem.FDITipo := aTipo;
DataItem.FDIMessaggio := strMessaggio;
frmLog.FItems.Add(DataItem);
if (frmLog.FilterLogStation = '') or (frmLog.FilterLogStation = aRecipient) or
(frmLog.FilterLogStation = aSender) then
begin
frmLog.FActiveItems.Add(DataItem);
frmLog.ListLog.AddItem('',DataItem);
end;
except
DataItem.Free;
raise;
end;
frmLog.ListLog.Repaint;
end;
procedure TfrmLog.FormCreate(Sender: TObject);
begin
FFilterLogStation := '';
FItems := TObjectList.Create;
FActiveItems := TList.Create;
end;
procedure TfrmLog.FormDestroy(Sender: TObject);
begin
FActiveItems.clear;
FreeAndNil(FActiveItems);
FreeAndNil(FItems);
end;
procedure TfrmLog.ListLogData(Sender: TObject; Item: TListItem);
var
DataItem: TDataItem;
begin
DataItem := FActiveItems[Item.Index];
Item.Caption := FormatDateTime('dd/mm/yyy hh.nn.ss', DataItem.DITimeStamp);
Item.SubItems.Add(DataItem.DISender);
Item.SubItems.Add(DataItem.DIRecipient);
// Tipo Messaggio
case DataItem.DITipo of
tmSend: Item.SubItems.Add('Inviato');
tmReceived: Item.SubItems.Add('Ricevuto');
tmSystem: Item.SubItems.Add('Sistema');
end;
Item.SubItems.Add(DataItem.DIMessaggio);
Item.MakeVisible(true);
end;
procedure TfrmLog.SetFilterLogStation(const Value: String);
var
I: Integer;
begin
FFilterLogStation := Value;
ListLog.Items.BeginUpdate;
try
ListLog.Clear;
FActiveItems.Clear;
for I := 0 to FItems.Count - 1 do
if (CompareText(UpperCase(FFilterLogStation),UpperCase(TDataItem(FItems[I]).DISender)) = 0) or
(CompareText(UpperCase(FFilterLogStation),UpperCase(TDataItem(FItems[I]).DIRecipient)) = 0)
or (FFilterLogStation = '') then
begin
FActiveItems.Add(FItems[I]);
end;
ListLog.Items.Count := FActiveItems.Count;
finally
ListLog.Items.EndUpdate;
ListLog.Repaint;
end;
end;
procedure TfrmLog.FormDestroy(Sender: TObject);
begin
FActiveItems.clear;
FreeAndNil(FActiveItems);
FreeAndNil(FItems);
end;
UPDATE 2 - Try with TMemo
this is the result:
(First SendBroadCast HeartBit)
ANS|CARICO1|I'm Alive
ANS|CARICO2|I'm Alive
(Second SendBroadCast HeartBit)
ANS|CARICO1|I'm Alive
ANS|CARICO2|I'm Alive
(Third SendBroadCast HeartBit)
ANS|CARICO1|I'm Alive
ANS|CARICO1|I'm Alive
I add a TStringList variable in my TMyContext class.
In debug session, for each Context, if I inspect the queue of message that is saved on my TStringList variable the messages are correct!
So, I think that the problem is in Synchronize...
type
TTipoStazione = (tsNone,tsCarico,tsScarico);
TLog = class(TIdSync)
private
FMsg : string;
FFrom : String;
protected
procedure DoSynchronize; override;
public
end;
TMyContext = class(TIdContext)
public
IP: String;
UserName: String;
Stazione : Integer;
tipStaz : TTipoStazione;
Con: TDateTime;
isValid : Boolean;
ls : TStringList;
// compname:string;
procedure ProcessMsg;
end;
TForm1 = class(TForm)
ts: TIdTCPServer;
Memo1: TMemo;
btconnect: TButton;
edport: TEdit;
Button2: TButton;
procedure btconnectClick(Sender: TObject);
procedure tsConnect(AContext: TIdContext);
procedure tsExecute(AContext: TIdContext);
procedure tsDisconnect(AContext: TIdContext);
constructor Create(AOwner: TComponent);override;
procedure FormDestroy(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
{ Private declarations }
procedure SendMsgBroadcast(aMsg : String);
public
{ Public declarations }
procedure MyWriteListLog(strMessaggio : String);
end;
implementation
constructor TLog.Create(const aFrom: String; const AMsg: String);
begin
inherited Create;
FMsg := AMsg;
FFrom := aFrom;
end;
procedure TLog.DoSynchronize;
begin
Form1.MyWriteListLog(FMsg);
end;
procedure TMyContext.ProcessMsg;
var
str,TypeMsg:string;
myTLog: TLog;
begin
if Connection.IOHandler.InputBufferIsEmpty then
exit;
str:=self.Connection.IOHandler.ReadLn;
ls.Add('1='+str);
myTLog := Tlog.Create;
try
myTLog.FMsg := str;
myTLog.FFrom := UserName;
myTLog.Synchronize;
ls.Add('2='+str);
finally
myTLog.Free;
end;
end;
constructor TForm1.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ts.ContextClass := TMyContext;
DMVern := TDMVern.Create(nil);
end;
procedure TForm1.btconnectClick(Sender: TObject);
begin
ts.DefaultPort:=strtoint(edport.Text);
ts.Active:=true;
MyWriteListLog('Listening');
end;
procedure TForm1.tsConnect(AContext: TIdContext);
var
strErr : String;
I: Integer;
tmpNrStaz: String;
tmpMsg : String;
begin
strErr := '';
ts.Contexts.LockList;
try
with TMyContext(AContext) do
begin
ls := TStringList.Create;
isValid := false;
Con := Now;
if (Connection.Socket <> nil) then
IP :=Connection.Socket.Binding.PeerIP;
tmpMsg := Connection.IOHandler.ReadLn;
try
if not (Pos('START|',tmpMsg) > 0) then
begin
strErr := 'Comando non valido';
exit;
end;
UserName := Copy(tmpMsg,Length('START|')+1,Length(tmpMsg));
if Trim(UserName) = '' then
begin
strErr := 'How Are You?';
exit;
end;
tipStaz := tsNone;
if UpperCase(Copy(UserName,1,6)) = 'CARICO' then
tipStaz := tsCarico
else if UpperCase(Copy(UserName,1,7)) = 'SCARICO' then
tipStaz := tsCarico;
if tipStaz = tsNone then
begin
strErr := 'Tipo Stazione non valida.';
exit;
end;
tmpNrStaz := '';
for I := Length(UserName) downto 1 do
begin
if (UserName[i] in ['0'..'9']) then
tmpNrStaz:= UserName[i] + tmpNrStaz
else if tmpNrStaz <> '' then
break;
end;
if tmpNrStaz = '' then
begin
strErr := 'Numero Stazione non specificato.';
exit;
end;
Stazione := StrToInt(tmpNrStaz);
isValid := true;
tmpMsg := 'HELLO|' + UserName;
Connection.IOHandler.WriteLn(tmpMsg);
finally
if strErr <> '' then
begin
Connection.IOHandler.WriteLn(strErr);
Connection.Disconnect;
end;
end;
end;
finally
ts.Contexts.UnlockList;
end;
end;
procedure TForm1.tsExecute(AContext: TIdContext);
var
Ctx: TMyContext;
tmp : String;
begin
Ctx := TMyContext(AContext);
Ctx.ProcessMsg;
end;
procedure TForm1.tsDisconnect(AContext: TIdContext);
begin
TMyContext(AContext).ProcessMsg;
end;
procedure TForm1.MyWriteListLog(strMessaggio: String);
begin
Memo1.Lines.Add(strMessaggio);
end;
procedure TForm1.Button2Click(Sender: TObject);
var
aMsg: String;
begin
aMsg := 'REQ|HeartBit';
SendMsgBroadcast(aMsg);
end;
procedure TForm1.SendMsgBroadcast(aMsg: String);
var
List: TList;
I: Integer;
Context: TMyContext;
begin
List := ts.Contexts.LockList;
try
for I := 0 to List.Count-1 do
begin
Context := TMyContext(List[I]);
if Context.isValid then
begin
try
Context.Connection.IOHandler.WriteLn(aMsg);
except
end;
end;
end;
finally
ts.Contexts.UnlockList;
end;
end;
You are using a virtual ListView, but I see two mistakes you are making with it:
You are calling AddItem() and Clear() on it. Do not do that. The whole point of a virtual ListView is to not put any real data in it at all. After you add or remove objects in your FActiveItems list, all you have to do is update the TListView.Items.Count property to reflect the new item count. It will invalidate itself by default to trigger a repaint (but if you want to trigger a repaint manually, use Invalidate() instead of Repaint(), and call it only when you have done something to modify FActiveItems).
Your OnData handler is calling TListItem.MakeVisible(). That call does not belong in that event, it belongs in WriteListLog() instead. OnData triggered whenever the ListView needs data for an item for any reason, including during drawing. Don't perform any UI management operations in a data management event.
Try this instead:
procedure WriteListLog(aTimeStamp : TDateTime;
aSender,aRecipient: String;aTipo:TTipoMessaggio;strMessaggio: String);
var
DataItem: TDataItem;
Index, ActiveIndex: Integer;
begin
DataItem := TDataItem.Create;
try
DataItem.FDITimeStamp := aTimeStamp;
DataItem.FDISender := aSender;
DataItem.FDIRecipient := aRecipient;
DataItem.FDITipo := aTipo;
DataItem.FDIMessaggio := strMessaggio;
Index := frmLog.FItems.Add(DataItem);
try
if (frmLog.FilterLogStation = '') or
AnsiSameText(frmLog.FilterLogStation, aRecipient) or
AnsiSameText(frmLog.FilterLogStation, aSender) then
begin
ActiveIndex := frmLog.FActiveItems.Add(DataItem);
frmLog.ListLog.Items.Count := frmLog.FActiveItems.Count;
frmLog.Items[ActiveIndex].MakeVisible(true);
end;
except
frmLog.FItems.Delete(Index);
DataItem := nil;
raise;
end;
except
DataItem.Free;
raise;
end;
end;
procedure TfrmLog.FormCreate(Sender: TObject);
begin
FFilterLogStation := '';
FItems := TObjectList.Create(True);
FActiveItems := TList.Create;
end;
procedure TfrmLog.FormDestroy(Sender: TObject);
begin
FItems.Free;
FActiveItems.Free;
end;
procedure TfrmLog.ListLogData(Sender: TObject; Item: TListItem);
var
DataItem: TDataItem;
begin
DataItem := TDataItem(FActiveItems[Item.Index]);
Item.Caption := FormatDateTime('dd/mm/yyy hh.nn.ss', DataItem.DITimeStamp);
Item.SubItems.Add(DataItem.DISender);
Item.SubItems.Add(DataItem.DIRecipient);
// Tipo Messaggio
case DataItem.DITipo of
tmSend: Item.SubItems.Add('Inviato');
tmReceived: Item.SubItems.Add('Ricevuto');
tmSystem: Item.SubItems.Add('Sistema');
else
Item.SubItems.add('');
end;
Item.SubItems.Add(DataItem.DIMessaggio);
end;
procedure TfrmLog.SetFilterLogStation(const Value: String);
var
I: Integer;
DataItem: TDataItem;
begin
if FFilterLogStation = Value then Exit;
ListLog.Items.Count := 0;
FActiveItems.Clear;
FFilterLogStation := Value;
try
for I := 0 to FItems.Count - 1 do
begin
DataItem := TDataItem(FItems[I]);
if (FFilterLogStation = '') or
AnsiSameText(FFilterLogStation, DataItem.DISender) or
AnsiSameText(FFilterLogStation), DataItem.DIRecipient) then
begin
FActiveItems.Add(DataItem);
end;
end;
finally
ListLog.Items.Count := FActiveItems.Count;
end;
end;
Could any one tell me how can I add characters one by one from a text file to a Memo?
The text file contains different paragraphs of texts. I want to add the characters of each paragraph one by one till the end of the paragraph. Then after 10 seconds delay the next paragraph to be shown in the Memo.
Thanks,
Sei
You would probably use a TTimer. Drop a TTimer, a TMemo and a TButton on your form. Then do
var
lines: TStringList;
pos: TPoint;
const
CHAR_INTERVAL = 75;
PARAGRAPH_INTERVAL = 1000;
procedure TForm6.Button1Click(Sender: TObject);
const
S_EMPTY_FILE = 'You are trying to display an empty file!';
begin
Memo1.ReadOnly := true;
Memo1.Clear;
Memo1.Lines.Add('');
pos := Point(0, 0);
if lines.Count = 0 then
raise Exception.Create(S_EMPTY_FILE);
while (pos.Y < lines.Count) and (length(lines[pos.Y]) = 0) do inc(pos.Y);
if pos.Y = lines.Count then
raise Exception.Create(S_EMPTY_FILE);
NextCharTimer.Enabled := true;
end;
procedure TForm6.FormCreate(Sender: TObject);
begin
lines := TStringList.Create;
lines.LoadFromFile('C:\Users\Andreas Rejbrand\Desktop\Test.txt');
end;
procedure TForm6.NextCharTimerTimer(Sender: TObject);
begin
NextCharTimer.Interval := CHAR_INTERVAL;
Memo1.Lines[Memo1.Lines.Count - 1] := Memo1.Lines[Memo1.Lines.Count - 1] + lines[pos.Y][pos.X + 1];
inc(pos.X);
if pos.X = length(lines[pos.Y]) then
begin
NextCharTimer.Interval := PARAGRAPH_INTERVAL;
pos.X := 0;
repeat
inc(pos.Y);
Memo1.Lines.Add('');
until (pos.Y = lines.Count) or (length(lines[pos.Y]) > 0);
end;
if pos.Y = lines.Count then
NextCharTimer.Enabled := false;
end;
A thread alternative to a timer. Tests a 'carriage return' in the file for a paragraph:
const
UM_MEMOCHAR = WM_USER + 22;
type
TForm1 = class(TForm)
Memo1: TMemo;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
procedure UMMemoChar(var Msg: TMessage); message UM_MEMOCHAR;
public
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
type
TCharSender = class(TThread)
private
FCharWait, FParWait: Integer;
FFormHandle: HWND;
FFS: TFileStream;
protected
procedure Execute; override;
public
constructor Create(FileName: string; CharWait, ParagraphWait: Integer;
FormHandle: HWND);
destructor Destroy; override;
end;
constructor TCharSender.Create(FileName: string; CharWait, ParagraphWait: Integer;
FormHandle: HWND);
begin
FCharWait := CharWait;
FParWait := ParagraphWait;
FFormHandle := FormHandle;
FFS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
FreeOnTerminate := True;
inherited Create(False);
end;
destructor TCharSender.Destroy;
begin
FFS.Free;
inherited;
end;
procedure TCharSender.Execute;
var
C: Char;
begin
while (FFS.Position < FFS.Size) and not Terminated do begin
FFS.Read(C, SizeOf(C));
if (C <> #10) then
PostMessage(FFormHandle, UM_MEMOCHAR, Ord(C), 0);
if C = #13 then
Sleep(FParWait)
else
Sleep(FCharWait);
end;
end;
{TForm1}
procedure TForm1.Button1Click(Sender: TObject);
begin
Memo1.Clear;
TCharSender.Create(
ExtractFilePath(Application.ExeName) + 'text.txt', 20, 1000, Handle);
end;
procedure TForm1.UMMemoChar(var Msg: TMessage);
begin
Memo1.SelStart := Memo1.Perform(WM_GETTEXTLENGTH, 0, 0);
Memo1.Perform(WM_CHAR, Msg.WParam, 0);
end;
There's lots of ways to do this, and I'm not sure how you intend to handle newlines. However, all routes lead to TMemo.Lines which is a TStrings instance that wraps up the windows messages needed to interact with the underlying Windows edit control.
For example, these routines should get you started.
procedure AddNewLine(Memo: TMemo);
begin
Memo.Lines.Add('');
end;
procedure AddCharacter(Memo: TMemo; const C: Char);
var
Lines: TStrings;
begin
Lines := Memo.Lines;
if Lines.Count=0 then
AddNewLine(Memo);
Lines[Lines.Count-1] := Lines[Lines.Count-1]+C;
end;
OK, here is the complete code for the Splashbar.pas, still have three progressbars, as I want to see what they look like before I choose one. It also includes some stuff that's disabled, as I can't get them to work.
unit Splashbar;
interface
uses ExtActns, Windows, SysUtils, Classes, Graphics, Forms, Controls, StdCtrls,
Buttons, ExtCtrls, AdvProgr, ComCtrls, NetAPI32, SHFolder, WinInet, ActnList,
AdvSmoothSplashScreen, AdvSmoothProgressBar, AdvSmoothProgressBarReg,
UTCT1b, GIFImg;
type
TSplashBar1 = class(TForm)
Bevel1: TBevel;
ProgressBar1: TProgressBar;
AdvProgress1: TAdvProgress;
Timer1: TTimer;
Label1: TLabel;
AdvSmoothProgressBar1: TAdvSmoothProgressBar;
ActionList1: TActionList;
DatabaseCopy: TAction;
procedure FormCreate(Sender: TObject);
procedure DatabaseCopyExecute(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private { Private declarations }
{ procedure URLOnDownloadProgress (Sender: TDownLoadURL;
Progress, ProgressMax: Cardinal;
StatusCode: TURLDownloadStatus;
StatusText: String; var Cancel: Boolean) ; }
public { Public declarations }
procedure OpenSplash;
procedure ShowProgress;
procedure CloseSplash;
end;
var
SplashBar1 : TSplashBar1;
dirpath: string;
Total: Integer;
Percent: Integer;
implementation
{$R *.dfm}
function GetSpecialFolderPath(folder : integer) : string;
const
SHGFP_TYPE_CURRENT = 0;
var
path: array [0..MAX_PATH] of char;
begin
if SUCCEEDED(SHGetFolderPath(0,folder,0,SHGFP_TYPE_CURRENT,#path[0])) then
Result := path
else
Result := '';
end;
function GetInetFile(const fileURL, FileName: String): boolean;
const BufferSize = 1024;
var
hSession, hURL: HInternet;
Buffer: array[1..BufferSize] of Byte;
BufferLen: DWORD;
f: File;
sAppName: string;
begin
Result:=False;
sAppName := ExtractFileName(Application.ExeName);
hSession := InternetOpen(PChar(sAppName),
INTERNET_OPEN_TYPE_PRECONFIG,
nil, nil, 0);
try
hURL := InternetOpenURL(hSession,
PChar(fileURL),
nil,0,0,0);
try
AssignFile(f, FileName);
Rewrite(f,1);
repeat
InternetReadFile(hURL, #Buffer,
SizeOf(Buffer), BufferLen);
BlockWrite(f, Buffer, BufferLen)
until BufferLen = 0;
CloseFile(f);
Result:=True;
finally
InternetCloseHandle(hURL)
end
finally
InternetCloseHandle(hSession)
end
end;
procedure TSplashBar1.FormCreate(Sender: TObject);
begin
Timer1.Enabled := True;
DatabaseCopy.Execute;
dirpath:=GetSpecialFolderPath($0023)+'\UTCT\';
end;
procedure TSplashBar1.DatabaseCopyExecute(Sender: TObject);
var
InternetFile,LocalFile: string;
begin
InternetFile:='http://160.14.20.20/log/Docs/test.xls';
LocalFile:=(dirpath + 'test.xls');
if GetInetFile(InternetFile,LocalFile)=True then
Label1.Caption := 'Working...';
//OnDownloadProgress := URLOnDownloadProgress;
//else
// StatusBar1.Panels[2].Text := 'MTable Offline!' ;
CopyFile(PChar(internetFile), PChar(LocalFile), False);
end;
procedure TSplashBar1.Timer1Timer(Sender: TObject);
const cnt: integer = 1;
begin
ProgressBar1.Position := cnt;
if cnt = 1 then Label1.Caption := 'Waiting...'
else
if cnt = 100 then begin
Label1.Caption := 'Done!';
Timer1.Enabled := False;
end
else begin
Label1.Caption := 'Working...';
end;
end;
procedure TSplashBar1.OpenSplash;
begin
Label1.Caption := '';
Show;
Update;
end;
procedure TSplashBar1.CloseSplash;
begin
Close;
end;
procedure TSplashBar1.ShowProgress;
var
xs: integer;
begin
Label1.caption:='';
Total := 1000;
for xs := 1 to Total do
begin
Sleep(5);
Percent := (xs * 100) div Total;
Label1.caption := StringOfChar('|', Percent) + IntToStr(Percent) + '%';
Label1.Repaint;
end;
end;
end.
// {procedure TSplashBar1.URLOnDownloadProgress;
// begin
// ProgressBar1.Max:= ProgressMax;
// ProgressBar1.Position:= Progress;
// AdvProgress1.Max:= ProgressMax;
// AdvProgress1.Position:= Progress;
// AdvSmoothProgressBar1.Position:= Progress;
//
// end; }
First error (W1056):
Make sure that
{$R *.RES}
is not entered twice in your dpr (Project|View Source)
Second error (H2077):
Somewhere else in your code, can't help with it
Third error (W1019):
You have to put
var
X: integer
right after
procedure TSplashBar1.ShowProgress;
You seem to have defined X somewhere other than the procedure, which the loop control variable error is indicating.
Did you include TSplashBar's unit in your .dpr file's 'uses' clause, and does it define a global SplashBar variable?