Lazarus

Programming => General => Topic started by: Lampbert on January 29, 2020, 01:54:45 pm

Title: [SOLVED]Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on January 29, 2020, 01:54:45 pm
Hello,

My program creates a TTimer and the user enters into TEdit a value in seconds and presses TButton and the value in seconds is counted down in [hh:nn:ss] and displayed on TLabel. Here is the code:

Code: Pascal  [Select][+][-]
  1. unit Unit1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls,
  9.   DateUtils;
  10.  
  11. type
  12.  
  13.   { TForm1 }
  14.  
  15.   TForm1 = class(TForm)
  16.     Button1: TButton;
  17.     Edit1: TEdit;
  18.     Label1: TLabel;
  19.     procedure Button1Click(Sender: TObject);
  20.     procedure CreateTimerNormal;
  21.     procedure timCountdownTimer(Sender: TObject);
  22.   private
  23.  
  24.   public
  25.  
  26.   end;
  27.  
  28. var
  29.   Form1: TForm1;
  30.   TimeOut: TDateTime;
  31.   NewTimer: TTimer;
  32.  
  33. implementation
  34.  
  35. {$R *.lfm}
  36.  
  37. { TForm1 }
  38.  
  39. procedure TForm1.CreateTimerNormal;
  40. begin
  41.   if not Assigned(NewTimer) then begin
  42.     NewTimer := TTimer.Create(Self);
  43.     with NewTimer do
  44.     begin
  45.       Interval := 1000;
  46.       OnTimer := @timCountdownTimer;
  47.       Enabled := False;
  48.     end;
  49.   end;
  50. end;
  51.  
  52. function SecsToHmsStr(ASecs: integer):string;
  53. begin
  54.   Result := Format('%2d:%2.2d:%2.2d',
  55.     [ASecs div 3600, ASecs mod 3600 div 60, ASecs mod 3600 mod 60]);
  56. ;end;
  57.  
  58. procedure TForm1.timCountdownTimer(Sender: TObject);
  59. begin
  60.   Label1.Caption := SecsToHmsStr(SecondsBetween(Now, TimeOut));
  61.   if Now > Timeout then NewTimer.Enabled := False;
  62. end;
  63.  
  64. procedure TForm1.Button1Click(Sender: TObject);
  65. var
  66.   Seconds: integer;
  67. begin
  68.   CreateTimerNormal;
  69.   if TryStrToInt(Edit1.Text, Seconds) then
  70.   begin
  71.     TimeOut := IncSecond(Now, Seconds);
  72.     NewTimer.Enabled := True;
  73.     Label1.Caption := SecsToHmsStr(SecondsBetween(Now, TimeOut));
  74.   end
  75.   else
  76.     ShowMessage('Error in number of seconds');
  77. end;
  78.  
  79. end.

How can I, instead of using a TEdit where the user inputs the number of seconds, use a TFloatSpinEdit where the user enters the number of hours and minutes [hh:nn], and it will count down in the same way as before?

[EDIT]: The user enters into TFloatSpinEdit the number of hours; so, for example, 6.5 would make the TTimer count down from 6 hours and 30 minutes.
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Bart on January 29, 2020, 02:19:53 pm
A TFloatSpinEdit can hold only floating point numbers, so how is the user supposed to enter both hours and minutes then?

You can of cousse always hack the system by setting DecimalSeparator to ':' and then use Trunc() and Frac() on the Value property, but this is not going to work on GTK2 at least.
TFloatSpinEditEx has a DecimalSeparator proerty, so there this hack will work.

Feel free to abuse this control in such a way.
The up-down arrows will however the spin up the value from e.g. 12:59 to 12:60, possibly not what you want (and users still can input such invalid times).

Consider using TDateTimeControls.
Alternatively use a TMaskEdit and filter out invalid time in it's OnChange event.

