Delphi properly position balloon hint associated with a listview item - delphi

How do I ensure a balloon hint that I want to associate with a listview item is properly positioned so that it is next to the item in question, and always shows the full ballon text on screen?
For example, if I enter an invalid character when editing a file name in Windows Explorer, a balloon pops up saying what the invalid characters are. The entire balloon is always on screen even if the list item is near a screen edge or partially off screen. The tail is always positioned at the middle bottom of the list item. The bubble is usually to the bottom right of the tail, but may be above it or to the left if the list item is near the bottom and/or right edges of the screen.
Primarily, I am unable to get the bubble and tail to stay close to the list item.
procedure TForm1.ListEdited(Sender: TObject; Item: TListItem;
var S: string);
var
AHint: string;
R: TRect;
B : TBalloonHint;
begin
if TRegEx.IsMatch(S, '[\\/:*?"<>|]') then
begin
AHint := 'A file name cannot contain any of the following' + sLineBreak +
'characters: \/:*?"<>|';
R := Item.DisplayRect(drBounds);
R.TopLeft := ClientToScreen(R.TopLeft);
R.BottomRight := ClientToScreen(R.BottomRight);
B := TBalloonHint.Create(Self);
B.Description := AHint;
B.HideAfter := 5000;
B.ShowHint(R);
S := TRegEx.Replace(S, '[\\/:*?"<>|]', '');
end;
end;
I've tried the various overloads of ShowHint, as well as the JEDI balloon hint component. I've also adjusted the Top property of the rectangle, which may position the balloon better when the item is in a certain area of the screen, but the ballon is then off position when the item is on some other part of the screen.
Delphi 10.3 Rio, Win 7 x64.

DisplayRect gives client coordinates relative to the listview containing the item, not the form. Hence when converting to screen coordinates, you have to use the listview as the base, not the form:
R := Item.DisplayRect(drBounds);
R.TopLeft := ListView1.ClientToScreen(R.TopLeft); // <--
R.BottomRight := ListView1.ClientToScreen(R.BottomRight); // <--

Related

Is there a way to turn off the Caption on a TDBRadioGroup

