Delphi - Rate of clicking TImage slow compared with TButton
Asked Answered
C

1

6

I have a form with both a TImage and a TButton control. I noticed the rate of responding to the OnClick event seemed a bit slow for the TImage (rapid clicking!) so I measured it. For 100+ clicks (and clicking as fast as I could, keeping the rate as consistent as I could for each control) I got the metrics: TButton: Average ~105-116ms TImage: Average ~220-235ms

I repeated this a few times with similar results. Why is the TImage processing clicks about half the rate of the TButton? Could it be slower to process the Windows message queue from WM_LBUTTON_DOWN to the OnClick event? Maybe it is swallowing Clicks if they are within N ms of the previous click?

There's doesn't seem to be anything in the properties of the TImage that affects this.

Note: Using Delphi 7 and the standard VCL controls here, if that is relevant.

EDIT: Here is some example code demonstrating how I timed things:

// Define variables (in class definition)
m_dwBtnClicks, m_dwImgClicks: DWORD;
m_dwLastBtnClickTicks, m_dwLastImgClickTicks: DWORD;
m_fTotalBtnClicksTicks, m_fTotalImgClicksTicks: Single;

// Initialise variables (in form's OnCreate event)
m_dwBtnClicks := 0;
m_dwImgClicks := 0;

m_dwLastBtnClickTicks := 0;
m_dwLastImgClickTicks := 0;

m_fTotalImgClicksTicks := 0.0;
m_fTotalImgClicksTicks := 0.0;

// OnClick events
procedure TfrmQwerty.btnClick(Sender: TObject);
var
    dwTime: DWORD;
begin
    // TButton click!
    Inc(m_dwBtnClicks);
    dwTime := GetTickCount();
    if (m_dwLastBtnClickTicks > 0) then
        m_fTotalBtnClicksTicks := (m_fTotalBtnClicksTicks + (dwTime - m_dwLastBtnClickTicks));

    m_dwLastBtnClickTicks := dwTime;
end;

procedure TfrmQwerty.imgClick(Sender: TObject);
var
    dwTime: DWORD;
begin
    // TImage click!
    Inc(m_dwImgClicks);
    dwTime := GetTickCount();
    if (m_dwLastImgClickTicks > 0) then
        m_fTotalImgClicksTicks := (m_fTotalImgClicksTicks + (dwTime - m_dwLastImgClickTicks));

    m_dwLastImgClickTicks := dwTime;
end;

// Some TTimer::OnTimer event to update the results on-screen
procedure TfrmQwerty.OnTextEntryTimer(Sender: TObject);
var
    fTime: Single;
begin
    // Stop the timer
    TextEntryTimer.Enabled := False;

    if (m_dwBtnClicks > 1) then
        begin
        fTime := m_fTotalBtnClicksTicks / m_dwBtnClicks;
        lblButtonClicks.Caption := Format('BtnClicks = %d [Avg = %.3fms]', [
            m_dwBtnClicks, fTime]);
        end;

    if (m_dwImgClicks > 1) then
        begin
        fTime := m_fTotalImgClicksTicks / m_dwImgClicks;
        lblImageClicks.Caption := Format('ImgClicks = %d [Avg = %.3fms]', [
            m_dwImgClicks, fTime]);
        end;

    // Restart the timer
    TextEntryTimer.Enabled := True;