Or use my TTimeEdit (http://svn.code.sf.net/p/flyingsheep/code/trunk/MijnLib/timeedit.pp) component (you must create it at runtime, there's no design time package yet) which handles most of that for you by ways of the ForceValidTimeOnChange and ForceValidTimeOnExit properties.

Bart
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on January 29, 2020, 02:24:34 pm
Hi Bart, it's my mistakes, sorry - I meant that the user would enter the number of hours in the TFloatSpinEdit, so, for example, 6.5 would make the TTimer count down from 6:30:00 (hh:nn:ss).
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Bart on January 29, 2020, 03:48:26 pm
That would be no problem at all.
1 day as a TDateTime = 1.0 as a Double.
The Value of the TFloatSpinedit is hours, so simply divide by 24.

Bart
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: lucamar on January 29, 2020, 05:02:31 pm
What you want is to convert a number of hours to seconds which can be easily done with either:
Code: Pascal  [Select][+][-]
  1. Seconds := Trunc(FloatSpinEdit1.Value * 3600);
or:
Code: Pascal  [Select][+][-]
  1. Seconds := Round(FloatSpinEdit1.Value * 3600);

Either of them should do the trick nicely. :)

Your Button1.OnClick handler should then be something like:

Code: Pascal  [Select][+][-]
  1. procedure TMainForm.Button1Click(Sender: TObject);
  2. const Minimum = 1; {Minimum seconds for it to be worth the effort}
  3. var
  4.   Seconds: integer;
  5. begin
  6.   CreateTimerNormal;
  7.   Seconds := Trunc(FloatSpinEdit1.Value * 3600);
  8.   if Seconds > Minimum then
  9.   begin
  10.     TimeOut := IncSecond(Now, Seconds);
  11.     NewTimer.Enabled := True;
  12.     Label1.Caption := SecsToHmsStr(SecondsBetween(Now, TimeOut));
  13.   end
  14.   else
  15.     ShowMessage('Error in number of seconds');
  16. end;

ETA: A note about your SecsToHmsStr - You would get the same result using:
Code: Pascal  [Select][+][-]
  1. Label1.Caption := FormatDateTime('tt', TimeOut - Now);

HTH!
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on January 29, 2020, 05:51:16 pm
What you want is to convert a number of hours to seconds which can be easily done with either:
Code: Pascal  [Select][+][-]
  1. Seconds := Trunc(FloatSpinEdit1.Value * 3600);
or:
Code: Pascal  [Select][+][-]
  1. Seconds := Round(FloatSpinEdit1.Value * 3600);

Either of them should do the trick nicely. :)

Your Button1.OnClick handler should then be something like:

Code: Pascal  [Select][+][-]
  1. procedure TMainForm.Button1Click(Sender: TObject);
  2. const Minimum = 1; {Minimum seconds for it to be worth the effort}
  3. var
  4.   Seconds: integer;
  5. begin
  6.   CreateTimerNormal;
  7.   Seconds := Trunc(FloatSpinEdit1.Value * 3600);
  8.   if Seconds > Minimum then
  9.   begin
  10.     TimeOut := IncSecond(Now, Seconds);
  11.     NewTimer.Enabled := True;
  12.     Label1.Caption := SecsToHmsStr(SecondsBetween(Now, TimeOut));
  13.   end
  14.   else
  15.     ShowMessage('Error in number of seconds');
  16. end;

ETA: A note about your SecsToHmsStr - You would get the same result using:
Code: Pascal  [Select][+][-]
  1. Label1.Caption := FormatDateTime('tt', TimeOut - Now);

HTH!

Many, many, many thanks for that - my problem is now 100% resolved. Thanks you lucamar
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: lucamar on January 29, 2020, 06:02:06 pm
Glad to be of help :)
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on January 29, 2020, 10:45:24 pm
Hi again,

I'm trying to get that countdown to display in a TStringGrid cell when the cell is created at runtime, instead of Label1.Caption, but I can't get the time to update, it just stays still.

Code: Pascal  [Select][+][-]
  1.   StringGrid1.InsertRowWithValues(StringGrid1.RowCount + 0,
  2.     [A]); // Where 'A: String' is 'SecsToHmsStr(SecondsBetween(Now, TimeOut))'


Obviously here, the program enters the String into the StringGrid, but the timer isn't told about the cell that's created at runtime and therefore cannot reference it. How would I be able to reference that cell so that the countdown happens in that cell?

