0

I am trying to track and log what a user does in my app. One way to help is to have a list of what buttons and menu etc they click as they use the program. Then if the program crashes for any reason I have a log of the exact steps they took to try and reproduce the issue.

So I drop an ApplicationEvents component on the main form and then under OnMessage I use...

procedure TForm1.ApplicationEvents1Message(var Msg: tagMSG;var Handled: Boolean);
var Target: TControl;
    Point: TPoint;
begin
     Handled := FALSE;
     if (Msg.Message = WM_LBUTTONDOWN) then
     begin
          GetCursorPos(Point);
          Target := FindDragTarget(Point,True);
          if Assigned(Target) then
          begin
               AddLogText('CLICKED '+(Target).owner.name+'.'+(Target).name);
          end;
    end;
end;

This does seem to work and I can log all clicks but there is a nasty side effect. Sometimes and without a reporducable set of steps the above code seems to eat a mouse click. This means at seemingly random times a toolbar button won't click and need a double-click. Or a dialog button needs a double-click. Needless to say this is annoying as hell and made me question my mouse before I realised it was only happening in my app.

If I remark the above code the issue is gone.

So does anyone have a reliable way to log all clicks across all forms/objects in my Delphi program without causing bad side effects? I want to intercept and log all clicks but not have any of the messages mysteriously eaten.

Edit: after the helpful comments below, this is the current working code.

procedure TForm1.ApplicationEvents1Message(var Msg: tagMSG;var Handled: Boolean);
var Target: TControl;
    Point: TPoint;
begin
     Handled := FALSE;
     if (Msg.Message = WM_LBUTTONDOWN) then
     begin
          Point.X := Msg.pt.X;
          Point.Y := Msg.pt.Y;
          Target := FindDragTarget(Point,True);
          if Assigned(Target) and Assigned(Target.Owner) then
          begin
               //use (Target).owner.name and (Target).name for logging here
          end;
     end;
end;
Some1Else
  • 715
  • 11
  • 26
  • 2
    Seems implausible. What happens when you remove various code in the function above. Start by removing the call to AddLogText – David Heffernan Dec 16 '17 at 08:50
  • Thank you so much for that simple idea I overlooked. Remarking that line fixed the problem, so not a Delphi issue. I will see what could be causing that in my log code, but for now this is a non-issue. – Some1Else Dec 16 '17 at 09:05
  • This is really debugging 101. Use techniques like this to identify and narrow down the source of your issue, then it is likely that the problem will become obvious. – David Heffernan Dec 16 '17 at 09:18
  • You can use Windows [Problem Steps Recorder](https://support.microsoft.com/en-us/help/22878/windows-10-record-steps). – Victoria Dec 16 '17 at 14:50
  • 3
    On a side note, `WM_LBUTTONDOWN` is a posted message, so there is a slight delay in receiving it. The mouse may have moved between the time the event is generated and the time you receive it. As such, you shouldn't be using `GetCursorPos()`, use the coordinates inside the message instead. – Remy Lebeau Dec 16 '17 at 16:56
  • Just to clarify, this means using Point.X := Msg.pt.X; Point.Y := Msg.pt.Y; rather than GetCursorPos . – Some1Else Dec 17 '17 at 00:23
  • No, [GetMessagePos](https://msdn.microsoft.com/en-us/library/windows/desktop/ms644938(v=vs.85).aspx). – Victoria Dec 17 '17 at 01:24
  • What syntax do I use to call GetMessagePos from within Delphi? The Msg.pt.X code above works fine. – Some1Else Dec 17 '17 at 02:42
  • I'm a little confused @Victoria. Docs say that `GetMessagePos` *retrieves the cursor position for the last message retrieved by the GetMessage function*. Who calls `GetMessage`? – saastn Dec 17 '17 at 07:17
  • @saastn the message loop does that – David Heffernan Dec 17 '17 at 09:19
  • @Victoria in this case, knowing the actual message, it makes sense to use the point supplied in the message params – David Heffernan Dec 17 '17 at 09:20
  • If commenting out `AddLogText` resolved the issue, that doesn't guarantee the problem is in that function. Commenting out the line also removed `(Target).owner.name` and `(Target).name`. The first of those violates _Law of Demeter_ and raises an important alarm bell: _Is `(Target).owner` always assigned? I doubt it; in which case you'll sometimes get access violations._ (You checked `Target`, but not `Target.Owner`.) – Disillusioned Dec 18 '17 at 00:35
  • Doesn't the "if Assigned(Target) then" handle if it assigned or not? Or should I be checking for if not equal to nil? – Some1Else Dec 18 '17 at 00:41
  • 1
    No, `Asigned(Target)` only checks ***Target***. It doesn't check Target ***.Owner***. You need an additional `Assigned(Target.Owner)` check. On a side note: _Do you handle non-mouse action? Your approach is not particularly robust for keyboard users._ – Disillusioned Dec 18 '17 at 00:46
  • OK, I now changed the check to "if Assigned(Target) and Assigned(Target.Owner) then". I never got access violations before with that check, but it is good to be sure. I am only tracking mouse clicks at this stage. The program is very mouse orientated. – Some1Else Dec 18 '17 at 00:50
  • 1
    Your use of the program is very mouse oriented, but you might be surprised about how other users approach it – David Heffernan Dec 18 '17 at 09:06
  • @David, sure, I missed the fact it's the `WM_LBUTTONDOWN` message :( My fault, sorry for misleading advice. – Victoria Dec 18 '17 at 21:51
  • @David. How would I add code to intercept key entry then? Could I have the same parent object tracking so I could then have eg "Typed x at form1.edit3.text"? – Some1Else Dec 19 '17 at 06:49

0 Answers0