Link to home
Start Free TrialLog in
Avatar of AuthorwareXtras
AuthorwareXtras

asked on

Create Tool Tips from DLL

I am trying to create a DLL that will allow another development system (not Delphi) to set up tool tips for areas of the screen.  The other development system does not have tools tips capabilities but is capable of calling functions in Delphi DLLs, hence the approach I am making.

What I have tried so far is to create a borderless form from the DLL specifying it as fsStayOnTop and creating it at a specific position and width and height.  I have turned on tool tips for the form.

Calling this function in the DLL creates the form and the tool tips work.  If this approach is the correct one though, I need a way to make the form transparent so that it is just the 'region' it occupies on screen that forces the tool tip to appear.  I am not sure though, if the tool tips will still appear if the form is transparent??

Alternatively I need some other way of specifying a rectangular (or even any other shape if that is possible) area on screen where a tool tip can be made to appear all created from a DLL.

Thanks
ASKER CERTIFIED SOLUTION
Avatar of atul_parmar
atul_parmar
Flag of India image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
hello AuthorwareXtras, , I am not sure I understand what you are trying to do? ? ?
You talk about creating a  "  borderless form from the DLL " . . . and then say some conditions about Form transparency, and tool tips appearing or not ? ? I am not sure about the "Form" being on screen and not interfering with any windows (programs) that may be below it (mouse click and such). In the last part of your question you say something about having "Areas" like a rectangle or polygon , , defined on the Screen (not a window, program) that can do the " tool tip " or " hint " kind of information window pop-up, , is that what you mean?, that you do not really have a form showing? It sems to me that the user would have a difficult time telling id the " hint " info window was from your DLL or from some other program?
Maybe you can say some more about what you are tring to do, and not so much about how you want to do it?
Avatar of AuthorwareXtras
AuthorwareXtras

ASKER

In essence I want to set up tool tipd from a dll over areas of another application that calls the DLL.  The other application (not Delphi) does not have the ability to natively have tool tips.

I have got a lot of the way there with the link provided by atul_parmar and I can set arbitrary areas of the application to have tool tips, but if I set a region over a button in the other application, then it seems the mouse over the button is masking the tool tip area and the tool tip is not showing.  This is counter productive unfortunately as it is over buttons and the like that I want to set the tool tips.
Since this is for a single program window that is calling this DLL, , ,you can do what the delphi  THints  does, and have an in process mouse hook, which can solve the problem of program areas that have a child control in them, since it gets all mouse messages for all windows in that program
Sounds good, do you have any pointers for that?
I have been busy today, but I will look into it soon, maybe tomorrow, ,
I guess that you know that a system "Tool Tip" can use the child controls (buttons) as tool tip display, you just have to set the child control handle and tip string into the tool tip window
OK, , First, I take it from your comments that this will be implemented in a program that is not Delphi and has some limited programming methods. So I am at a Loss as to what to do for coding the DLL, since I have NO IDEA what can be called or what variable types are available, and I do not want to ask about that, I do not really care much. . . . . .
I think the system tool tip window will be a much more easy (shorter code) way, instead of trying the in-process mouse hook like the THints uses. Here is some code for a Library that will find the top window in that thread that has buttons and add a system "Tool Tip" for all of the buttons on that window.
I have made it so the FindThreadWindows procedure is called when this loads, , and this works for Dynamic loading of the DLL, but if it is Static loading, it do not think it will work, since no windows are created at that time. However you can call the FindThreadWindows procedure after window creation.
Also all of the MessageBox functions are for debugging and can be cut out.

  Code for DllTips library below -->




library DllTips;


uses
  Windows, Messages, CommCtrl, SysUtils;

{$R *.RES}

type
  TAryHnds = Array of THandle;

const
  TTS_BALLOON    = $40;
  TTM_SETTITLE = (WM_USER + 32);
  iUseDef: Integer = Integer(CW_USEDEFAULT);

var
  MInstance: THandle = 0;
  hMainWnd: THandle = 0;
  hToolTip: THandle = 0;
  aryButtons: TAryHnds;



