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;
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.