-5

How can I replace my noob code with nice loop ? I have 32 timers on form1 , every timer runs every 1 second and execute bat file, then waiting until bat file finishes his job and timer runs again.

enter image description here

enter image description here

Here is the code from image

procedure TForm1.Timer1Timer(Sender: TObject);
var nr:string;
begin
  nr := '1';
  Timer1.Enabled := False;

  if g_stop=False then
  begin
    if FileExists('test'+nr+'.bat') then
    begin
      ExeAndWait(ExtractFilePath(Application.ExeName) + 'test'+nr+'.bat', SW_SHOWNORMAL);
    end;
    Timer1.Enabled := True;
  end;
end;



procedure TForm1.Timer2Timer(Sender: TObject);
var nr:string;
begin
  nr := '2';
  Timer2.Enabled := False;

  if g_stop=False then
  begin
    if FileExists('test'+nr+'.bat') then
    begin
      ExeAndWait(ExtractFilePath(Application.ExeName) + 'test'+nr+'.bat', SW_SHOWNORMAL);
    end;
    Timer2.Enabled := True;
  end;
end;




procedure TForm1.Timer3Timer(Sender: TObject);
var nr:string;
begin
  nr := '3';
  Timer3.Enabled := False;

  if g_stop=False then
  begin
    if FileExists('test'+nr+'.bat') then
    begin
      ExeAndWait(ExtractFilePath(Application.ExeName) + 'test'+nr+'.bat', SW_SHOWNORMAL);
    end;
    Timer3.Enabled := True;
  end;
end;
jmp
  • 2,456
  • 3
  • 30
  • 47
  • all have to work in parallel of course – jmp Jan 14 '17 at 22:11
  • An image of your code is absolutely useless here. See [this Meta post](http://meta.stackoverflow.com/a/285557/62576) for a list of the many reasons that images of code are not acceptable. Also, your form image is useless as well; it provides nothing of benefit to the question (unless you're simply wanting to show off how neatly you arranged all those TTimer components). – Ken White Jan 14 '17 at 22:15
  • 3
    What problem are you trying to solve. This is not likely to be the solution. – David Heffernan Jan 14 '17 at 22:17
  • 4
    Please do realize that your code does not run in parallel, the timer events are fired in the main thread. – whosrdaddy Jan 14 '17 at 22:18
  • @David Heffernan read first string. The problem is: is there a shorten way. – jmp Jan 14 '17 at 22:27
  • There's surely a better way. Bit what is it? We need to know the problem. – David Heffernan Jan 14 '17 at 22:31
  • 3
    I'm curious what the batch files do... it may make a lot more sense to rewrite their functionality directly into the application itself. Or something else, perhaps. – J... Jan 14 '17 at 23:51

2 Answers2

1

Using a timer isn't the way to go here since you want to run lengthy operations in the background. The way you have it currently, running the batch files will block your user ui if the batch files take some time.

Better approach: Use explicit threads. Create your own thread class as a descendant of TThread. Each instance of your class is given a specific file name that it will take care of that it is run continuously.

unit BatchExecutionThread;

interface
uses Classes;

type

TBatchExecutionThread = class (TThread)
  private
    pBatchFileToExecute: string;
  public
    constructor Create(ABatchFileToExecute: string);

    procedure Execute; override;
end;

implementation

uses SysUtils, Windows;

constructor TBatchExecutionThread.Create(ABatchFileToExecute: string);
begin
  inherited Create;

  pBatchFileToExecute := ABatchFileToExecute;
end;

procedure TBatchExecutionThread.Execute;
begin
   { While no stop requested }
   while(not Terminated) do
   begin
      try
        { Execute Batch file if it exists }
        if FileExists(pBatchFileToExecute) then
        begin
          ExeAndWait(pBatchFileToExecute, SW_SHOWNORMAL);
        end;
      except
        { Ignore exception }
      end;
      { Wait a second }
      Sleep(1000);
   end;
end;

end.

Then, from your application, you can create a whole lot of different instances of this thread class for each of the file you want to run.

For example like this:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, BatchExecutionThread, System.Generics.Collections;

type
  TForm1 = class(TForm)
    ButtonStart: TButton;
    ButtonStop: TButton;
    procedure ButtonStartClick(Sender: TObject);
    procedure ButtonStopClick(Sender: TObject);
  private
    pThreads: TList<TBatchExecutionThread>;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.ButtonStartClick(Sender: TObject);
var
  i: Integer;
  FileName: string;
  Thread: TBatchExecutionThread;
begin
  if(pThreads = nil) then
  begin
    { Create a list where we store the running threads }
    pThreads := TList<TBatchExecutionThread>.Create;

    { Create 10 threads with the batch file names from 1 to 10 }
    for i:= 1 to 10 do
    begin
      { Build the filename }
      FileName := ExtractFilePath(Application.ExeName) + 'test'+ i.ToString() +'.bat';

      { Create a thread for this file }
      Thread :=  TBatchExecutionThread.Create(FileName);
      Thread.FreeOnTerminate := true;

      { Add the thread to the list }
      pThreads.Add(Thread);

      { Start the thread }
      Thread.Start();
    end;
  end;
  { else Already started }
end;

procedure TForm1.ButtonStopClick(Sender: TObject);
var
 Thread: TBatchExecutionThread;
begin
  if(pThreads <> nil) then
  begin
    { Tell all threads to stop }
    for Thread in pThreads do
    begin
      Thread.Terminate;
    end;

    { Delete list of threads }
    FreeAndNil(pThreads);
  end;
  { else not started yet }
end;

end.
NineBerry
  • 26,306
  • 3
  • 62
  • 93
  • 2
    Useful, but there are some problems if someone wishes to reuse it. Issues raised are objectively poor as not classifiable as "style preference", but the code does work: 1) Don't blanket swallow, hide and ignore exceptions. Ostrich programming is very difficult to support and maintain. 2) Take note of what the classes used already provide and don't reimplement existing features: the stop-request mechanism duplicates `TThread.Terminate();` and `Terminated`. 3) Related to 1 a missing batch file has a thread quietly doing nothing every second - errors need to be reported to user in some way. – Disillusioned Jan 15 '17 at 04:27
  • 4) There's no benefit in limiting the list to a specific type of thread. There's no dependency on `TBatchExecutionThread` after the thread is added to the list. So may as well allow the list to hold any `TThread`. – Disillusioned Jan 15 '17 at 04:30
  • PS: Is `FreeOnTerminate := True;` missing? – Disillusioned Jan 15 '17 at 04:38
  • While this solution is a good start, it is a bit pointless. I would limit the number of threads to the number of cores in the CPU and spread tasks accordingly... – whosrdaddy Jan 15 '17 at 13:31
  • I'd say the very `TThread` approach adds a lot of boilerplate here. Better just use `AsyncCall` for older Delphi or `TThread.CreateAnonymousThread` for recent ones. – Arioch 'The Jan 16 '17 at 13:34
  • " I would limit the number of threads to the number of cores in the CPU and spread tasks accordingly " - great point, and.... here we come to the OmniThreadLibrary and its "Parallel Loop" function :-D http://stackoverflow.com/q/4390149/976391 and http://stackoverflow.com/q/26978936/976391 – Arioch 'The Jan 16 '17 at 13:36
  • In the question, there is no limit as to how many of the batch files run in parallel. When the batch files do I/O themselves, there is no reason to limit the number of batch files executed at the same time to the number of CPU cores. – NineBerry Jan 16 '17 at 14:24