function GetWindows(hWnd: Integer; var aryWnds: TAryHnds): BOOL; StdCall;
begin
Result := True;
  {This will take the TAryHnds passed to it
   and add all window handles returned}
SetLength(aryWnds, Succ(Length(aryWnds)));
// make Array larger and add window handle
aryWnds[High(aryWnds)] := hWnd;
end;


procedure FindThreadWindows;
var
aryWnds, aryChild: TAryHnds;
w, c: Integer;
cName: Array[Byte] of Char;
begin
// first get all of the top level windows in thread
EnumThreadWindows(GetCurrentThreadId, @GetWindows, Integer(@aryWnds));
if Length(aryWnds) > 0 then
  begin
  MInstance := GetWindowLong(aryWnds[0], GWL_HINSTANCE);
  // This MInstance is used in the createWindowEx function
  if MInstance <> 0 then
    hMainWnd := aryWnds[0] else Exit;
  end else
  Exit;

   // the messageBox function below is for DeBugging and can be left out
MessageBox(aryWnds[0], PChar('aryWnds '+IntToStr(Length(aryWnds))), 'Length Array', MB_ICONINFORMATION);

  // go through all top level windows until some buttons are found
for w := 0 to High(aryWnds) do
  begin
  EnumChildWindows(aryWnds[w], @GetWindows, Integer(@aryChild));
  if Length(aryChild) < 1 then
    MessageBox(aryWnds[0], 'No Child Windows Found', 'No Children', MB_ICONINFORMATION)
    else
    begin
    for c := 0 to High(aryChild) do
      if GetClassName(aryChild[c], @cName, SizeOf(cName)) > 0 then
        begin
        StrUpper(@cName);
    // search for "BUTTON" controls, if to use a Delphi VCL app then use TBUTTON instead of BUTTON
        if StrComp(@cName, 'BUTTON') = 0 then
          begin
          SetLength(aryButtons, Succ(Length(aryButtons)));
          aryButtons[High(aryButtons)] := aryChild[c];
          end;
        end;
    if Length(aryButtons) > 0 then
      begin
      hMainWnd := aryWnds[w];
      Break;
      end;
    end;
  end;
// the messageBox function below is for DeBugging and can be left out
MessageBox(aryWnds[0], PChar('aryButton '+IntToStr(Length(aryButtons))), 'Length Array', MB_ICONINFORMATION);

// if you like you can call MakeButtonToolTip here for a single function call to ToolTip
end;


function GetMainWnd(out hIns, hBut: Cardinal): THandle;
begin
// this is a DeBugging function to verify that there are some Buttons to Tool Tip
Result := hMainWnd;
hIns := MInstance;
if length(aryButtons) > 0 then
  hBut := aryButtons[0]
  else
  hBut := 0;;
end;


function MakeButtonToolTip: Cardinal;
var
i: Integer;
InfoTool: TToolInfo;
cText: Array[Byte] of Char;
begin
// this function creates the Tool Tip and adds the buttons
Result := 0;
if (hMainWnd = 0) or (Length(aryButtons) < 1) or (MInstance = 0) then Exit;
hToolTip := CreateWindowEx(0, TOOLTIPS_CLASS, nil,
                     {TTS_BALLOON} 0, iUseDef, iUseDef,
                     iUseDef, iUseDef, hMainWnd, 0, MInstance, nil);
if hToolTip = 0 then Exit;
  // code below adds the buttons to toolTip
for i := 0 to High(aryButtons) do
if GetClientRect(aryButtons[i], InfoTool.Rect) then
  begin
  GetWindowText(aryButtons[i], @cText, SizeOf(cText));
  with InfoTool do
    begin
    cbSize := SizeOf(toolInfo);
    uId := 0;
    hwnd := aryButtons[i];
    uFlags := TTF_SUBCLASS;
    hInst  := 0;
    if lStrLen(@cText) > 0 then
      lpszText := @cText
      else
      lpszText := 'No Text';

    if SendMessage(hToolTip, TTM_ADDTOOL, 0, Integer(@InfoTool)) <> 0 then
      Result := hToolTip;
    end;
  end;
 