end;
Colatitude answered 1/6, 2015 at 11:26 Comment(22)
What are you timing? Please show a short demo program that allows us to do the exact same timing.Neap
TButton uses BN_CLICKED system notification for triggering OnClick event whilst TImage only tracks mouse down/mouse up event pair, so I would not be surprised there can be difference.Buddhi
@DavidHeffernan: OK, some example code added to show how things were timed. Nothing fancy. Was replacing the TButton with a TImage on a QWERTY-style on-screen keyboard when I noticed the big difference.Colatitude
@TLama: Ok, interesting observation. I would have thought that a BN_CLICKED message is generated from a mouse down/up pair internally, so the difference is still of interest. Is this just an in-built limitation of the control, then, and not much one can do?Colatitude
BN_CLICKED will indeed be generated by the mouse down/up messages.Neap
@TLama: OK, update here based on TLama's observation: Instead of using the TImage::OnClick event, if I use the TImage::OnMouseUp event, I get back to similar timings for the TButton and the same level of responsiveness! Thanks TLama. If you knock up a quick answer about the BN_CLICKED message and suggest using OnMouseUp, I'll accept that answer.Colatitude
@David, question that might answer this topic is, why does not TButton have a double click event ? I think it simply does not need to wait for a decision if the event is going to be click or double click (due to the used button style it does not receive BN_DBLCLK notification). But it's just a wild guess. I've no time for deeper investigation right now.Buddhi
@Alain, try to significantly increase double click interval in your system mouse settings and check if the image click rate slows down as well.Buddhi
@TLama, aah you're thinking that the system is generating DBL_CLICK messages which are slowing down the TImage. Hmm, good thinking. Ok, will check that now.Colatitude
@Buddhi is almost certainly on to something hereNeap
More updates: By moving the "Double-click speed" slider to either end in the Windows Mouse properties I was NOT able to see a difference in TButton::OnClick. At a guess, internally TButton routes both single and double-clicks to the same event handler. On the other hand, TLama has spotted the TImage::DblClick event handler and this seems to be key. If I handle both OnClick and OnDblClick, I get back the same level of responsiveness. Curiously, though, handling just OnMouseUp feels much smoother than the pair of Click events. Just handling OnMouseUp seems to be the way to go here.Colatitude
@TLama: Seems like you've found the answer. Internally, TImage handles the double-click Windows message and that explains the observed behaviour. This article msdn.microsoft.com/en-us/library/aa931259.aspx (references Windows Mobile 6.5, think this applies to all Windows variants) explains what is going on. Thanks TLama! Do you want to knock an answer up, ending with the suggestion to use the OnMouseUp event instead?Colatitude
@Colatitude Do note that if you are only handling the OnMouseUp event it is possible that user clicks on one button (image) but moves the mouse over another before releasing the mouse button which would fire the OnMouseUp event of the later button (image) instead of the one that the click was started on which probably is not desirable.Portray
@SilverWarior: Aah yes, good observation! I could handle that in various ways (eg. by ensuring that an OnMouseDown event occurred within a few hundred ms prior to OnMouseUp). As this is intended for a touchscreen application, I can safely ignore this. But I will put a comment in the code about this limitation so the next developer is aware of it. Thanks!Colatitude
@Colatitude I think it would be best if you would try to find a way to prevent TImage from processing the double clicks by default. I'm not on my development machine now but I could go and take a look at this when i come home.Portray
No, timing is not relevant, you've gotta capture in mouse down so you know a mouse up is associated with it. Try deriving a new image without csDoubleclicks, or removing the style from the existing instance.Egide
@SilverWarior: Not sure how to go about that. The Delphi designer exposes the OnDblClick event by default, suggesting that the VCL has registered the TImage window class to receive double-click notifications (see this MSDN article: msdn.microsoft.com/en-us/library/windows/desktop/…). Could I sub-class TImage, and de-register the notification? Seems like a lot of work when simply handling OnMouseUp works!Colatitude
@SertacAkyuz: Aah, so you suggest using RegisterClass to remove the double-click notification from my specific TImage instance? Another interesting thought, will see if I can get this to work.Colatitude
@Alain - No, I'm talking about the control style. Image1.ControlStyle := Image1.ControlStyle - [csDoubleClicks];Egide
@SertacAkyuz: Brilliant, that works! No more WM_LBUTTONDBLCLK messages are sent to the control and therefore, I can handle the OnClick event as normal. BTW, this is what I meant by using "RegisterClass", just wasn't sure of the syntax. Thanks!Colatitude
@Colatitude To be perfectly accurate, those messages are sent to the control but they are handled as though they were mouse down messages instead.Amortizement
@J...:Aah yes, appreciate the correction. I meant that, need to be more precise with my language. Have learnt a bunch from this little issue, thanks everyone!Colatitude
A
5

