Tom
Tom

Reputation: 111

Delphi: Logging UI events fired by user

I've been asked to put something in place into our programs in order to log somewhere what's happening on forms...

Logging something such ...clicked either this or that button, context menu, events fired on components and so on..

This mostly because we are dealing with legacy code, not quite structured unfortunately that we are adjusting/reviewing (Delphi XE10) when possible and we would like to track everything is happening (or most of it) from an user's point of view in order to - being able to have a lead on what the user did when something mysterious happens - being able to review the code where necessary when we have no idea of how thing have happened - eventually speed issues

I'm not talking about exceptions or data logging...those are properly handled.

It's just about the UI.

Do you know if there is any library that might do something similar? If not, how would you try to achieve this?

Code examples are happily accepted (even simple such as one/more form/s with a few buttons etc)

Thanks all!!!

Upvotes: 0

Views: 957

Answers (2)

Frazz
Frazz

Reputation: 3043

You could develop a non visual component and drop it on every form, but this would have to be a very complex component that either uses RTTI or peeks at windows messages to try to listen to what is happening to the other visual components in the form.

I see two simpler ways to do at least part of what you are asking...

1) Intercept calls to eventhandlers

Develop a component (somewhat simpler than the one I hinted at above) that hooks up to the events of the control classes you're interested in, logs the calls, and then forwards them to the original eventhandler procedures (if any).

Suppose you are interested in TWinControl.OnEnter and OnExit, TButton.OnClick and TEdit.OnChange...

TEventLogger = Class(TComponent)
Private
  Procedure Hookup(Const EventName: String; Const Component: TComponent; Const LoggingHandler: TNotifyEvent; Var Handler: TNotifyEvent);
Public
  Procedure Setup(Const Container: TComponent);
  Procedure HandleChange(Sender: TObject);
  Procedure HandleClick(Sender: TObject);
  Procedure HandleEnter(Sender: TObject);
  Procedure HandleExit(Sender: TObject);
End;

Procedure TEventLoggerHookup(Const EventName: String; Const Component: TComponent; Const LoggingHandler: TNotifyEvent; Var Handler: TNotifyEvent);
Begin
  if Assigned(Handler) Then
    Handlers.AddObject(Component.Name + ';' + EventName, Handler);
  Handler := LoggingHandler;
End;

Procedure TEventLogger.Setup(Const Container: TComponent);
Var
  i: Integer;
  c: TComponent;
Begin
  For i:=0 to Container.ComponentCount - 1 Do Begin
    c := Container.Components[i];
    if c Is TWinControl Then Begin
      Hookup('Enter',HandleEnter,TWinControl(c).OnEnter;
      Hookup('Exit',HandleExit,TWinControl(c).OnExit;
    End;
    If c Is TButton Then 
      Hookup('Click',HandleClick,TButton(c).OnClick;
    If c Is TEdit Then 
      Hookup('Change',HandleChange,TEdit(c).OnChange;
  End;
End;

Procedure TEventLogger.Procedure HandleChange(Sender: TObject);
Var
  s: String;
  i: Integer;
  e: TNotifyEvent;
Begin
  With Sender As TComponent Do Begin
    s := Name + ';Change';
    Log(s);
    i:= Handlers.IndexOf(s);
    If i <> -1 Then Begin
      e := Handlers.Objects[i];
      e(Sender);
    End;
  End;
End;

This is not tested, but I think you got the point. Things to keep in mind:

  • Handlers is a simple TStringList... but you could do better with a generic TDictionary<Key,Value>. Of course you should create it and destroy it appropriately.
  • All the listed events are TNotifyEvents. But you can easily overload the Hookup procedure to handle other types ov events.
  • Setup must be executed when components have been already created and properties streamed in from the dfm. This may require you add some code to each form.
  • This technique may (and will) backfire if your application creates controls at runtime in a dynamic way, or if it messes with eventhandlers at runtime.

2) Use inheritance

Derive a new class of visual component for each one used that you want to log. All you need to define for each of these is a Log method and override the specific "event" methods your're interested in. For example:

TLogButton = class(TButton)
private
  procedure Log(Const Event: String);
protected
  procedure Click; override;
end;

TLogButton.Click;
begin
  Log('Click');
  Inherited;
end;

You could then implement the Log method adding all the info you need (form and component class and name, or others). If you want to do something complex, then you will be better off delegating the log operation to a specialized class.

This is also not tested. Things to keep in mind:

  • To set it up, you can just use a massive Search and Replace... to modify TButton in TLogButton... in both pas and dfm files (I do hope you are using text dfm, if not you better convert them to text).
  • You'll have to add your new unit to the Interface section of all forms that contain controls you're interested in
  • You should register you new classes in a custom library so as to have them available in the IDE, otherwise you won't be able to open your forms at design time.
  • You have to do this for every class you're interested in... which can become messy if the application uses many different but similar classes of controls.
  • You have to know the internals of the classes you want to log... not all methods are overridable... many third party libraries do not come with sources... it can be hell.

Conclusions

Overall... I'd probably go with the latter solution. It is basically easier to implement on a single class of controls, and you could probably already get some very useful tracing of user activity with just a bunch of classes. The former solution is a bit more sophisticated, and could probably be extended to cover some situations... but it is harder to test throughly and it is too risky if you do not know your code base very well.

Upvotes: 1

Wouter van Nifterick
Wouter van Nifterick

Reputation: 24086

As a lightweight solution you can use OutputDebugString(PChar('My logtext')), or TFile.AppendAllText('log.txt','My logtext').

If you want fancy logging with special viewers, thread-safe, etc, you could include some special logging framework. In that case it's better search online and compare features yourself instead of asking for an opinion here.

Upvotes: 1

Related Questions