I have a TDBRadioGroup that I've added to my form.
I'd really like to have the caption to the left of it instead of on top (the form's a little busy and tall, and I'm trying to squeeze it in).
I can add my own label to the left of the Radio Group. But the control insists on reserving space of a Caption that does not exists. Is there a way I can turn it off completely?
The best we've come up with so far is sticking it on a TPanel and then hiding the top couple lines off-panel.
A TGroupBox (and it's descendant TDBGroupBox) are basically wrappers around the Windows GroupBox. The control is designed to sport a user-defined label across the upper-left corner, and doesn't have any style setting to remove it.
So, short of creating your own control to host a series of TRadioButton controls yourself and display them, there's no built-in way to disable the space reserved for the caption. You can suppress the text, of course, by setting the Caption := '', but the padding for the text descenders is not removed simply because the caption isn't displayed.
You can override the paint procedure for TRadioGroup so that the frame is drawn closer to the top of your item list. You could create a new component of type TNoCaptionRadioGroup. You might still have to use the panel trick that you have tried, but by lowering the top of the frame you can grab the space consumed by the non-existent caption. Something like this:
tNoCaptionRadioBox = class(TRadioGroup)
protected
procedure paint; override;
end;
procedure tNoCaptionRadioBox.paint;
var
H: Integer;
R: TRect;
begin
with Canvas do
begin
Font := Self.Font;
H := TextHeight('0');
R := Rect(0, H, Width, Height);
if Ctl3D then
begin
Inc(R.Left);
Inc(R.Top);
Brush.Color := clBtnHighlight;
FrameRect(R);
OffsetRect(R, -1, -1);
Brush.Color := clBtnShadow;
end else
Brush.Color := clWindowFrame;
FrameRect(R);
end;
end;
This is taken from the code for painting a TCustomGroupBox. I have removed the code for drawing the caption and have changed the top of the frame to the full height of the Font. Your actual captioned radio buttons will still be drawn where Windows wants them to be and with the default spacing.
Remember to register the new component by running the package installation tool.
procedure Register;
begin
RegisterComponents('myComponents', [tNoCaptionRadioBox]);
end;

Positioning A Form From A SysTray Icon

I'd like to display a form off of a systray icon event, which just shows some information next to the taskbar and disappears itself after some time. The main issue I'm running into is positioning the form in a way that it is both in the correct position and visible. I found a couple of ways to determine where the icon is, but in testing I found them inconsistent based on OS (I attempted this in another app and ended up giving up and using a centered modal form). For example:
procedure GetWorkAreaRect(var outrect: TRect);
// returns the dimensions of the work area.
begin
Systemparametersinfo(SPI_GETWORKAREA, 0, #outrect, 0);
end;
The problem when that works is determining from there where to put the form (above, below, left, right). Any suggestions?
Edit: The problem is in positioning the form in relationship to the system tray icon, not necessarily determining where the system tray icon is. I made another attempt and got it working as long as some conditions are met. Most notably, it doesn't work if the taskbar is set to auto-hide, because the assumption is made that the click is made outside of the work area. This is not true when the form is set to auto-hide.
function PositionForm(X, Y, Width, Height: Integer): TPoint;
// receives mouse-click position in X and Y, form width and height in width and height
// returns Left and Top in TPoint.X and TPoint.Y.
var
workrect: TRect;
resrect: TPoint;
begin
GetWorkAreaRect(workrect);
if Y > WorkRect.Bottom then // taskbar is on bottom
begin
resRect.X := WorkRect.Right - Width;
resrect.Y := WorkRect.Bottom - Height;
end
else
if X > WorkRect.Right then // taskbar is on right
begin
resrect.X := WorkRect.Right - Width;
resrect.Y := WorkRect.Bottom - Height;
end
else
if X < WorkRect.Left then // taskbar is on left
begin
resrect.X := WorkRect.Left;
resrect.Y := WorkRect.Bottom - Height;
end
else
if Y < WorkRect.Top then // taskbar is on top
begin
resrect.X := WorkRect.Right - Width;
resrect.Y := WorkRect.Top;
end;
Result := ResRect;
end;
So on the surface, it seems the issue is to find an independent way to determine where the taskbar resides...or could the logic be extended above to take care of this?
When your notification icon receives the message corresponding to an action, you can query at that point to find out an associated point on the screen.
For example if you are handling WM_RBUTTONUP, WM_CONTEXTMENU etc. in your icon's message procedure you can call GetMessagePos to find out the position on the icon associated with the message.
I wrap this up with the following function so that I can decode the message into a TPoint:
function MessagePoint: TPoint;
begin
Result := TSmallPoint(GetMessagePos());
end;
So what you can do is, in your icon's message procedure, make a note of this point. When you need to show the form, use this point to determine where your icon lives. Since the point can be in the taskbar, you'll need to clip it into the work area.
After your question update it seems you want to know how to find out the location of the taskbar. Do that by calling SHAppBarMessage passing ABM_GETTASKBARPOS.
Windows does not expose a native way to query where system tray icons are positioned, or even if they are visible at all. But you can do it manually with some lower level API trickery, as demonstrated in the following article:
CTrayIconPosition - where is my tray icon?
That works up to XP, at least, maybe even Vista. Windows 7 drastically redesigned the way the system tray acts, so I do not know if these techniques still work anymore.
You can use TJvDesktopAlert to display simple notifications, if you have JCL and JVCL.
procedure TForm1.ShowDesktopAlert(const AHeader, AMessage: string);
begin
with TJvDesktopAlert.Create(nil) do
begin
StyleOptions.DisplayDuration := 5000;
HeaderText := AHeader;
MessageText := AMessage;
AutoFree := True;
Execute;
end;
end;

How to Clip Text in a TTreeview (Continued)

I have taken the advice, or at least I think I have, that I got in my previous question.
To summarize, I replaced the THTMLTreeList from TMS with a TTreeList and made two columns in it. I set the first column to 150 pixels and I overrode the AdvancedCustomDrawItem event with this code:
procedure TForm1.trXMLAdvancedCustomDrawItem(Sender: TCustomTreeView;
Node: TTreeNode; State: TCustomDrawState; Stage: TCustomDrawStage;
var PaintImages, DefaultDraw: Boolean);
var hContext: HDC;
s: PChar;
iLength: Integer;
uRect: TRect;
begin
DefaultDraw := False;
hContext := trXML.Canvas.Handle;
s := PChar(Node.Text);
iLength := Length(Node.Text);
uRect := Node.DisplayRect(True);
DrawText(hContext, s, iLength, uRect, DT_END_ELLIPSIS);
end;
I got the result I expected, that is, when I draw the tree, the text in the first column is clipped. And when I change the size of the first column, the text is appropriately clipped. But when the TreeList is not wide enough and has a scroll bar on the bottom and I scroll to the right, the text now extends into the second column by the same amount as the scroll bar has been moved. It's like the text is drawn relative to the client area of the TreeList rather than the partially hidden first column so it always extends 150 pixels into the TreeList.
It seems to me that I am using the wrong thing for the device context handle or the TRect but I do very little graphic type programming so I don't know what to change. Any help would be appreciated.
I just checked TMS's source code and
TTreeList already uses the DT_END_ELLIPSIS flag
and does per-column clipping of drawn text when the DefaultDraw parameter is set to True, so you don't need to draw the text yourself manually.

How to make a panel appear when the mouse moves over it? delphi

How can I make a panel appear with everything that is in it when I move my mouse over its location?
When I move it off again, it fades back out?
Doing it when it is visible is not a problem (except the fading out), I can do this with onmouseleaves.
But when it is invisible how do you make it visible?
thankssss
Put the panel on another (blank) panel. Make the "magic" panel show up when you get mouse movement over the blank panel.
Edited, because I now learned the OP has the Panel over a WebBrowser. My solution of placing an dummy / blank panel no longer works; Interfering with mouse messages going to the WebBrowser is also not a good idea, so here's a simple way to fix this. I'm using an TTimer with it's interval set to "100" and I'm pooling the mouse coordinates.
procedure TForm25.Timer1Timer(Sender: TObject);
var PR: TRect; // Panel Rect (in screen coordinates)
CP: TPoint; // Cursor Position (always in screen coordinates)
begin
// Get the panel's coordinates and convert them to Screen coordinates.
PR.TopLeft := Panel1.ClientToScreen(Panel1.ClientRect.TopLeft);
PR.BottomRight := Panel1.ClientToScreen(Panel1.ClientRect.BottomRight);
// Get the mouse cursor position
CP := Mouse.CursorPos;
// Is the cursor over the panel?
if (CP.X >= PR.Left) and (CP.X <= PR.Right) and (CP.Y >= PR.Top) and (CP.Y <= PR.Bottom) then
begin
// Panel should be made visible
Panel1.Visible := True;
end
else
begin
// Panel should be hidden
Panel1.Visible := False;
end;
end;
If you have an area that your panel will appear in, you can capture the mouse move event for the underlying form or parent panel and check it is within the bounds that your invisible panel will appear in.
eg. (pseudocode)
procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
begin
if ((X > MyPanel.Left) and (Y > MyPanel.Top) and (X < mypanel.right) and
(Y < mypanel.bottom)) then
begin
mypanel.visible := true;
end;
end;

How do I get the coordinates of the mouse when a control is clicked?

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;

Resources