I have a tall listbox with a variable number of items. It won't always be full. I know when a user doesn't select an item by this code:
if ( lstbox.ItemIndex = -1 ) then
ShowMessage('here');
But this does not work when I an Item is selected and then I click in the 'whitespace' of the listbox. How do I detect that sort of situation?
You can do this in a number of ways. One would be in the OnMouseDown event using the X and Y parameters to that event, which are the client co-ordinates in the listbox at which the user clicked:
procedure TMyForm.ListBox1MouseDown(Sender: TObject;
Button: TMouseButton;
Shift: TShiftState;
X, Y: Integer);
begin
if TListbox(Sender).ItemAtPos(Point(X, Y), TRUE) <> -1 then
// item was clicked
else
// 'whitespace' was clicked
end;
But this won't affect the behaviour in any OnClick event. If you need to perform this test in OnClick then you need to obtain the mouse position and convert it to the listbox client co-ordinates before doing the same test:
procedure TMyForm.ListBox1Click(Sender: TObject);
var
msgMousePos: TSmallPoint;
mousePos: TPoint;
begin
// Obtain screen co-ords of mouse at time of originating message
//
// Note that GetMessagePos returns a TSmallPoint which we need to convert to a TPoint
// in order to make further use of it
msgMousePos := TSmallPoint(GetMessagePos);
mousePos := SmallPointToPoint(msgMousePos);
mousePos := TListbox(Sender).ScreenToClient(mousePos);
if TListbox(Sender).ItemAtPos(mousePos, TRUE) <> -1 then
// item clicked
else
// 'whitespace' clicked
end;
NOTE: GetMessagePos() obtains the mouse position at the time of the most recently observed mouse message (which should in this case be the message that originated the Click event). However, if your Click handler is invoked directly then the mouse position returned by GetMessagePos() is likely to have little or no relevance to the processing in the handler. If any such direct invocation might sensibly utilise the current mouse position, then this may be obtained using GetCursorPos().
GetCursorPos() is also much more straightforward to use as it obtains the mouse position in a TPoint value directly, avoiding the need to convert from TSmallPoint:
GetCursorPos(mousePos);
Either way, the fact that your handler is dependent upon mouse position in any way makes directly invoking this event handler problematic so if this is a consideration then you might wish to isolate any position independent response in the event handler into a method that can be explicitly invoked if/when required, independently of the mouse interaction with the control.
Related
I am trying to implement a feature similar to most media players where by moving your mouse above the media duration track bar it will display a small popup informing you on the time your mouse is currently above. I noticed an odd behaviour though while implementing the code given below.
procedure TForm1.TrackBar1MouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Single);
var
pers: Extended;
begin
pers := (X/TrackBar1.Width);
PixelLabel.Text := FloatToStr(pers * TrackBar1.Max);
end;
If I click in the middle of the track bar I will get a value quite close to the actual track bar value at that point, so if for example the track bar range goes from 0 to 2000 and I click somewhere in the middle I get 1000, but as I move to the left or right I start to get smaller and bigger values respectively. So if my mouse is close to the start for example I might get 180 instead of 100 that should be the actual value at that point. Can someone point out what is it that I am doing wrong here?
EDIT
By actual track bar value what I mean is the value derived from:
procedure TForm1.TrackBar1Change(Sender: TObject);
begin
ActualLabel.Text := 'Actual Val: '+FloatToStr(TrackBar1.Value);
end;
So I will move the mouse lets say at position 308 (the track bar goes from 0 to 609 here) and I will get a perch value of 0.50574 that is telling me that the value of the track bar under the mouse at position 308 is 10114, but by clicking the mouse and firing up the onChange function I get a value of 10116. This difference sends to increase as we go further from the middle of the track bar to either one of its sides.
EDIT 2
A clearer example would be this. As seen in the image below I move the mouse at position X=572. This position if expressed as a percentage for the whole track bar would be 572/609 = 0,9392. So one would expect that the percentage of the value of the track bar at that position (min:0 - max:200 as the image shows) would be the same. In other words, MValue/max = 0,9392.
But after clicking the track bar at that exact position and then requesting its value it won't return what I computed as 'MValue' as the image below shows (Mouse is not visible but this image is indeed after I clicked the track bar in the same position, as you can see the ActualValue was updated)
Problem 1
Your calculation of value does not match that used by the control. The control does it with this code which can be found in FMX.StdCtrls:
function PosToValue(MinValue, MaxValue, ViewportSize, ThumbSize, TrackSize,
Pos: Single; IgnoreViewportSize: boolean): Single;
var ValRel: Double;
begin
Result := MinValue;
if (ViewportSize < 0) or IgnoreViewportSize then
ViewportSize := 0;
ValRel := TrackSize - ThumbSize;
if ValRel > 0 then
begin
ValRel := (Pos - ThumbSize / 2) / ValRel;
if ValRel < 0 then
ValRel := 0;
if ValRel > 1 then
ValRel := 1;
Result := MinValue + ValRel * (MaxValue - MinValue - ViewportSize);
end;
end;
The difference is that the code here makes an allowance for the thumb size. You formula is equivalent to calling this function and passing a value of 0 for ThumbSize.
If you want to replicate the behaviour of the control, then you'll need to use this algorithm also. You'll need the protected hack to crack the class.
type
THackedTrackBar = class(TTrackBar);
procedure TForm1.TrackBar1MouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Single);
var
tb: THackedTrackBar;
begin
tb := THackedTrackBar(TrackBar1);
Label1.Text := FloatToStr(
PosToValue(
tb.Min,
tb.Max,
tb.ViewportSize,
tb.GetThumbSize(tb.FIgnoreViewportSize),
tb.Width,
X,
tb.FIgnoreViewportSize
)
);
end;
Problem 2
OnMouseMove does not fire when the cursor is over the track thumb. This seems to be a basic limitation of the FMX TTrackBar control. The underlying framework is obviously aware that the cursor is over the thumb because it paints it in a different colour, the so-called hot-tracking effect. However, the framework appears to do this to the detriment of letting you know that the mouse is moving.
The thumb on the track bar is implemented as a separate object. It's an object of type TThumb. The TTrackBar control exposes the object via a protected property. You can use the protected hack to get hold of the thumb object and then set its OnMouseMove event handler. Not a whole lot of fun, but certainly one way to work around the issue.
For your initial problem you will need to dig into the style to find the exact amount of left and right padding around the track bar. Note though that this will vary depending on the platform, style and Delphi version.
You might consider using your own style so you know everything will be constant.
To solve the issue raised by David Heffernan you could put a transparent control over the TTrackBar (make it a client aligned child of the track bar), intercept mouse events, do your own processing and pass them down to the track bar.
I want to get the absolute co-ordinates of the mouse when the mouse is over a control which has been placed on a host control. E.g. Host control is a panel a button is placed on the panel. I want to get the mouse co-ordinates relative to the panel when the mouse is over the button.
I have tried the obvious just to see what I get:
procedure TfmWorkingScreen.pnlScreenAreaMouseMove(Sender: TObject;
Shift: TShiftState; X, Y: Integer);
begin
StatusBar1.SimpleText := 'Left ' + IntToStr(X) + ' Right ' + IntToStr(Y);
end;
Clearly this will only work when the mouse is over the panel control. Is there a way to get the required co-ordinates?
Add an OnMouseMove event handler to the child control, the button in your example. In that OnMouseMove event handler you receive the X and Y cursor coordinates with respect to the child control's client area. If the host control is the immediate parent of the control whose OnMouseMove event has fired you use the control's ClientToParent method:
var
PosRelParent: TPoint:
....
PosRelParent := (Sender as TControl).ClientToParent(Point(X, Y));
If the parent control may be further up the parent/child relationship you can pass the parent control to ClientToParent:
PosRelParent := (Sender as TControl).ClientToParent(Point(X, Y), TheParent);
If you wish to express the position relative to some arbitrary controls client area then you can do so by converting via screen coordinates, a global frame of reference.
var
PosRelScreen, PosRelOtherControl: TPoint:
OtherControl: TWinControl;
....
PosRelScreen := (Sender as TControl).ClientToScreen(Point(X, Y));
PosRelOtherControl := OtherControl.ScreenToClient(PosRelScreen);
As one final offering, you can use GetMessagePos to obtain the screen relative coordinates of the mouse for the last message retrieved by a call to GetMessage.
var
MsgPos: TPoint;
....
MsgPos := TSmallPoint(GetMessagePos());
At this point you can use SomeControl.ScreenToClient(MsgPos) to get the coordinates of the cursor relative to that control's client area. Obviously it only makes sense to call GetMessagePos if you are in an event handler triggered by a queued mouse message.
If you want to listen to your mouse outside your program aswell you need to create a mouse hook.
I'ver earlier summitted a article and source code to About.com : http://delphi.about.com/od/windowsshellapi/a/delphi-hooks.htm
If this have you interest I would love to go deeper on then subject.
My Delphi 7 app has two TPageControls with a TSplitter between them. On each TPageControl are two TTabSheets. One each TTabSheet is a TWebBrowser. Got the picture?
The problem with this component arrangement is that it is impossible to track the location of the mouse since the TWebBrowser does not have a OnMouseMove event and the TForm's OnMouseMove event is never triggered under this pile of ClientAligned components.
What I need to know is the XY position of the mouse, relative to the app's form, at all times. IOW, I need to know when the mouse moved, and when it does, a function that would:
GetMouseLocationNow(var X, Y : Integer);
How can I do this?
To track mouse move application-wide, you have to track WM_MOUSEMOVE message. You can use TApplicationEvents component for that. So, drop TApplicationEvents on form, and process WM_MOUSEMOVE in OnMessage event. Low order word in LParam specifies X coordinate of the cursor (relative to the window that the message is posted to), and high order word Y coordinate.
procedure TfrmMain.ApplicationEventsMessage(var Msg: tagMSG; var Handled: Boolean);
var
Pt: TPoint;
begin
if Msg.message = WM_MOUSEMOVE then begin
Pt := Point(WORD(Msg.lParam), HiWord(Msg.lParam));
windows.ClientToScreen(Msg.hwnd, Pt);
windows.ScreenToClient(Handle, Pt);
MouseMoved(Pt.X, Pt.Y);
end;
end;
procedure TfrmMain.MouseMoved(const AX, AY: Integer);
begin
// do the work here
end;
In a TImage's OnClick event, I would like to extract the x,y coordinates of the mouse. I would prefer them in relation to the image, but in relation to the form or window is just as good.
Mouse.CursorPos contains the TPoint, which in turn contains the X and Y position. This value is in global coordinates, so you can translate to your form by using the ScreenToClient routine which will translate screen coordinates to window coordinates.
According to the Delphi help file, Windows.GetCursorPos can fail, Mouse.CursorPos wraps this to raise an EOsException if it fails.
var
pt : tPoint;
begin
pt := Mouse.CursorPos;
// now have SCREEN position
Label1.Caption := 'X = '+IntToStr(pt.x)+', Y = '+IntToStr(pt.y);
pt := ScreenToClient(pt);
// now have FORM position
Label2.Caption := 'X = '+IntToStr(pt.x)+', Y = '+IntToStr(pt.y);
end;
The Mouse.CursorPos property will tell you the current position of the mouse. If the computer is running sluggishly, or if your program is slow to respond to messages, then it might not be the same as the position the mouse had when the OnClick event first occurred. To get the position of the mouse at the time the mouse button was clicked, use GetMessagePos. It reports screen coordinates; translate to client coordinates with TImage.ScreenToClient.
The alternative is to handle the OnMouseDown and OnMouseUp events yourself; their parameters include the coordinates. Remember that both events need to occur in order for a click to occur. You may also want to detect drag operations, since you probably wouldn't want to consider a drag to count as a click.
As others have said, you can use Mouse.CursorPos or the GetCursorPos function, but you can also just handle the OnMouseDown or OnMouseUp event instead of OnClick. This way you get your X and Y values as parameters to your event handler, without having to make any extra function calls.
How about this?
procedure TForm1.Button1Click(Sender: TObject);
var
MausPos: TPoint;
begin
GetCursorPos(MausPos);
label1.Caption := IntToStr(MausPos.x);
label2.Caption := IntToStr(MausPos.y);
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
SetCursorPos(600, 600);
end;
Found this online somewhere once and saved it in my codesnippet DB :)
This page will probably solve all your questions however... There appear to be functions to go from client to screen coordinates and back etc..
Good luck!
To Firemonkey (FMX):
var
p: TPointF;
begin
p := Screen.MousePos;
end;
In a Delphi 7 application, I want to move a component following the mouse. I'm doing something like this:
procedure MyComponent.MouseMove(Sender: TObject;Shift: TShiftState; X, Y: Integer);
begin
AnotherComponent.Top := X;
AnotherComponent.Left := Y;
end;
When I move the mouse the CPU usage for the main core goes up to 100% on a recent PC.
Any idea or tick to reduce the CPU usage in this case ?
You could create a TTimer that polls the current mouse position every 0.10 seconds or so, then positions "AnotherComponent" according to the current mouse position.
Then you wouldn't fire your event for every pixel of mouse movement- you won't need any OnMouseMove event on your controlling component at all.
On my computer, this basically has no performance impact at all.
procedure TForm1.Timer1Timer(Sender: TObject);
var
pt: TPoint;
begin
//Is the cursor inside the controlling component? if so, position some
//other control based on that mouse position.
GetCursorPos(pt);
if MouseWithin(pt.x,pt.y,MyComponent,Form1.Left,Form1.Top) then begin
//replace with whatever real positioning logic you want
AnotherComponent.Top := pt.y;
AnotherComponent.Left := pt.x;
end;
end;
function TForm1.MouseWithin(mouseX, mouseY: integer;
const comp: TWinControl; const ParentWindowLeft: integer;
const ParentWindowTop: integer): boolean;
var
absoluteCtrlX, absoluteCtrlY: integer;
begin
//take a control, and the current mouse position.
//tell me whether the cursor is inside the control.
//i could infer the parent window left & top by using ParentwindowHandle
//but I'll just ask the caller to pass them in, instead.
//get the absolute X & Y positions of the control on the screen
//needed for easy comparison to mouse position, which will be absolute
absoluteCtrlX := comp.Left + ParentWindowLeft;
absoluteCtrlY := comp.Top + ParentWindowTop +
GetSystemMetrics(SM_CYCAPTION);
Result := (mouseX >= absoluteCtrlX)
and (mouseX < absoluteCtrlX + comp.Width)
and (mouseY >= absoluteCtrlY)
and (mouseY <= absoluteCtrlY + comp.Height);
end;
Finally I've change my code for this one:
procedure MyComponent.MouseMove(Sender: TObject;Shift: TShiftState; X, Y: Integer);
begin
if GetTickCount-LastMoveTick>50 then begin
AnotherComponent.Top := Y;
AnotherComponent.Left := X;
LastMoveTick := GetTickCount;
end;
end;
Really easy to implement (2 lines added), no timer, works well for me...
It has nothing to do with the Mouse Move itself.
Unless it's what you intended, you are mismatching X, Y with Top, Left. Top is the Y coord and Left the X one.
The problem is the actual moving of AnotherComponent.
To try and understand it, I suggest that you write a TestMove routine that moves your AnotherComponent automatically with adjustable repetition/delays to monitor the CPU.
I bet it triggers a costly repaint or some other CPU intensive calculation.
So Examine closely if you have any event handler on this component first, then go with the inherited behavior...
Maybe, instead of moving the component itself you move a 'shadow' and only move the component once the user lets the mousebutton go. Sort of like drag&drop.
It can't be the move itself that needs so much cpu power, most probably the move causes the component to redraw itself somehow.
Can you avoid that AnotherComponent is redrawn on each move? It should not be necessary, unless it is a movie container.
Anything tied to the mouse move event will be called very frequently as mice are a high resolution input device. I wouldn't worry about the cpu usage though because your handler only gets fired as fast as possible based on how busy the system is. In other words, it's only maxing the CPU because nothing else is.
From MSDN:
The mouse generates an input event
when the user moves the mouse, or
presses or releases a mouse button.
The system converts mouse input events
into messages and posts them to the
appropriate thread's message queue.
When mouse messages are posted faster
than a thread can process them, the
system discards all but the most
recent mouse message.
Now there may be some exceptions to this. You could do some testing to be sure by running some other processing intensive activity and see how much the mouse move stuff impacts it.