Thanks.
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: winni on January 29, 2020, 11:49:30 pm
Hi!

That code works without problems.

So: Is string 'A' not empty? Add a sign to string for testing.

What is the interval of the timer? If it is to fast it may be that there is not enough time inbetween two tics to display the row.
Insert after the stringGrid.InsertRow......   a

Application.ProcessMessages

to handle all waiting events.

Winni
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on January 30, 2020, 01:09:56 pm
Hi winni,

My code does not work without problems. Please see the attached project and you will see that, while Label1.Caption is updated and counted down every second, the StringGrid1 cell containing 'a' is not.

Thank you.
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: lucamar on January 30, 2020, 01:40:00 pm
Since you're using InsertRowWithValues() you already know the row/column where A is shown so an obvious way to keep track of it for update is to store the position in some global variable (or form field) which can then be used to access the proper StringGrid.Cells[] in the OnTimer event handler.

It's difficult to say more without seeing your code and what the intention is, sorry. Didn't see your posted project :-[
I'll give a look when (if) I find some free time.

ETA: OK, here is your project with some small modifications to implement the above idea. HTH!
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on January 31, 2020, 05:55:00 pm
Thanks again for your help lucamar, that solves the problem.
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on February 02, 2020, 04:11:44 pm
I'm now trying to add multiple countdowns to the StringGrid that will all continue to count down to zero, instead of just one countdown at a time.

From some research, I found that all TComponents have the Tag property (http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/Classes_TComponent_Tag.html) where you can store a value with a TComponent. Since I'm trying to create multiple timers at runtime, I figured this would be a good way to reference them.

Bear in mind also that the user can both add and remove rows in the StringGrid.

Should I be able to do it with:
Code: Pascal  [Select][+][-]
  1. NewTimer.Tag := StringGrid1.Row;

Or is there a better way to do it that doesn't involve Tag - can a single TTimer run multiple countdowns?

See attached the project. Thanks.
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: lucamar on February 02, 2020, 05:17:57 pm
You can do that but what I would probably do is use an array or TObjectList or similar class to store the timers in such a way that I could access the timer and the grid row with the same index.

Using the Tag of the timer would mean that each time a row is inserted/deleted you would have to traverse all the timers to adjust the number of the linked row, which might have changed. Not impossible but tedious and bug-prone.
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on February 02, 2020, 05:38:49 pm
Cheers lucamar, having a look at the wiki, TObjectList looks like the way to go for sure. Will see if I can figure out how to use it now.
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on February 02, 2020, 09:28:30 pm

Hi again :) - this is what my CreateTimerNormal procedure looks like now:

Code: Pascal  [Select][+][-]
  1. procedure TForm1.CreateTimerNormal;
  2. begin
  3.   NewTimer := TTimer.Create(Self);
  4.   with NewTimer do
  5.     begin
  6.       Interval := 1000;
  7.       OnTimer := @timCountdownTimer;
  8.       Enabled := False;
  9.     end;
  10.   NewTimer.Name := 't' + IntToStr(StringGrid1.RowCount);
  11.   TimerList.Add(NewTimer);
  12.   Memo1.Lines.Add('List item number ' + IntToStr(TimerList.Count) + '- ' + NewTimer.Name); // Just so I can visualize TObjectList doing its thing
  13. end;

With this at the start:

Code: Pascal  [Select][+][-]
  1. procedure TForm1.FormCreate(Sender: TObject);
  2. begin
  3.   TimerList := TObjectList.Create;
  4. end;  

Now, in all of my other procedures, since I was only using one countdown, instead of loads, the Timer was referenced as NewTimer, but that's not the name of the timers anymore - now it's t[number]. But how do I reference them in my other procedures so that I can (end goal) have loads of them running at once and counting down in my StringGrid?

Thank you.
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: lucamar on February 03, 2020, 10:27:06 am
I think you didn't understand what I tried to tell you. What I would do is something like:

