As promised last time, I present here a very simple implementation of an automatically cancelling hourglass exploiting the same life-time management used previously to automatically dispose of temporary objects.
First, The Code And An Apology
[dm]11[/dm]
I have tested the download this time and it appears fine.
Also a quick apology to anyone who experienced difficulty over the weekend accessing the site. Those problems should now have been resolved by my ISP.
Then, How It Is Used
As you will see if you download it, the implementation really is embarrasingly simply – the license and documentation is quite a bit more extensive than the code itself! This is most apparent in the usage itself, handled by just two functions, one of which is needed only occasionally:
- HourglassOn()
- HourglassOff()
To turn the hourglass on, call HourglassOn().
You don’t actually need to do anything for it to be turned off!
procedure TMyForm.ButtonOKClick(Sender: TObject); begin HourglassOn; // Do your processing here end;
Once your code leaves the scope in which you turned the hourglass on, the reference you created by calling HourglassOn() is released. If that is the only reference then the hourglass will turn itself off again. Even if your code calls other routines that in turn call HourglassOn(), the references will all be released and once all of them have been released, the hourglass goes off:
procedure TMyForm.ButtonOKClick(Sender: TObject); begin HourglassOn; // Do your processing here SomeCommonProcessing; end; procedure TMyForm.SomeCommonProcessing; begin HourglassOn; // Common processing code occurs here end;
And as with AutoFree(), these references are released even where an unhandled exception is encountered or raised.
So why even have an HourglassOff() facility?
Honestly?
I’m not sure. Certainly I don’t recall ever having to use it. It was implemented when I thought it may be useful to ensure that the hourglass is off prior to presenting a message box or other user interaction, but before I had realised that even this doesn’t appear to be necessary. I left it in just in case.
You never know.
🙂
HourglassOff() effectively clears the decks – any subsequent calls to HourglassOn() will effectively be managing an entirely new hourglass cursor. Any references to a prior existing hourglass will silently be released without interfering with the new cursor.
More Tinkering With (Deltics.)Forms
There is actually a third function that I’ve not mentioned so far: HourglassActive().
This returns a Boolean that indicates whether an hourglass (as represented by an hourglass object in this implementation) is currently active. This is used primarily by a recent change in my Deltics.Forms unit in a slight modification of the behaviour of ShowModal.
ShowModal saves the current Screen.Cursor at the time that a form is shown modally before applying a Screen.Cursor of crDefault. When the modal form has been dismissed, if Screen.Cursor has not been further modified the saved cursor is restored. If the modal form itself has changed the Screen.Cursor at all, the saved cursor is NOT restored.
In Deltics.Forms, ShowModal has now been extended to also consider the HourglassActive() indicator. That is, regardless of whether the modal form has modified the Screen.Cursor, once a modal form is dismissed the hourglass cursor (NOT any saved cursor) is reinstated if HourglassActive() is TRUE.
Limitations
Firstly and most obviously of course, this is not a general purpose cursor management framework. It is solely concerned with simple use of an hourglass to indicate a “busy” state in an application.
Secondly the implementation deals with hourglass control for processes entirely bounded by scope. If you have some process that encompasses separate scopes then there is the possibility of cursor flicker:
procedure TMyForm.ButtonDoItClick(Sender: TObject); begin ProcessPart1; ProcessPart2; end; procedure TMyForm.ProcessPart1; begin HourglassOn; // Do some processing end; procedure TMyForm.ProcessPart2; begin HourglassOn; // Do some other processing end;
In this case the cursor will toggle between hourglass and default states for each of ProcessPart1 and ProcessPart2 as the hourglass turned on by each is turned off when each completes. Usually this sort of situation is easily resolved by ensuring that the hourglass is turned on in the event – originating in the GUI – that spans the scopes involved, i.e. that calls the methods.
Other than that, this very simple implementation has served me well, although it has not perhaps been exercised over rigorously.
Caveat Developor
I should point out that, as Barry Kelly mentioned in the comments to the AutoFree() post, the behaviour on which this relies (or perhaps more specifically the precise timing of the behaviour) is not a formally defined aspect of the language or the run-time, and so cannot be guaranteed in future Delphi versions.
He is better placed than I (he actually works on the Delphi compiler!) to know, but I would be very surprised if this behaviour did change as there may well be code “in the wild” – quite apart from that which I’ve presented recently – that relies on, or is at least sensitive to, this timing.
For myself I’m not overly concerned – Barry wasn’t saying that it would change, only that it could.
Never-the-less, I guess I should consider myself warned. And, now, so too should you.
🙂
Am I right in thinking you’re making use of the fact that *object* references to a TInterfacedObject aren’t reference counted?
> _Hourglass: THourglass = NIL
Otherwise this reference would keep the object alive, yes?
If so, neat :-).
Yes, exactly so.
Since it’s not really safe to assume that the cursor was set to crDefault before the “HourglassOn();” call is made, it would make sense to buffer the cursor prior to the hourglass activation.
Var
_CursorBuffer : TCursor;
constructor THourglass.Create;
begin
inherited;
_Hourglass := self;
_CursorBuffer := Screen.Cursor;
Screen.Cursor := crHourglass;
end;
and so on…
Hi Lois,
Yes, you’re right although the assumption is usually quite safe in my applications.
🙂
If I were to incorporate that I would save the cursor in the THourglass object rather than as a unit variable.
If HourglassOff() is used to force the hourglass off, then any subsequent THourglass that might be created should respect the cursor in effect at the time that it itself is created, not the one that may have been in effect when some earlier THourglass was created that may not yet have been destroyed.
You could make an On() method on the interface returned by Hourglass…(). i.e.:
IHourglass = interface
procedure On;
// other methods, incl. Off and Active (or a property)
end;
function Hourglass: IHourglass;
begin
Result := THourglass.Create; // note: you don’t need the instance variable unless you’re trying for something more complex than an hourglass.
end;
Then the user code would become:
Hourglass.On; // or “Hourglass.&On;” as Delphi >=2005 IDE’s code completion likes to write it.
This way, the interface MUST be returned. A safe approach, unlike relying on an invisible variable. All you need is to move the actual hourglass management to the methods, rather than the constructor.
The IDE has some trouble with the method name “On” (class/code completion in particular), because as you know it’s also a keyword, but the compiler swallows it just fine. I’ve tested this on Delphi 7, 2006 and 2009 (with and without the ampersand prefix). All working fine. It is an abuse of the word “on”, but then so is the keyword itself…
@Anonymous: You could, although I think the object reference to the current hourglass is still required.
The approach you suggest means that every reference to “Hourglass” would create a new THourglass instance. In the original implementation the interface reference count on the single (active) hourglass tracks the number of “live” hourglass calls, and it is that reference counting that enables the reliable, automatic cancellation of the hourglass even when nested calls are made from procedures called by procedures, etc.
The approach you suggest loses that behaviour.
Also note that the behaviour that might change is not the returning of the interface itself nor even the existence of a temporary variable, only the timing of what the compiler does with that temporary variable when it is an interface reference.
When the result is an interface, the compiler MUST .AddRef() that interface when it is returned (that is part of the behaviour of the called function, not the caller).
That is not something that could change as far as I can see, at least not without a VERY high risk of breaking a LOT of code.
What might – theoretically – change, is the timing of the call to .Release() for any temporary variables in the calling code.
Currently that occurs when the caller exits. Barry pointed out that this is not defined by the language or the runtime, and so it might be released earlier than that in the future, i.e. presumably immediately after the call.
So what currently compiles (effectively) as:
begin
HourglassOn();
[try]
// do work
[finally]
[temporary hourglass intf.Release()]
[end]
end;
might in future become:
begin
HourglassOn();
[temporary hourglass intf.Release]
// do work
end;