if Result = 0 then
  begin
  DestroyWindow(hToolTip);
  hToolTip := 0;
  end;
end;


procedure LibraryProc(Reason: Cardinal);
begin
if (Reason = Dll_Process_Detach) then
  DestroyWindow(hToolTip);
end;


exports
  FindThreadWindows,
  GetMainWnd,
  MakeButtonToolTip;
 

begin
DLLProc := @LibraryProc;
FindThreadWindows;
 // I call this when this DLL loads to find the Buttons
 // but you can leave this out and call it latter
end.
 


 = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

below is the code for a Delphi VCL program that I tested it in.
  If you use Delphi VCL the be sure to change Library code above -
    if StrComp(@cName, 'BUTTON') = 0 then

    to

    if StrComp(@cName, 'TBUTTON') = 0 then


//  VCL button click procedure below
procedure TForm1.but_LoadTipDllClick(Sender: TObject);
var
hLibTip, hMain, hInst, hButton, hToolTip: THandle;
GetMainWnd1: function(out hIns, hBut: Cardinal): THandle;
MakeButtonToolTip: function: Cardinal;
// FindThreadWindows: procedure;
begin
hLibTip := LoadLibrary('DllTips.dll');
if hLibTip = 0 then
  begin
  ShowMessage('Did not load the DllTips.DLL');
  Exit;
  end;
//ShowMessage('Loaded the DllTips.DLL');

@GetMainWnd1 := GetProcAddress(hLibTip, 'GetMainWnd');
if @GetMainWnd1 = nil then
  begin
  FreeLibrary(hLibTip);
  ShowMessage('Did not load the GetMainWnd function');
  Exit;
  end;

hMain := GetMainWnd1(hInst, hButton); // debugging function to see if DLL found anything
ShowMessage('hMain '+IntToStr(hMain)+'  hInst '+IntToStr(hInst)+'  hButton '+IntToStr(hButton));
if hMain <> Handle then
  begin
  FreeLibrary(hLibTip);
  ShowMessage('hMain is Not Equal To This Form Handle');
  Exit;
  end;

if hButton <> 0 then
  begin
  @MakeButtonToolTip := GetProcAddress(hLibTip, 'MakeButtonToolTip');
  if @MakeButtonToolTip = nil then
    begin
    FreeLibrary(hLibTip);
    ShowMessage('Did not load the MakeButtonToolTip function');
    Exit;
    end;

  hToolTip := MakeButtonToolTip;
  if hToolTip = 0 Then
    begin
    FreeLibrary(hLibTip);
    ShowMessage('hToolTip is Zero ');
    Exit;
    end;
  end;
end;


  == = = = = = = = = = = = = = = = = = = = = =
this works for me, ask questions if you need more info
Thaks for your efforts Slick812, but I need to be able to specify more than just buttons and so I think that specyfying a position on screen is the way to go.  I am almost there from the link that atul_parmar gave me.  I can get tools tips to appear in the correct position on screen, but the behaviour over 'buttons' is not 100% predictable.  Some times the mouse needs to move very close to the centre of the button before the tip will show, other times not (on the same button).  

When I am referring to buttons here, they may not be buttons in the true Windows sense as they are not created directly with a true progamming language, but in Macromedia Authorware.
OK, I have little Idea what you may be dealing with here, as far as the  Windows API of your Macromedia Authorware program. Somehow I thought you were dealing with system controls (like buttons, edits), how ever, I used buttons just for a "How To example", but you can place it on any windowed control, if there are ant windowed controls, If you have windowed controls you can just get the window rectangle and see if it is were you want a tip, , I guess you did not understand my code or even try it . . . .
But if they are visual controls (like TSpeedButton), you should NOT have any problems like you are describing with your "buttons" and the "area" Tool tip not working, , maybe I can do an inprocess mouse hook in DLL, , if I have time I might try
I have not tried it, but I will give it a go.  I am not sure how Authorware instantiates buttons and whether they are buttons in the true Windows sense.  I will need to specify tool tips in different areas though and it will not necessarily be just buttons.  I think therefore that setting a tool tip over an area of the application window is therefore the best approach.