Code: Pascal  [Select][+][-]
  1. procedure TForm1.CreateTimerForRow(const Row: Integer);
  2.  
  3.   function InRange(Index, MinIdx, MaxIdx: Integer): Boolean;
  4.   begin
  5.     Result := (Index >= MinIdx) and (Index <= MaxIdx);
  6.   end;
  7.  
  8. var
  9.   Last: Integer;
  10. begin
  11.   { Make sure there is a place for the timer }
  12.   Last := TimerList.Count - 1;
  13.   if not InRange(Row, 0, Last) then
  14.     while TimerList < Last do
  15.       TimerList.Add(Nil);
  16.   { Murphy-guard: shouldn't happen (much) }
  17.   if Assigned(TimerList.Items[Row]) then
  18.     TimerList.Items[Row].Free;
  19.  
  20.   { Now set the new timer in its proper place }
  21.   TTimer(TimerList.Items[Row]) := TTimer.Create(Self);
  22.   NewTimer := TTimer(TimerList.Items[Row]);
  23.   with NewTimer do begin
  24.     Interval := 1000;
  25.     OnTimer := @timCountdownTimer;
  26.     Enabled := False;
  27.   end;
  28. end;

From then on you can refer to the timer corresponding to any grid row just by doing:
NewTimer := TTimer(TimerList.Items[Row]);
(as we did above) which will allow you to implement, for example, a popup menu for the grid with items to stop/restart/modify the timer corresponding to, say, the selected row.

Now we come to one small problem: Since all your timers are calling the same OnTimer handler, how do you know which timer is it and to which grid row it is assocciated? For that we can now use safely your previous suggestion of using the timer's Tag property, which is as simple as modyfing part of the previous code to look like:

Code: Pascal  [Select][+][-]
  1. function TForm1.CreateTimerForRow(const Row: Integer): Integer;
  2.  
  3.   { function InRange() }
  4.  
  5. var
  6.   Last: Integer;
  7. begin
  8.   {
  9.   ... Code to make place for the timer ...
  10.   }
  11.  
  12.   { Now set the new timer in its proper place }
  13.   TTimer(TimerList.Items[Row]) := TTimer.Create(Self);
  14.   NewTimer := TTimer(TimerList.Items[Row]);
  15.   with NewTimer do begin
  16.     Interval := 1000;
  17.     OnTimer := @timCountdownTimer;
  18.     Enabled := False;
  19.     Tag := Row;
  20.   end;
  21. end;

The handler timCountdownTimer() (as taken from my previous full example) should then be something like:

Code: Pascal  [Select][+][-]
  1. procedure TForm1.timCountdownTimer(Sender: TObject);
  2. var
  3.   ATimer: TTimer;
  4. begin
  5.   ATimer := Sender as TTimer;
  6.   StringGrid1.Cells[ColumNo, ATimer.Tag] := SecsToHmsStr(SecondsBetween(Now, TimeOut));;
  7.   if Now > Timeout then ATimer.Enabled := False;
  8. end;

As always, this all is just a Q&D example of how it could be done; adapt and expand to your heart's content! :)

HTH!
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on February 23, 2020, 03:13:14 pm
Thank you very much for the code, but I've still been running into great difficulty trying to get this code to work - I get loads of errors that I do not know how to fix. Do you still have the project on your end that you could send this to me?
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: lucamar on February 23, 2020, 04:33:36 pm
No, sorry; I write those small examples on-the-fly and almost never store them unless there is a whole program project behind them.

Maybe it would be better if you sent your project so that I could see what errors it gives and why and how they could be solved? I don't have much time now but I may be able to give it a look this night or tomorrow.

Alternatively I could write a full multi-alarm project (which would come in handy as I'm about to expand my little "clock" program ;)) but that will take quite some more time.
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on February 23, 2020, 05:00:23 pm
Thank you very much! Attached is the project with the two errors.
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: lucamar on February 23, 2020, 05:08:45 pm
Got it, thanks. I'm about to go out for a few hours but, gods willing, I'll give it a whirl this night or early tomorrow. :)
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on February 23, 2020, 05:38:00 pm
Thanks very much!
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: lucamar on February 24, 2020, 02:36:12 pm
So you don't think I've forgotten you ...  ;)

