Update on 2014-08-30:
- Fixed bug preventing multiline ToolTips.
- Added support for icons.
- Added named function parameters.
Code: Select all
; ======================================================================================================================
; ToolTipEx() Display ToolTips with custom fonts and colors.
; Code based on the original AHK ToolTip implementation in Script2.cpp.
; Tested with: AHK 1.1.15.04 (A32/U32/U64)
; Tested on: Win 8.1 Pro (x64)
; Change history:
; 1.1.01.00/2014-08-30/just me - fixed bug preventing multiline tooltips.
; 1.1.00.00/2014-08-25/just me - added icon support, added named function parameters.
; 1.0.00.00/2014-08-16/just me - initial release.
; Parameters:
; Text - the text to display in the ToolTip.
; If omitted or empty, the ToolTip will be destroyed.
; X - the X position of the ToolTip.
; Default: "" (mouse cursor)
; Y - the Y position of the ToolTip.
; Default: "" (mouse cursor)
; WhichToolTip - the number of the ToolTip.
; Values: 1 - 20
; Default: 1
; HFONT - a HFONT handle of the font to be used.
; Default: 0 (default font)
; BgColor - the background color of the ToolTip.
; Values: RGB integer value or HTML color name.
; Default: "" (default color)
; TxColor - the text color of the TooöTip.
; Values: RGB integer value or HTML color name.
; Default: "" (default color)
; HICON - the icon to display in the upper-left corner of the TooöTip.
; This can be the number of a predefined icon (1 = info, 2 = warning, 3 = error - add 3 to
; display large icons on Vista+) or a HICON handle. Specify 0 to remove an icon from the ToolTip.
; Default: "" (no icon)
; CoordMode - the coordinate mode for the X and Y parameters, if specified.
; Values: "C" (Client), "S" (Screen), "W" (Window)
; Default: "W" (CoordMode, ToolTip, Window)
; Return values:
; On success: The HWND of the ToolTip window.
; On failure: False (ErrorLevel contains additional informations)
; ======================================================================================================================
ToolTipEx(Text:="", X:="", Y:="", WhichToolTip:=1, HFONT:="", BgColor:="", TxColor:="", HICON:="", CoordMode:="W") {
; ToolTip messages
Static ADDTOOL := A_IsUnicode ? 0x0432 : 0x0404 ; TTM_ADDTOOLW : TTM_ADDTOOLA
Static BKGCOLOR := 0x0413 ; TTM_SETTIPBKCOLOR
Static MAXTIPW := 0x0418 ; TTM_SETMAXTIPWIDTH
Static SETMARGN := 0x041A ; TTM_SETMARGIN
Static SETTHEME := 0x200B ; TTM_SETWINDOWTHEME
Static SETTITLE := A_IsUnicode ? 0x0421 : 0x0420 ; TTM_SETTITLEW : TTM_SETTITLEA
Static TRACKACT := 0x0411 ; TTM_TRACKACTIVATE
Static TRACKPOS := 0x0412 ; TTM_TRACKPOSITION
Static TXTCOLOR := 0x0414 ; TTM_SETTIPTEXTCOLOR
Static UPDTIPTX := A_IsUnicode ? 0x0439 : 0x040C ; TTM_UPDATETIPTEXTW : TTM_UPDATETIPTEXTA
; Other constants
Static MAX_TOOLTIPS := 20 ; maximum number of ToolTips to appear simultaneously
Static SizeTI := (4 * 6) + (A_PtrSize * 6) ; size of the TOOLINFO structure
Static OffTxt := (4 * 6) + (A_PtrSize * 3) ; offset of the lpszText field
Static TT := [] ; ToolTip array
; HTML Colors (BGR)
Static HTML := {AQUA: 0xFFFF00, BLACK: 0x000000, BLUE: 0xFF0000, FUCHSIA: 0xFF00FF, GRAY: 0x808080, GREEN: 0x008000
, LIME: 0x00FF00, MAROON: 0x000080, NAVY: 0x800000, OLIVE: 0x008080, PURPLE: 0x800080, RED: 0x0000FF
, SILVER: 0xC0C0C0, TEAL: 0x808000, WHITE: 0xFFFFFF, YELLOW: 0x00FFFF}
; -------------------------------------------------------------------------------------------------------------------
; Init TT on first call
If (TT.MaxIndex() = "")
Loop, 20
TT[A_Index] := {HW: 0, IC: 0, TX: ""}
; -------------------------------------------------------------------------------------------------------------------
; Check params
TTTX := Text
TTXP := X
TTYP := Y
TTIX := WhichToolTip = "" ? 1 : WhichToolTip
TTHF := HFONT = "" ? 0 : HFONT
TTBC := BgColor
TTTC := TxColor
TTIC := HICON
TTCM := CoordMode = "" ? "W" : SubStr(CoordMode, 1, 1)
If TTXP Is Not Digit
Return False, ErrorLevel := "Invalid parameter X-position!", False
If TTYP Is Not Digit
Return False, ErrorLevel := "Invalid parameter Y-Position!", False
If (TTIX < 1) || (TTIX > MAX_TOOLTIPS)
Return False, ErrorLevel := "Max ToolTip number is " . MAX_TOOLTIPS . ".", False
If (TTHF) && !(DllCall("Gdi32.dll\GetObjectType", "Ptr", TTHF, "UInt") = 6) ; OBJ_FONT
Return False, ErrorLevel := "Invalid font handle!", False
If TTBC Is Integer
TTBC := ((TTBC >> 16) & 0xFF) | (TTBC & 0x00FF00) | ((TTBC & 0xFF) << 16)
Else
TTBC := HTML.HasKey(TTBC) ? HTML[TTBC] : ""
If TTTC Is Integer
TTTC := ((TTTC >> 16) & 0xFF) | (TTTC & 0x00FF00) | ((TTTC & 0xFF) << 16)
Else
TTTC := HTML.HasKey(TTTC) ? HTML[TTTC] : ""
If !InStr("CSW", TTCM)
Return False, ErrorLevel := "Invalid parameter CoordMode!", False
; -------------------------------------------------------------------------------------------------------------------
; Destroy the ToolTip window, if Text is empty
TTHW := TT[TTIX].HW
If (TTTX = "") && (TTHW) {
If DllCall("User32.dll\IsWindow", "Ptr", TTHW, "UInt")
DllCall("User32.dll\DestroyWindow", "Ptr", TTHW)
TT[TTIX] := {HW: 0, TX: ""}
Return True
}
; -------------------------------------------------------------------------------------------------------------------
; Get the virtual desktop rectangle
SysGet, X, 76
SysGet, Y, 77
SysGet, W, 78
SysGet, H, 79
DTW := {L: X, T: Y, R: X + W, B: Y + H}
; -------------------------------------------------------------------------------------------------------------------
; Initialise the ToolTip coordinates. If either X or Y is empty, use the cursor position for the present.
PT := {X: 0, Y: 0}
If (TTXP = "") || (TTYP = "") {
VarSetCapacity(Cursor, 8, 0)
DllCall("User32.dll\GetCursorPos", "Ptr", &Cursor)
Cursor := {X: NumGet(Cursor, 0, "Int"), Y: NumGet(Cursor, 4, "Int")}
PT := {X: Cursor.X + 16, Y: Cursor.Y + 16}
}
; -------------------------------------------------------------------------------------------------------------------
; If either X or Y is specified, get the position of the active window considering CoordMode.
Origin := {X: 0, Y: 0}
If ((TTXP <> "") || (TTYP <> "")) && ((TTCM = "W") || (TTCM = "C")) { ; if (*aX || *aY) // Need the offsets.
HWND := DllCall("User32.dll\GetForegroundWindow", "UPtr")
If (TTCM = "W") {
WinGetPos, X, Y, , , ahk_id %HWND%
Origin := {X: X, Y: Y}
}
Else {
VarSetCapacity(OriginPT, 8, 0)
DllCall("User32.dll\ClientToScreen", "Ptr", HWND, "Ptr", &OriginPT)
Origin := {X: NumGet(OriginPT, 0, "Int"), Y: NumGet(OriginPT, 0, "Int")}
}
}
; -------------------------------------------------------------------------------------------------------------------
; If either X or Y is specified, use the window related position for this parameter.
If (TTXP <> "")
PT.X := TTXP + Origin.X
If (TTYP <> "")
PT.Y := TTYP + Origin.Y
; -------------------------------------------------------------------------------------------------------------------
; Create and fill a TOOLINFO structure.
TT[TTIX].TX := "T" . TTTX ; prefix with T to ensure it will be stored as a string in either case
VarSetCapacity(TI, SizeTI, 0) ; TOOLINFO structure
NumPut(SizeTI, TI, 0, "UInt")
NumPut(0x0020, TI, 4, "UInt") ; TTF_TRACK
NumPut(TT[TTIX].GetAddress("TX") + (1 << !!A_IsUnicode), TI, OffTxt, "Ptr")
; -------------------------------------------------------------------------------------------------------------------
; If the ToolTip window doesn't exist, create it.
If !(TTHW) || !DllCall("User32.dll\IsWindow", "Ptr", TTHW, "UInt") {
; ExStyle = WS_TOPMOST, Style = TTS_NOPREFIX | TTS_ALWAYSTIP
TTHW := DllCall("User32.dll\CreateWindowEx", "UInt", 8, "Str", "tooltips_class32", "Ptr", 0, "UInt", 3
, "Int", 0, "Int", 0, "Int", 0, "Int", 0, "Ptr", A_ScriptHwnd, "Ptr", 0, "Ptr", 0, "Ptr", 0)
DllCall("User32.dll\SendMessage", "Ptr", TTHW, "UInt", ADDTOOL, "Ptr", 0, "Ptr", &TI)
DllCall("User32.dll\SendMessage", "Ptr", TTHW, "UInt", MAXTIPW, "Ptr", 0, "Ptr", A_ScreenWidth)
DllCall("User32.dll\SendMessage", "Ptr", TTHW, "UInt", TRACKPOS, "Ptr", 0, "Ptr", PT.X | (PT.Y << 16))
DllCall("User32.dll\SendMessage", "Ptr", TTHW, "UInt", TRACKACT, "Ptr", 1, "Ptr", &TI)
}
; -------------------------------------------------------------------------------------------------------------------
; Update the text and the font and colors, if specified.
If (TTBC <> "") || (TTTC <> "") { ; colors
DllCall("UxTheme.dll\SetWindowTheme", "Ptr", TTHW, "Ptr", 0, "Str", "")
VarSetCapacity(RC, 16, 0)
NumPut(4, RC, 0, "Int"), NumPut(4, RC, 4, "Int"), NumPut(4, RC, 8, "Int"), NumPut(1, RC, 12, "Int")
DllCall("User32.dll\SendMessage", "Ptr", TTHW, "UInt", SETMARGN, "Ptr", 0, "Ptr", &RC)
If (TTBC <> "")
DllCall("User32.dll\SendMessage", "Ptr", TTHW, "UInt", BKGCOLOR, "Ptr", TTBC, "Ptr", 0)
If (TTTC <> "")
DllCall("User32.dll\SendMessage", "Ptr", TTHW, "UInt", TXTCOLOR, "Ptr", TTTC, "Ptr", 0)
}
If (TTIC <> "")
DllCall("User32.dll\SendMessage", "Ptr", TTHW, "UInt", SETTITLE, "Ptr", TTIC, "Str", " ")
If (TTHF) ; font
DllCall("User32.dll\SendMessage", "Ptr", TTHW, "UInt", 0x0030, "Ptr", TTHF, "Ptr", 1) ; WM_SETFONT
DllCall("User32.dll\SendMessage", "Ptr", TTHW, "UInt", UPDTIPTX, "Ptr", 0, "Ptr", &TI)
; -------------------------------------------------------------------------------------------------------------------
; Get the ToolTip window dimensions.
VarSetCapacity(RC, 16, 0)
DllCall("User32.dll\GetWindowRect", "Ptr", TTHW, "Ptr", &RC)
TTRC := {L: NumGet(RC, 0, "Int"), T: NumGet(RC, 4, "Int"), R: NumGet(RC, 8, "Int"), B: NumGet(RC, 12, "Int")}
TTW := TTRC.R - TTRC.L
TTH := TTRC.B - TTRC.T
; -------------------------------------------------------------------------------------------------------------------
; Check if the Tooltip will be partially outside the virtual desktop and adjust the position, if necessary.
If (PT.X + TTW >= DTW.R)
PT.X := DTW.R - TTW - 1
If (PT.Y + TTH >= DTW.B)
PT.Y := DTW.B - TTH - 1
; -------------------------------------------------------------------------------------------------------------------
; Check if the cursor is inside the ToolTip window and adjust the position, if necessary.
If (TTXP = "") || (TTYP = "") {
TTRC.L := PT.X, TTRC.T := PT.Y, TTRC.R := TTRC.L + TTW, TTRC.B := TTRC.T + TTH
If (Cursor.X >= TTRC.L) && (Cursor.X <= TTRC.R) && (Cursor.Y >= TTRC.T) && (Cursor.Y <= TTRC.B)
PT.X := Cursor.X - TTW - 3, PT.Y := Cursor.Y - TTH - 3
}
; -------------------------------------------------------------------------------------------------------------------
; Show the Tooltip using the final coordinates.
DllCall("User32.dll\SendMessage", "Ptr", TTHW, "UInt", TRACKPOS, "Ptr", 0, "Ptr", PT.X | (PT.Y << 16))
DllCall("User32.dll\SendMessage", "Ptr", TTHW, "UInt", TRACKACT, "Ptr", 1, "Ptr", &TI)
TT[TTIX].HW := TTHW
Return TTHW
}
Code: Select all
#NoEnv
#Persistent
SetBatchLines, -1
If !FileExist("wink.gif")
UrlDownloadToFile, http://ahkscript.org/boards/images/smilies/icon_e_wink.gif, Wink.gif
HIL := IL_Create(1, 1, 0) ; large icons are only supported on Vista+
IL_Add(HIL, "Wink.gif", 0xFFFFFF, 1)
HICON := IL_EX_GetHICON(HIL, 1, 0)
CoordMode, Mouse, Screen
Menu, Tray, Tip, Press Esc to exit!
MouseGetPos, X, Y
HFONT := GetHFONT("s12", "Tahoma")
; ToolTip number 1 will follow the mouse
ToolTipEx(X . " - " . Y, , , , HFONT, "Black", "White")
; ToolTip number 2 will stay
ToolTipEx("Press Esc to exit!", A_ScreenWidth // 2, A_ScreenHeight // 2, 2, HFONT, "White", "Navy", HICON, "S")
PX := "", PY := ""
Loop {
MouseGetPos, X, Y
If (PX <> X) || (PY <> Y)
ToolTipEx(X . " - " . Y)
PX := X, PY := Y
Sleep, 10
}
Return
Esc::
ExitApp
; ======================================================================================================================
GetHFONT(Options := "", Name := "") {
Gui, New
Gui, Font, % Options, % Name
Gui, Add, Text, +hwndHTX, Dummy
HFONT := DllCall("User32.dll\SendMessage", "Ptr", HTX, "UInt", 0x31, "Ptr", 0, "Ptr", 0, "UPtr") ; WM_GETFONT
Gui, Destroy
Return HFONT
}
; ======================================================================================================================
; IL_EX -> http://ahkscript.org/boards/viewtopic.php?f=6&t=1273
IL_EX_GetHICON(ILID, Index, Styles := 0x20) {
Return DllCall("ComCtl32.dll\ImageList_GetIcon", "Ptr", ILID, "Int", Index - 1, "UInt", Styles, "UPtr")
}
; ======================================================================================================================
#Include ToolTipEx.ahk
Code: Select all
...
ResultType Line::ToolTip(LPTSTR aText, LPTSTR aX, LPTSTR aY, LPTSTR aID)
{
int window_index = *aID ? ATOI(aID) - 1 : 0;
if (window_index < 0 || window_index >= MAX_TOOLTIPS)
return LineError(_T("Max window number is ") MAX_TOOLTIPS_STR _T("."), FAIL, aID);
HWND tip_hwnd = g_hWndToolTip[window_index];
// Destroy windows except the first (for performance) so that resources/mem are conserved.
// The first window will be hidden by the TTM_UPDATETIPTEXT message if aText is blank.
// UPDATE: For simplicity, destroy even the first in this way, because otherwise a script
// that turns off a non-existent first tooltip window then later turns it on will cause
// the window to appear in an incorrect position. Example:
// ToolTip
// ToolTip, text, 388, 24
// Sleep, 1000
// ToolTip, text, 388, 24
if (!*aText)
{
if (tip_hwnd && IsWindow(tip_hwnd))
DestroyWindow(tip_hwnd);
g_hWndToolTip[window_index] = NULL;
return OK;
}
// Use virtual desktop so that tooltip can move onto non-primary monitor in a multi-monitor system:
RECT dtw;
GetVirtualDesktopRect(dtw);
bool one_or_both_coords_unspecified = !*aX || !*aY;
POINT pt, pt_cursor;
if (one_or_both_coords_unspecified)
{
// Don't call GetCursorPos() unless absolutely needed because it seems to mess
// up double-click timing, at least on XP. UPDATE: Is isn't GetCursorPos() that's
// interfering with double clicks, so it seems it must be the displaying of the ToolTip
// window itself.
GetCursorPos(&pt_cursor);
pt.x = pt_cursor.x + 16; // Set default spot to be near the mouse cursor.
pt.y = pt_cursor.y + 16; // Use 16 to prevent the tooltip from overlapping large cursors.
// Update: Below is no longer needed due to a better fix further down that handles multi-line tooltips.
// 20 seems to be about the right amount to prevent it from "warping" to the top of the screen,
// at least on XP:
//if (pt.y > dtw.bottom - 20)
// pt.y = dtw.bottom - 20;
}
POINT origin = {0};
if (*aX || *aY) // Need the offsets.
CoordToScreen(origin, COORD_MODE_TOOLTIP);
// This will also convert from relative to screen coordinates if appropriate:
if (*aX)
pt.x = ATOI(aX) + origin.x;
if (*aY)
pt.y = ATOI(aY) + origin.y;
TOOLINFO ti = {0};
ti.cbSize = sizeof(ti) - sizeof(void *); // Fixed for v1.0.36.05: Tooltips fail to work on Win9x and probably NT4/2000 unless the size for the *lpReserved member in _WIN32_WINNT 0x0501 is omitted.
ti.uFlags = TTF_TRACK;
ti.lpszText = aText;
// Note that the ToolTip won't work if ti.hwnd is assigned the HWND from GetDesktopWindow().
// All of ti's other members are left at NULL/0, including the following:
//ti.hinst = NULL;
//ti.uId = 0;
//ti.rect.left = ti.rect.top = ti.rect.right = ti.rect.bottom = 0;
// My: This does more harm that good (it causes the cursor to warp from the right side to the left
// if it gets to close to the right side), so for now, I did a different fix (above) instead:
//ti.rect.bottom = dtw.bottom;
//ti.rect.right = dtw.right;
//ti.rect.top = dtw.top;
//ti.rect.left = dtw.left;
// No need to use SendMessageTimeout() since the ToolTip() is owned by our own thread, which
// (since we're here) we know is not hung or heavily occupied.
// v1.0.40.12: Added the IsWindow() check below to recreate the tooltip in cases where it was destroyed
// by external means such as Alt-F4 or WinClose.
if (!tip_hwnd || !IsWindow(tip_hwnd))
{
// This this window has no owner, it won't be automatically destroyed when its owner is.
// Thus, it will be explicitly by the program's exit function.
tip_hwnd = g_hWndToolTip[window_index] = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, NULL, TTS_NOPREFIX | TTS_ALWAYSTIP
, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, NULL, NULL);
SendMessage(tip_hwnd, TTM_ADDTOOL, 0, (LPARAM)(LPTOOLINFO)&ti);
// v1.0.21: GetSystemMetrics(SM_CXSCREEN) is used for the maximum width because even on a
// multi-monitor system, most users would not want a tip window to stretch across multiple monitors:
SendMessage(tip_hwnd, TTM_SETMAXTIPWIDTH, 0, (LPARAM)GetSystemMetrics(SM_CXSCREEN));
// Must do these next two when the window is first created, otherwise GetWindowRect() below will retrieve
// a tooltip window size that is quite a bit taller than it winds up being:
SendMessage(tip_hwnd, TTM_TRACKPOSITION, 0, (LPARAM)MAKELONG(pt.x, pt.y));
SendMessage(tip_hwnd, TTM_TRACKACTIVATE, TRUE, (LPARAM)&ti);
}
// Bugfix for v1.0.21: The below is now called unconditionally, even if the above newly created the window.
// If this is not done, the tip window will fail to appear the first time it is invoked, at least when
// all of the following are true:
// 1) Windows XP;
// 2) Common controls v6 (via manifest);
// 3) "Control Panel >> Display >> Effects >> Use transition >> Fade effect" setting is in effect.
SendMessage(tip_hwnd, TTM_UPDATETIPTEXT, 0, (LPARAM)&ti);
RECT ttw = {0};
GetWindowRect(tip_hwnd, &ttw); // Must be called this late to ensure the tooltip has been created by above.
int tt_width = ttw.right - ttw.left;
int tt_height = ttw.bottom - ttw.top;
// v1.0.21: Revised for multi-monitor support. I read somewhere that dtw.left can be negative (perhaps
// if the secondary monitor is to the left of the primary). So it seems best to assume it is possible:
if (pt.x + tt_width >= dtw.right)
pt.x = dtw.right - tt_width - 1;
if (pt.y + tt_height >= dtw.bottom)
pt.y = dtw.bottom - tt_height - 1;
// It seems best not to have each of the below paired with the above. This is because it allows
// the flexibility to explicitly move the tooltip above or to the left of the screen. Such a feat
// should only be possible if done via explicitly passed-in negative coordinates for aX and/or aY.
// In other words, it should be impossible for a tooltip window to follow the mouse cursor somewhere
// off the virtual screen because:
// 1) The mouse cursor is normally trapped within the bounds of the virtual screen.
// 2) The tooltip window defaults to appearing South-East of the cursor. It can only appear
// in some other quadrant if jammed against the right or bottom edges of the screen, in which
// case it can't be partially above or to the left of the virtual screen unless it's really
// huge, which seems very unlikely given that it's limited to the maximum width of the
// primary display as set by TTM_SETMAXTIPWIDTH above.
//else if (pt.x < dtw.left) // Should be impossible for this to happen due to mouse being off the screen.
// pt.x = dtw.left; // But could happen if user explicitly passed in a coord that was too negative.
//...
//else if (pt.y < dtw.top)
// pt.y = dtw.top;
if (one_or_both_coords_unspecified)
{
// Since Tooltip is being shown at the cursor's coordinates, try to ensure that the above
// adjustment doesn't result in the cursor being inside the tooltip's window boundaries,
// since that tends to cause problems such as blocking the tray area (which can make a
// tooltip script impossible to terminate). Normally, that can only happen in this case
// (one_or_both_coords_unspecified == true) when the cursor is near the bottom-right
// corner of the screen (unless the mouse is moving more quickly than the script's
// ToolTip update-frequency can cope with, but that seems inconsequential since it
// will adjust when the cursor slows down):
ttw.left = pt.x;
ttw.top = pt.y;
ttw.right = ttw.left + tt_width;
ttw.bottom = ttw.top + tt_height;
if (pt_cursor.x >= ttw.left && pt_cursor.x <= ttw.right && pt_cursor.y >= ttw.top && pt_cursor.y <= ttw.bottom)
{
// Push the tool tip to the upper-left side, since normally the only way the cursor can
// be inside its boundaries (when one_or_both_coords_unspecified == true) is when the
// cursor is near the bottom right corner of the screen.
pt.x = pt_cursor.x - tt_width - 3; // Use a small offset since it can't overlap the cursor
pt.y = pt_cursor.y - tt_height - 3; // when pushed to the the upper-left side of it.
}
}
SendMessage(tip_hwnd, TTM_TRACKPOSITION, 0, (LPARAM)MAKELONG(pt.x, pt.y));
// And do a TTM_TRACKACTIVATE even if the tooltip window already existed upon entry to this function,
// so that in case it was hidden or dismissed while its HWND still exists, it will be shown again:
SendMessage(tip_hwnd, TTM_TRACKACTIVATE, TRUE, (LPARAM)&ti);
return OK;
}
...