The VCL source is your friend here. As noted, this is caused by double click messages being sent by Windows when clicking fast enough to generate them.

Let's look at what happens when clicking fast enough to trigger a double click:

Step 1 - Left mouse button goes down :

procedure TControl.WMLButtonDown(var Message: TWMLButtonDown);
begin
  SendCancelMode(Self);
  inherited;
  if csCaptureMouse in ControlStyle then
    MouseCapture := True;
  if csClickEvents in ControlStyle then  // !! Note here
    Include(FControlState, csClicked);   //    Storing that we've been clicked
  DoMouseDown(Message, mbLeft, []);
end;

Step 2 - Left mouse button goes up.

procedure TControl.WMLButtonUp(var Message: TWMLButtonUp);
begin
  inherited;
  if csCaptureMouse in ControlStyle then MouseCapture := False;
  if csClicked in ControlState then      // !! Note here
  begin                                  //    Firing CLICK event primed  
    Exclude(FControlState, csClicked);   //    from the method above
    if ClientRect.Contains(SmallPointToPoint(Message.Pos)) then
      Click;
  end;
  DoMouseUp(Message, mbLeft);
end;

Step 3 - Left mouse button goes down again.

This time, it's a double click! Note that this is handling an entirely different message - a double click message from the OS, not a mouse down message. The handler here still fires the MouseDown event, but does not prime the control to fire a click event when the mouse button comes back up.

procedure TControl.WMLButtonDblClk(var Message: TWMLButtonDblClk);
begin
  SendCancelMode(Self);
  inherited;
  if csCaptureMouse in ControlStyle then MouseCapture := True;
  if csClickEvents in ControlStyle then DblClick;
  DoMouseDown(Message, mbLeft, [ssDouble]);
end;

Since a Button is a special TWinControl it receives the special BN_CLICKED message that is generated any time the button is clicked, regardless of whether it might be a double click or not. Being a simple control it does a simple job and you therefore see twice as many click events from a button when clicking quickly (faster than the double-click rate).

procedure TCustomButton.CNCommand(var Message: TWMCommand);
begin
  if Message.NotifyCode = BN_CLICKED then Click;
end;

You can also note that, since a TButton will receive these special messages it is created without the csClickEvents option in its ControlStyle, so although it is also a TControl, the handling in the above steps used for the TImage (and other) controls does not apply (ie: priming for the Click in the WMLButtonDown handler).

As you have discovered, the OnMouseDown or OnMouseUp events will allow you to capture all such events in your TImage control, regardless of whether they should be treated as clicks or double clicks.


Alternatively, if you don't care about your TImage processing double clicks you can set the control style as :

 Image1.ControlStyle := Image1.ControlStyle - [csDoubleClicks];

Here, in the TControl.WndProc :

if not (csDoubleClicks in ControlStyle) then
  case Message.Msg of
    WM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, WM_MBUTTONDBLCLK:
      Dec(Message.Msg, WM_LBUTTONDBLCLK - WM_LBUTTONDOWN);
  end;

You can see the double click events are transformed to simple mouse down events.

Amortizement answered 1/6, 2015 at 12:42 Comment(5)
Click events happen on mouse up rather than mouse down.Neap
@DavidHeffernan I don't think I said otherwise. Unless you mean the double click, which definitely happens on mouse down.Amortizement
You said: As you have discovered, the OnMouseDown event will allow you to capture all mouse down events ... regardless of whether they should be treated as clicks or double clicks.Neap
I think that was a little mistype and OnMouseUp was intended. The meaning was clear, methinks...Colatitude
@DavidHeffernan Yeah, I changed that wording in case that was your objection - it's dicey as the double clicks are mousedowns and the clicks are mouseups. Hopefully it is more clear now.Amortizement

© 2022 - 2024 — McMap. All rights reserved.