Recent

Author Topic: GUI and command driven application  (Read 2902 times)

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 288
GUI and command driven application
« on: December 17, 2020, 03:40:55 pm »
Hi all,

I'll start by saying that I've never written a Console program in Delphi or Lazarus before, but have done tons of VCL style applications (i.e. those with a GUI). However, in my latest project, I have a user who wants to be able to use it from the command line, but I also have other users wanting the GUI.

I know I can incorporate switches in, and then pick them up using ParamStr and ParamCount, but I'm wanting to have a non-GUI version of the same application. I was thinking of modifying the LPR file to determine if a certain switch is there to indicate console only, then switch to that code rather than the GUI code. Or, would it be a better idea to have a separate project, effectively (i.e. LPR file) which uses code from the main program and not touch the original LPR file?

Cheers,

Gerald.

Handoko

  • Hero Member
  • *****
  • Posts: 5537
  • My goal: build my own game engine using Lazarus
Re: GUI and command driven application
« Reply #1 on: December 17, 2020, 03:54:30 pm »
If it is my project I will separate it into 3 parts and do the project like this:

I will start with the GUI first, because building GUI program using Lazarus is easier than the console program. At least I think so.

Then I will create a share unit or maybe several units for the data processing, calculation, and whatever that can be reused for the console program.

After tested and sure that GUI program runs correctly, then I will start writing the console program and reuse all the share units.

By properly separate them into 3 parts (GUI, share units, console interface), future maintaining and bug fixing will be much easier.
« Last Edit: December 17, 2020, 04:06:16 pm by Handoko »

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 288
Re: GUI and command driven application
« Reply #2 on: December 17, 2020, 04:53:14 pm »
I've got the majority of the back end code in a separate unit, as a class. I think most of the code in the main unit is to do with the GUI. I'll just need to check, and move what is needed out.

Many thanks,

Gerald.

MarkMLl

  • Hero Member
  • *****
  • Posts: 8572
Re: GUI and command driven application
« Reply #3 on: December 17, 2020, 07:27:20 pm »
I know I can incorporate switches in, and then pick them up using ParamStr and ParamCount, but I'm wanting to have a non-GUI version of the same application. I was thinking of modifying the LPR file to determine if a certain switch is there to indicate console only, then switch to that code rather than the GUI code. Or, would it be a better idea to have a separate project, effectively (i.e. LPR file) which uses code from the main program and not touch the original LPR file?

It's obviously possible to have completely separate projects using common backend code, and if you set up the target name appropriately you can end up with e.g.


tbd_gui-x86_64-linux-gtk2
tbd_gui-x86_64-linux-qt
tbd_tui-x86_64-linux
tbd-x86_64-linux


Alternatively you can fudge the .lpr so that if there's any parameter (other than file redirections, which in the case of unix at least are actioned and removed by the shell) it starts up as a command-driven program rather than a gui-driven one:

Code: Pascal  [Select][+][-]
  1. program lg600;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. uses
  6. {$ifdef LCL }
  7.   {$IFDEF UNIX}{$IFDEF UseCThreads}
  8.   cthreads,
  9.   {$ENDIF}{$ENDIF}
  10.   Interfaces, // this includes the LCL widgetset
  11.   Forms, unitLG600
  12.   { you can add units after this } ,
  13.         unparser, unitEffectsAndDpi, unitKeyboard, unitselectprofile, netlink,
  14. {$endif LCL }
  15.   consoleApp, hidraw, dirwalker;
  16.  
  17. {$R *.res}
  18.  
  19. begin
  20. {$ifdef LCL }
  21.   RequireDerivedFormResource:=True;
  22.   Application.Initialize;
  23. {$endif LCL }
  24.   if HasOptions() then
  25.     Halt(RunConsoleApp());
  26.  
  27. (* The objective here is to minimise the amount of manually-inserted text so as *)
  28. (* to give the IDE the best chance of managing form names etc. automatically.   *)
  29.  
  30. {$ifdef LCL }
  31.   Application.CreateForm(TFormLG600, FormLG600);
  32.   Application.CreateForm(TEffectsAndDpi, EffectsAndDpi);
  33.   Application.CreateForm(TButtonsAndKeys, ButtonsAndKeys);
  34.   Application.CreateForm(TFormProfile, FormProfile);
  35.   Application.Run;
  36. {$endif LCL }
  37. end.
  38.  

You can see there where the .lpr compiles differently depending on whether the LCL is to be linked in, and runs differently depending on HasOptions().