Thanks again for your efforts.
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Although I didn't explain myself very well, when I said " setting a tool tip over an area of the application window is therefore the best approach " I do actually mean setting it in a rectangle as you are here.

I am unable to load a library in the context you mean from Authorware.  Authorware needs at least one exported function that it can call to use the DLL.  I will see if I can modify your answer to make it work.

Thanks again for your comprehensive answer.
Firther to my last I built the DLL as provided and loaded it in Delphi and it works as advertised, so I know your code is good.

I then tried commenting out this section

procedure LibraryProc(Reason: Cardinal);
begin
if (Reason = Dll_Process_Detach) then
  begin
  KillTimer(hMain, ID_Hover);
  KillTimer(hMain, ID_Wait);
  DestroyWindow(hHintWin);
  UnRegisterClass(pClassName,MInstance);
  UnHookWindowsHookEx(hHook);
  end;
end;

begin
DLLProc := @LibraryProc;
GetWindowAndHook;
end.

and then creating a single function that is exported that simply calls GetWindowAndHook;

The function is visible to Authorware and I can call it, I get no error messages, but I also get no tool tips.  I am not sure what is going wrong.
I can not tell much of anything about why it doees not seem to work? ?
I would first try to keep the code like it is, (more or less, , maybe leave out the UnRegisterClass(pClassName,MInstance); , UnHookWindowsHookEx(hHook);) if it unloads the DLL right after the call to the DLL function?

and add a do-nothing function to export, just to have a function to call, , however, if you load this DLL before any windows are created (during program  startup) then it will find no windows and not do anything. . .
If you still have problemms, then you should add the "MessageBox( )"  functions at debug positions to see what your DLL is doing in the GetWindowAndHook procedure , , and the values of important program information, variables. You can make judgements about what is wrong by the variable data values.
???
I am not sure about the  Authorware loading and unloading of a called DLL function, , BUT, if the DLL function is called, , , and then, as soon as the execution of the function is finished, the DLL is Unloaded, my DLL will Not work, it is required to be in accesable memory (loaded as DLL) for as long as the rectangle detection is needed, if the DLL is unloaded, there is no memory for the system to access the HookFunc( ) function or the timer function
You may be right,  Thanks for your patience and help.
I thought about the loading and unloading of DLLs, as I remember, the Load of DLL is associated with the thread that calls LoadLibrary. .
so I wondered if I loaded a DLL and called a function in that DLL that would load the DllHint.dll, if the  DllHint.dll would be unloaded when that DLL was unloaded. . . I tried it and the  DllHint.dll was Not unloaded, and continued to work, even when the StartHint  dll was unloaded.

here is the code for the library that loads the DllHint.dll, the code in the  DllHint.dll is unchanged from what I posted here. . .



library StartHint;
uses
  Windows;
{$R *.RES}
var
hLibH: THandle;

function LoadHints: Boolean;
begin
hLibH := LoadLibrary('DllHint.dll');
Result := hLibH <> 0;
end;

exports
  LoadHints;
begin
end.

 = = = = = = = =  = = = = = = = = = = =
I hope this may help, but you seem to have closed this question, ,  so I guess you have already found a solution.
I have actually managed to get the DLL to load now by exposing a function that just calls GetWindowAndHook.  As before I have removed this section:

procedure LibraryProc(Reason: Cardinal);
begin
if (Reason = Dll_Process_Detach) then
  begin
  KillTimer(hMain, ID_Hover);
  KillTimer(hMain, ID_Wait);
  DestroyWindow(hHintWin);
  UnRegisterClass(pClassName,MInstance);
  UnHookWindowsHookEx(hHook);
  end;
end;

begin
DLLProc := @LibraryProc;
GetWindowAndHook;
end.