About the two errors, they are mine and quite obvious :-[

The first is in:
Code: Pascal  [Select][+][-]
  1.   { Make sure there is a place for the timer }
  2.   Last := TimerList.Count - 1;
  3.   if not InRange(Row, 0, Last) then
  4.     while (TimerList < Last) do // Overloaded operator
  5.       TimerList.Add(Nil);

Since TimerList is a TObjectList and Last an Integer and doen't exist an operator to compare them they are, of course, incompatible types!

The intention of that piece of code is to grow TimerList so that we can safely set/acess TimerList[Row], so the code should have been (doing away with Last):
Code: Pascal  [Select][+][-]
  1.   { Make sure there is a place for the timer }
  2.   while not InRange(Row, 0, TimerList.Count) do
  3.     TimerList.Add(Nil);
but that's not needed at all; we can just simply do:
Code: Pascal  [Select][+][-]
  1.   { Make sure there is a place for the timer }
  2.   if not InRange(Row, 0, TimerList.Count) then
  3.     TimerList.Capacity := Row + 1;


The second error, though a bit more subtle is equally stupid:
Code: Pascal  [Select][+][-]
  1.   { Now set the new timer in its proper place }
  2.   TTimer(TimerList.Items[Row]) := TTimer.Create(Self); // Arg cannot be assigned to
  3.   NewTimer := TTimer(TimerList.Items[Row]);
  4.   with NewTimer do begin
  5.     {... set timer properties ...}
  6.   end;

Well, of course! What a bad day I had, hadn't I? That should be replaced by:
Code: Pascal  [Select][+][-]
  1.   { Now set the new timer in its proper place }
  2.   NewTimer := TTimer.Create(Self);
  3.   with NewTimer do begin
  4.     {... set timer properties ...}
  5.   end;
  6.   TTimer(TimerList.Items[Row]) := NewTimer;


On another note, your code (if you excuse me saying so) is something of a mess (though that is probably because it's still in development, isn't it?) so I decided to start almost from scratch and build a fully working example. It'll take a little, cause I've a pretty busy day but since I started last night and it's almost complete I hope it'll be ready late this afternoon or early evening, including a text explaining (more or less) what it does, how and why it is the way it is (i.e. my reasoning for making things one way and not another).

See you later :)
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on February 26, 2020, 11:51:34 am
Thank you very much for your time lucamar! The code works just as I want it to now and, thanks to you, I can proceed with the aspects of my project that I understand.
Title: Re: Adapting this code to use TFloatSpinEdit?
Post by: lucamar on February 26, 2020, 09:16:55 pm
Thank you very much for your time lucamar! The code works just as I want it to now and, thanks to you, I can proceed with the aspects of my project that I understand.

OK, glad it works!

I've been a little busy but now I can (finally!) upload a full demo project including a rather long README (which cost me the lion's share of the time dedicated to this project) with most of the thinking that went on to it.

In it I changed some of the things posted before so maybe you'll want to give a look.

Anyway, it might be useful for some other people looking for similar answers to their questions so here it is, in all its (scarce) glory: the final "timers demo" (attached).

Have fun!  ;D


ETA:

Some small bugs managed to nest in the project. Very annoying. Re-attached a corrected version.
Title: Re: [SOLVED]Adapting this code to use TFloatSpinEdit?
Post by: Lampbert on February 27, 2020, 01:03:05 am
Thanks lucamar - I've just been reading your project and it's beautifully written (both formatting and logic) - I will definitely learn from this. Do you have other projects published that I could read (in Object Pascal)? Thanks!
Title: Re: [SOLVED]Adapting this code to use TFloatSpinEdit?
Post by: lucamar on February 27, 2020, 11:09:49 am
Thanks lucamar - I've just been reading your project and it's beautifully written (both formatting and logic) - I will definitely learn from this.

Thanks, that is what I always hope, that the little I do be of help to others. :)

Quote
Do you have other projects published that I could read (in Object Pascal)?

Not at the moment. I had some (Delphi ones) in Google Code but I I deleted them when they closed the service and didn't bother to move them to another service. I'm thinking of starting to publish again and building a web site with tutorial content but I'm very busy with other things and have very little free-time :(
TinyPortal © 2005-2018