5
votes

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;
1
What are you timing? Please show a short demo program that allows us to do the exact same timing.David Heffernan
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.TLama
@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.AlainD
@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.TLama
@Alain - No, I'm talking about the control style. Image1.ControlStyle := Image1.ControlStyle - [csDoubleClicks];Sertac Akyuz

1 Answers

5
votes

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.