I am now trying to modify your example to be able to supply the coordinates and text of the hint at run time.  Not managed to achieve this at the minute as I try and unpick your example.  I will specify one tool tip at a time and call the dll as many times a needed to set up the required number of tooltips.  I plan to call the exposed function (the one that calls GetWindowAndHook currently) and supply parameters for the tooltip.  Do you see an issue with that approach?
I tried to read through your last comment and get an idea of what you may mean to do, I beleive I have a "General" idea of what you may be doing, and as far as I can tell (without seeing actual code), it sounds as if it may work, can not see any "issues", but I have little idea about what methods you actually may use?
I do not beleive that initializing the many rectangles and hint text, one at a time in a single function is efficient, , ,
I believe that having a single dll function that passes an  array of  rectanlges and PChar text (I would never pass a delphi string to a DLL), would be a better way
here is a DLL function that might be used to initialize an array to use as the hints -

// the InitHints will use the input array to set the rectangles
function InitHints(pAryHints: PAryInput; Count: Integer): Integer;
var
i: Integer;
sDC: THandle;
Size1: TSize;
begin
Result := -1;
if IsBadCodePtr(pAryHints) then Exit;
Result := -2;
if (Count < 0) or (Count > MaxByte) then Exit;
SetLength(Hints1, Count);

Result := 1;
sDC := GetDC(0);
SelectObject(sDC, GetStockObject(ANSI_VAR_FONT));
for i := 0 to Pred(Count) do
  with Hints1[i], pAryHints^[i] do
  begin
  HintRect := RectH;
  if StrLen(pHint) > 0 then
    Hint := pHint
    else
    Hint := '  ';
  GetTextExtentPoint32(sDC, PChar(Hint), Length(Hint), Size1);
  Width := Size1.cx + 12;
  Height := Size1.cy + 4;
  Inc(Result);
  end;
ReleaseDC(0, sDC);
end;

 = = = = = = = = = = = = = = = =
but I have no idea what code programming options you may or may not have in your DLL calling program, , Authorware
Thanks, I will have a look.  

I can't pass pointers  to the DLL....Actually that's not strictly true, it is possible, but 'fiddly'.  I am attempting to write the DLL so your 'avegarge' Authorware user has the complexity of this procedure hidden from them. Therefore, the easiest option is an exported function something like this:

setUpHint(intLeft, intTop, intRight, IntBottom:Integer;strHintText:String):Boolean  // or maybe return something relevant?

This reflects the method I had in mind.  So for example, if I needed 4 hints setting up, I would call the function in the DLL 4 times with the appropriate parameters.  I was wondering if, in your experience, you thought this may work, or there may be issues??  I have this working using the method that atul_parmar suggested with the link right at the top of this page, but as I mentioned before there seems to be a problem with some 'buttons' in Authorware (again, I think this is an Authorware issue based on how 'buttons' are set up, which may not be 'true' windows buttons).  Your method works perfectly, I just need to set up the function so an Authorware user can set the hints at run time.

Authorware also has a 'List' type which is effectively an array and may, for example, look like this [100,100,300,300].  I can also set up a 'list of lists' (multidimensional array)  [[100,100,300,300,"Hint 1'],[400,400,600,600,"Hint 2"]] which I think would reflect the method you are using in your function, but I have yet to figure out how to pass a list to a DLL and extract the data from it, so at the minute, passing 4 integers in the easiest way for me.  

I am working on passing a list though, so as soon as I get that working, your function will be invaluable.

Thanks again...
???
I will just add a thought, since I have no experience with author ware, , you said that you can not pass "Pointers" in a funtion to DLL, , if that is the case, I do not see how you can ever pass text info to the DLL ? ?  You have this in your function -->  strHintText:String
WARNING, , the delphi string is a DELPHI ONLY variable, and I would not ever use a  "String" passed to a DLL, The Delphi "Memory manager" is the specific tracker for all "String" variables, so it is NOT a good idea to try and use "String" variables to places that the same memory manager in not in existance (like a DLL). Use a PChar variable TYPE instead . . As far as I have seen text is usually passed as a pointer to a DLL, I would guess that is some author ware thing to use for text, probally a  windows system "PChar" null terminated type of thing.
Yes I pass it as a PChar.  I have no issue with strings and I have a tried and tested method.  All of this is transparent to the Authorware user as they just pass a sring, but it is actually a PChar as you suggest that is passed.

I have not yet successfully passed a List or a 'List of Lists' and be able to work with it in Delphi yet though.  I am working on that one !