1

The accepted answer already outlines that timers are not the solution to your problem, and you should be using threading instead. However, I think there are more fundamental issues with the code you posted.

One important principle of software engineering is called DRY, short for Don't repeat yourself. Whenever you find yourself doing the same thing over and over again, chances are you're doing it wrong. But you're already on the right track with suggesting to use a loop rather than creating 32 timers individually.

It looks like you wrote a separate procedure for every timer, repeating the same code with very minor differences. Instead, you want to write the procedure only once, and assign it to all the timers. The only adjustment you have to make is determining the number for the filename from code.

procedure TForm1.TimerTimer(Sender: TObject);
  var FileName: string;
begin
  FileName := ExtractFilePath(Application.ExeName) + 'test' + String(TTimer(Sender).Name).Replace('Timer', '') + '.bat';
  TTimer(Sender).Enabled := False;

  if (not g_stop) then
  begin
    if FileExists(FileName) then
    begin
      ExeAndWait(FileName, SW_SHOWNORMAL);
    end;
    TTimer(Sender).Enabled := True;
  end;
end;

Notice that I'm also saving the filename to a variable, rather than constructing it two times, as in your code. That's the same principle - you don't want to do things twice. Imagine you wanted to change the way the files are named - with your original version, you'd have to adjust your code in 64 places (twice for every timer), now you would need to make only one adjustment.

In the same vein, you could create all your timers in a loop from code. Note again that this is probably not a good solution for your problem, but it's a good exercise non the less.

procedure TForm1.CreateTimers;
  var i: integer;
      Timer: TTimer;
begin
  for i := 1 to 32 do
  begin
    Timer := TTimer.Create(self);
    Timer.Interval := 1000;
    Timer.Name := 'Timer' + i.ToString;
    Timer.OnTimer := TimerTimer;
  end;
end;
DNR
  • 1,619
  • 2
  • 14
  • 22