In all cases, I consider it good practice to handle --help and --version specially with text output, and --about specially with graphical output, and to include a makefile to automate FPC build so the user doesn't have to futz about too much with commandline options.

Generally speaking you can handle redirected stdin and stdout, but the detail gets very OS-specific. The sort of situation in which that becomes an issue is if the program is to run without a GUI if a file is redirected into it, and you don't want the user to have to put a dummy - or -- on the commandline to make that happen.

HTH.

MarkMLl
« Last Edit: December 17, 2020, 08:17:42 pm by MarkMLl »
MT+86 & Turbo Pascal v1 on CCP/M-86, multitasking with LAN & graphics in 128Kb.
Logitech, TopSpeed & FTL Modula-2 on bare metal (Z80, '286 protected mode).
Pet hate: people who boast about the size and sophistication of their computer.
GitHub repositories: https://github.com/MarkMLl?tab=repositories

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 288
Re: GUI and command driven application
« Reply #4 on: July 13, 2023, 12:15:11 pm »
I've finally got around to doing this. I had, previously, just dealt with it from the main form, but then found out that certain commands to retrieve what has been passed wasn't reliable. So, I came up with this (which is similar to MarkMLI's above:
Code: Pascal  [Select][+][-]
  1. program project1;
  2.  
  3. {GUI and Console application combined
  4. This can be used as a console application, but can also interact with the GUI
  5. side of it. This means you don't need to repeat code for both the GUI and
  6. Console sides.
  7. Untested on more complex applications.
  8. When the console exits to the GUI, the icon is not created, probably because it
  9. is being launched from the console.
  10. }
  11.  
  12. {$mode objfpc}{$H+}
  13.  
  14. uses
  15.  {$IFDEF UNIX}
  16.  cthreads,
  17.  {$ENDIF}
  18.  {$IFDEF HASAMIGA}
  19.  athreads,
  20.  {$ENDIF}
  21.  Interfaces, // this includes the LCL widgetset
  22.  Classes, SysUtils, CustApp,//For the console side of this
  23.  Forms, Unit1
  24.  { you can add units after this };
  25.  
  26. {$R *.res}
  27.  
  28. type
  29.  
  30.  { TConsoleApp }
  31.  
  32.  TConsoleApp = class(TCustomApplication)
  33.  public
  34.   constructor Create(TheOwner: TComponent); override;
  35.   destructor Destroy; override;
  36.  end;
  37.  
  38. { TConsoleApp }
  39.  
  40. constructor TConsoleApp.Create(TheOwner: TComponent);
  41. begin
  42.  inherited Create(TheOwner);
  43.  StopOnException:=True;
  44. end;
  45.  
  46. destructor TConsoleApp.Destroy;
  47. begin
  48.  inherited Destroy;
  49. end;
  50.  
  51. var
  52.  ConsoleApp: TConsoleApp;
  53.  B         : Byte;
  54.  input,
  55.  script    : String;
  56.  tmp       : PChar;
  57.  params    : TStringArray;
  58.  Index     : Integer;
  59.  F         : TFileStream;
  60. begin
  61.  //Create GUI application
  62.  RequireDerivedFormResource:=True;
  63.  Application.Scaled:=True;
  64.  Application.Title:='Console Application Demo';
  65.  Application.Initialize;
  66.  Application.CreateForm(TForm1, Form1);
  67.  //Do we have 'console' passed as a parameter?
  68.  input:=Application.CheckOptions('d:','console:');
  69.  //No, we have something else so quit to the GUI
  70.  if input<>'' then //This will also quit if 'console' was supplied, but there was other text too
  71.  begin
  72.   WriteLn(input); //Display the errors
  73.   WriteLn('Exiting to GUI.');
  74.  end;
  75.  //No errors, and 'console' passed as a parameter
  76.  if(input='')and(Application.HasOption('c','console'))then
  77.  begin
  78.   //Create the console application
  79.   ConsoleApp:=TConsoleApp.Create(nil);
  80.   ConsoleApp.Title:='Console Application';
  81.   //Write out a header
  82.   WriteLn('********************************************************************************');
  83.   WriteLn('Entering Console');
  84.   //Did the user supply a file for commands to run?
  85.   script:=Application.GetOptionValue('c','console');
  86.   if script<>'' then
  87.    if not FileExists(script) then
  88.    begin
  89.     WriteLn('File '''+script+''' does not exist.');
  90.     script:='';
  91.    end
  92.    else
  93.    begin
  94.     WriteLn('Running script '''+script+'''.');
  95.     //Open the script file
  96.     F:=TFileStream.Create(script,fmOpenRead or fmShareDenyNone);
  97.    end;
  98.   //Intialise the array
  99.   params:=nil;
  100.   repeat
  101.    //Prompt for input
  102.    write('>');
  103.    //Read a line of input from the user
  104.    if script='' then ReadLn(input)
  105.    else
  106.    begin //Or from the file
  107.     input:='';
  108.     B:=0;
  109.     repeat
  110.      if F.Position<F.Size then B:=F.ReadByte; //Read byte by byte
  111.      if(B>31)and(B<127)then input:=input+Chr(B); //Valid printable character?
  112.     until(B=$0A)or(F.Position=F.Size); //End of line with $0A or end of file
  113.     WriteLn(input); //Output the line, as if entered by the user
  114.    end;
  115.    //Add to the memo on the main form
  116.    Form1.Memo1.Lines.Add('>'+input);
  117.    //Split the string at each space, unless enclosed by quotes
  118.    params:=input.Split(' ','"');
  119.    //Anything entered?
  120.    if Length(params)>0 then
  121.     //Remove the quotes
  122.     for Index:=0 to Length(params)-1 do
  123.      begin
  124.       tmp:=PChar(params[Index]);
  125.       params[Index]:=AnsiExtractQuotedStr(tmp,'"');
  126.      end
  127.    else //Input was empty, so create a blank entry
  128.    begin
  129.     SetLength(params,1);
  130.     params[0]:='';
  131.    end;
  132.    //Convert the command to lower case
  133.    params[0]:=LowerCase(params[0]);
  134.    //Parse the command
  135.    case params[0] of
  136.     'add'      : //Add files
  137.       if Length(params)>1 then //Is there any files given?
  138.        for Index:=1 to Length(params)-1 do
  139.         if params[Index][1]='>' then //It is a directory to select
  140.          WriteLn('Select directory '''+Copy(params[Index],2)+'''.')
  141.         else                         //Just add a file
  142.          WriteLn('Adding file: '''+params[Index]+'''.')
  143.       else WriteLn('Nothing to add.');//Nothing has been passed
  144.     'help'     : //Help command
  145.      begin
  146.       WriteLn('Help');
  147.       WriteLn('----');
  148.       WriteLn('add      : Adds the files listed after the command.');
  149.       WriteLn('           Use space to separate, and enclose in quotes if space required.');
  150.       WriteLn('exit     : Quits console and application.');
  151.       WriteLn('exittogui: Quits the console and opens the GUI application.');
  152.       WriteLn('help     : Shows this text.');
  153.      end;
  154.     'exit',      //Exit the console application
  155.     'exittogui': WriteLn('Exiting.');
  156.     ''         :;//Blank entry, so just ignore
  157.    otherwise WriteLn('Unknown command.'); //Something not recognised
  158.    end;
  159.    //End of the script? Then close the file
  160.    if script<>'' then
  161.     if F.Position=F.Size then
  162.     begin
  163.      F.Free;
  164.      script:='';
  165.     end;
  166.    //Continue until the user specifies to exit
  167.   until(params[0]='exit')or(params[0]='exittogui');
  168.   //Script file still open? Then close it
  169.   if script<>'' then F.Free;
  170.   //Footer at close of console
  171.   WriteLn('********************************************************************************');
  172.   //Close the console application
  173.   ConsoleApp.Free;
  174.   //Close the GUI application
  175.   if params[0]='exit' then Application.Terminate
  176.   else Application.Run //Otherwise open the GUI application
  177.  end else Application.Run; //Console application not specified, so open as normal
  178. end.
  179.  
This works beautifully on macOS. Windows, however, has other ideas. If anything is added after the executable name in Command Line Windows throws up an error "File not open".
I tried adding in a WriteLn, before any checks, and that just throws up the same error even when opened from Explorer.
HasOptions is not known at this point, even Application.HasOptions fails in the lpr. It works fine from the main form.

KodeZwerg

  • Hero Member
  • *****
  • Posts: 2269
  • Fifty shades of code.
    • Delphi & FreePascal
Re: GUI and command driven application
« Reply #5 on: July 13, 2023, 12:38:49 pm »
I guess I would write it differently than all above ways showing.
1. Launcher executable 32bit
2. GUI executable x86/x64
3. CLI executable x86/x64

in launcher I determine on what system I am running on (32/64), than determine if any commandline argument was given to start the CLI with passing over arguments or GUI.
Pretty simple, or?
« Last Edit: Tomorrow at 31:76:97 xm by KodeZwerg »

MarkMLl

  • Hero Member
  • *****
  • Posts: 8572
Re: GUI and command driven application
« Reply #6 on: July 13, 2023, 02:10:21 pm »
I've finally got around to doing this. I had, previously, just dealt with it from the main form, but then found out that certain commands to retrieve what has been passed wasn't reliable. So, I came up with this (which is similar to MarkMLI's above:

Over the last few months I've looked at a couple of programs that I wanted to be able to compile with a very wide spread of Lazarus versions, and have arrived at something like

Code: Pascal  [Select][+][-]
  1. ...
  2.  
  3. (* Note the conditional compilation here specifically to support "old-style"    *)
  4. (* resources etc. as used by a pre-v1 Lazarus typically with a pre-v2.4 FPC. In *)
  5. (* practice that means FPC 2.2.4, since no attempt is made to support older     *)
  6. (* versions due to their lack of the FPC_FULLVERSION predefined.                *)
  7.  
  8. {$if FPC_FULLVERSION >= 020400 }
  9.   {$ifdef LCL }
  10.     {$R *.res}
  11.   {$endif LCL }
  12. {$endif FPC_FULLVERSION        }
  13.  
  14. begin
  15. {$ifdef LCL }
  16.  
  17. (* Lazarus v1 (roughly corresponding to FPC 3.0) introduced this global         *)
  18. (* variable, defaulting to false. It controls error reporting at startup if an  *)
  19. (* expected .lfm is missing, so may be omitted if unsupported by the target LCL *)
  20. (* etc. version e.g. by using the test $if LCL_FULLVERSION >= 1000000...$ifend. *)
  21.  
  22. {$if declared(RequireDerivedFormResource) }
  23.   RequireDerivedFormResource:=True;
  24. {$endif declared                          }
  25.  
  26. (* Lazarus v2 or later might insert  Application.Scaled := true  here if the    *)
  27. (* project-level application settings include "Use LCL scaling". This will mess *)
  28. (* up compatibility with older versions, generally speaking it may be omitted,  *)
  29. (* if required guard using the test $if LCL_FULLVERSION >= 1080000...$ifend.    *)
  30.  
  31.   Application.Initialize;
  32. {$endif LCL }
  33.  
  34. (* If there is a parameter list and it either doesn't make sense or contains    *)
  35. (* --help etc. then display help text.                                          *)
  36.  
  37.   if not ParseParams(PluginName, devices) then
  38. ...
  39.  

I believe there has been previous discussion about the difficulty of checking for fully-qualified identifiers, e.g. your example of Application.HasOptions, you have to do it by hardcoding a compiler version.

MarkMLl
« Last Edit: July 15, 2023, 09:22:07 am by MarkMLl »
MT+86 & Turbo Pascal v1 on CCP/M-86, multitasking with LAN & graphics in 128Kb.
Logitech, TopSpeed & FTL Modula-2 on bare metal (Z80, '286 protected mode).
Pet hate: people who boast about the size and sophistication of their computer.
GitHub repositories: https://github.com/MarkMLl?tab=repositories

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 288
Re: GUI and command driven application
« Reply #7 on: July 13, 2023, 06:42:06 pm »
I have since found out, by trial and error, that it is just Windows that is being troublesome. The technique shown by my code above works fine on macOS and Linux.

I cannot find anyway round it for Windows, apart from two separate executables - one for GUI and one for Console.

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 288
Re: GUI and command driven application
« Reply #8 on: July 13, 2023, 08:28:25 pm »
Done it:
Code: Pascal  [Select][+][-]
  1. program project1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. uses
  6.  {$IFDEF UNIX}
  7.  cthreads,
  8.  {$ENDIF}
  9.  {$IFDEF HASAMIGA}
  10.  athreads,
  11.  {$ENDIF}
  12.  Interfaces, // this includes the LCL widgetset
  13.  Classes, SysUtils, CustApp,//For the console side of this
  14.  Forms, Unit1
  15.  {$IFDEF Windows},Windows{$ENDIF}
  16.  { you can add units after this };
  17.  
  18. var
  19.  FrunGUI: Boolean;
  20.  
  21. {$R *.res}
  22.  
  23. type
  24.  
  25.  { TConsoleApp }
  26.  
  27.  TConsoleApp = class(TCustomApplication)
  28.  protected
  29.   procedure DoRun; override;
  30.  public
  31.   constructor Create(TheOwner: TComponent); override;
  32.   destructor Destroy; override;
  33.  end;
  34.  
  35. { TConsoleApp }
  36.  
  37. constructor TConsoleApp.Create(TheOwner: TComponent);
  38. begin
  39.  inherited Create(TheOwner);
  40.  StopOnException:=True;
  41. end;
  42.  
  43. destructor TConsoleApp.Destroy;
  44. begin
  45.  inherited Destroy;
  46. end;
  47.  
  48. procedure TConsoleApp.DoRun;
  49. var
  50.  ErrorMsg: String;
  51.  input: String;
  52. begin
  53.  // quick check parameters
  54.  ErrorMsg:=CheckOptions('c', 'console');
  55.  if ErrorMsg<>'' then begin
  56.   ShowException(Exception.Create(ErrorMsg));
  57.   Terminate;
  58.   Exit;
  59.  end;
  60.  // parse parameters
  61.  if HasOption('c', 'console') then
  62.  begin
  63.   {$IFDEF Windows}
  64.   AllocConsole;
  65.   IsConsole:=True;
  66.   SysInitStdIO;
  67.   {$ENDIF}
  68.   repeat
  69.    Write('>');
  70.    ReadLn(Input);
  71.   until(Input='exit')or(Input='exittogui');
  72.   if Input='exittogui' then Frungui:=True else Frungui:=False;
  73.  end;
  74.  // stop program loop
  75.  Terminate;
  76. end;
  77.  
  78. procedure RunConsole;
  79. var
  80.  ConsoleApp: TConsoleApp;
  81. begin
  82.  ConsoleApp:=TConsoleApp.Create(nil);
  83.  ConsoleApp.Run;
  84.  ConsoleApp.Free;
  85. end;
  86.  
  87. begin
  88.  Frungui:=True;
  89.  RunConsole;
  90.  if Frungui then
  91.  begin
  92.   {$IFDEF Windows}
  93.   IsConsole:=False;
  94.   {$ENDIF}
  95.   //Create GUI application
  96.   IsConsole:=False;
  97.   RequireDerivedFormResource:=True;
  98.   Application.Scaled:=True;
  99.   Application.Initialize;
  100.   Application.CreateForm(TForm1,Form1);
  101.   Application.Run;
  102.  end;
  103. end.
  104.  
Fire it off from Explorer and it runs as a GUI, as normal. Fire it off from command line, with no parameters, and it runs as a GUI, as normal. Fire it off from command line with -c or --console following and it opens a command line console. Type exit to quit, or exittogui to quit to the GUI.
Still need to play around with it and see if I can open a file in the command line which stays open when the GUI opens. Anyway, it's progress.

WackaMold

  • New Member
  • *
  • Posts: 11
Re: GUI and command driven application
« Reply #9 on: August 13, 2023, 10:16:57 pm »
Just for completeness on Windows, this needs {$AppType GUI} in the source after the program; statement, or -WG on the FPC compiler command line to specify a graphic type application. If the EXE is built as {$AppType Console} or -WC then it would specify a console type application. In that case, launching the GUI side of the app from your file explorer would show a blank console window behind it, and we don't want that.

jamie

  • Hero Member
  • *****
  • Posts: 7702
Re: GUI and command driven application
« Reply #10 on: August 14, 2023, 12:43:39 am »
My approach would be to build all the functionality in the console app and in the GUI app, just run it as a process with command line switches and capture the output in the GUI.

  This way, all you need is to maintain one code base and have it work either way, stream the IO.
The only true wisdom is knowing you know nothing

Gustavo 'Gus' Carreno

  • Hero Member
  • *****
  • Posts: 1353
  • Professional amateur ;-P
Re: GUI and command driven application
« Reply #11 on: August 16, 2023, 08:17:45 am »
Hey geraldholdsworth,

Due to the comment about {$AppType GUI} (-WG) and {$AppType CONSOLE} (-WC) that WackaMold made, I don't think is wise to have an .lpr with both the GUI and Console code.

After reading this thread I would advise on one of 2 paths:
  • Using the method suggested by WackaMold and have 3 separate projects:
    • GUI Project
    • CLI Project
    • Shared Code Project
  • Using the method suggested by KodeZwerg:
    • Launcher executable 32bit
    • GUI executable x86/x64
    • CLI executable x86/x64

In my opinion this will make the binary each binary has lean as it can be for each specific task.
Having a single binary with both the CLI and GUI is quite appealing since all is contained in the same project.
But due to the fact that GUI programs and CLI program have different modes of compilation and have excluding needs when it comes to compiled resources, it's a tad wasteful and possibly asking for trouble down the line.

Just my 2c...

Cheers,
Gus

 

TinyPortal © 2005-2018