Skip to content
Home > Programming > How To: Creating Software Integrations Part2

How To: Creating Software Integrations Part2

Viewing 4 posts - 1 through 4 (of 4 total)
  • Author
    Posts
  • #190362
    autopilot
    Member

    Sending text to NotePad! It sounds easy, doesn’t it? Well this is where things get tricky. With the introduction of the 32-bit application, Microsoft separated the memory space that each program uses to produce a more stable product. While a more stable product was the result, it also made accessing the memory space of another application harder. Using the Windows API, we have three options to send text to another application. The first method is to set the focus on the other application (NotePad in this case) and use SendKeys. This option is not a very good one because SendKeys is not supported by Vista and so any program that uses SendKeys will not work with the Vista operating system. So now we are left with two options. We can send keystrokes just as if we were typing the text into the Notepad. This requires a lot of code and may be the best options for some integrations, but it is way too much work for what we are doing. Now we are left with one last option (which is really two). It is the API SendMessage/PostMessage.

    As with any API, we will have to make a declaration statement. Program declarations will go under the “Public Class” line but before any “Subs” or “Functions”. So lets add both the SendMessage and the PostMessage to our form.

    Declare Function SendMessageAsString Lib "USER32" Alias "SendMessageA" (ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer, ByVal lParam As String) As Integer
    Declare Function PostMessage Lib "USER32" Alias "PostMessageA" (ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Integer

    [attachment=8:1cl6px5q]int5.PNG[/attachment:1cl6px5q]As we look at the SendMessage API, we see that it takes 4 inputs and returns an integer. The first input we need is the windows handle for the control to send the message to. Now is when the other tools start to come in handy. Some how we need to find the handle for the control inside of Notepad that the text goes in. Let’s start Notepad as well as both JK’s API Spy and AutoIt3 Window Spy (part of AutoHotKeys install).
    Now we can see that both these spy programs give us similar information. We do notice that in JK’s the class name is “Edit” while in AutoIt3 the class name “Edit1”. What is the number for on the end? As you play with the window spy programs, you will see that a lot of times programs have multiple controls that are the same type. For instance, multiple buttons. The number at the end of the class name is the order in which it occurs when the controls are enumerated. In JK’s Spy, we can see that it is showing us the handle. So why not just hard code the handle 331008? Well every time a control is created, it is assigned a new handle. So the handle is never the same. Therefore we must find a way to get the handle each time.

    JK’s API Spy has a code generator tab that can be used to create the code needed to find the handle, but that is not always going to be the best way.

    Dim notepad As Long, editx As Long
    notepad = FindWindow("notepad", vbNullString)
    editx = FindWindowEx(notepad, 0&, "edit", vbNullString)

    The above is code generated by JK’s API Spy. I t key to note here that the JK’s API Spy code is actually for VB6 and not VB2005. Something to know and remember is that VB6 Longs were changed in VB2005 to Integers. Lets add another button to the form and use it to try and find the handle for the Edit class in the Notepad program.[attachment=5:1cl6px5q]int8.PNG[/attachment:1cl6px5q]Double Click on the button2 to add an event sub on the code page. Then we will paste the code from JK’s API Spy into the button click event and add a msgbox line to show us the returned handle. We must also add the declarations for FindWindow and FindWindowEx.

    Added Declarations:` Declare Auto Function FindWindow Lib “USER32” Alias “FindWindow” (ByVal lpClassName As String, ByVal lpWindowName As String) As Integer
    Declare Function FindWindowEx Lib “USER32” Alias “FindWindowExA” (ByVal hWnd1 As Integer, ByVal hWnd2 As Integer, ByVal lpsz1 As String, ByVal lpsz2 As String) As IntegerAdded button code: Dim notepad As Integer, editx As Integer
    notepad = FindWindow(“notepad”, vbNullString)
    editx = FindWindowEx(notepad, 0&, “edit”, vbNullString)
    MsgBox(editx.ToString)[attachment=4:1cl6px5q]<!-- ia4 -->int9.PNG<!-- ia4 -->[/attachment:1cl6px5q]When we run the program in debug mode (F5), we click the button1 and start NotePad. Then click button2 to see the handle that is returned by our code. We can use JK’s API Spy to verify the returned handle.[attachment=3:1cl6px5q]<!-- ia3 -->int10.PNG<!-- ia3 -->[/attachment:1cl6px5q]Now, I had said that the method provided by JK’s API Spy is not always the best way to get the handle. The reason for this is that while it works, it is a direct path and if the form is changed in an update, then you must update your program as well. So how else can we get the handle? We can use the EnumChildWindows API. While this is a harder concept to get, it is a more reliable way to fine the handles of controls on a form. The enumchildwindows looks like: Declare Function EnumChildWindows Lib “USER32” (ByVal hWndParent As Integer, ByVal lpEnumFunc As EnumChildWindowsCallback, ByVal lParam As Integer) As Integer`As we look at this declaration, we see that lpEnumFunc is a EnumChildWindowsCallback. What does that mean? Well, that is called a delegate and is something that we are going to have to design and define. How do we design and define the delegate? We must create a function that will find our controls on a form. So lets begin by looking at what information we have about the control and how we can use it. When we look at JK’s or AutoIt3, we see that we have a class name, but the caption or title is blank. We also know the index number from AutoIt3. There fore we can look for a class and index it.
    So we will create a function:` Private Function EnumChildWindowByClassName(ByVal ChildhWnd As Integer, ByVal lParam As Integer) As Integer

    End FunctionAnd a Delegate to corispond with it (goes up with program declarations): Delegate Function EnumChildWindowsCallback(ByVal hWnd As Integer, ByVal lParam As Integer) As IntegerNotice the Function and the Delegate are defined the same. So lets work on the function to enum the childs and return our control. We have two things that we are looking for. The class name and the index. So to insure that the index is reset before we enum the child controls, we must use a global variable for it. We also will use a global variable to set the class name we are looking for. A Global variable for passing back the found handle is also needed: Dim SFClassName As String
    Dim SFClassIndex As Integer
    Dim SFHnd As IntegerBut in the function, we will use a stringbuilder to get the class name of each child as we enum them. To find the class name, we will have to add another API call: Declare Function GetClassName Lib “USER32” Alias “GetClassNameA” (ByVal hwnd As Integer, ByVal lpClassName As StringBuilder, ByVal cch As Integer) As IntegerWe can see lpClassName set as StringBuilder. The StringBuilder is not part of the base namespace and so for us to use it, we must at the top of the code (above the “Public Class Form1”), we must use an imports statement to add the needed namespace:Imports System.TextIn our Function EnumChildWindowByClassName, we need to define our variable to get the class name for comparison. This variable will be a new stringbuilder and as such needs to be passed a max size. Therefore, we will define our max size first and then the stringbuilder: Dim StringBufferLength As Integer = 255
    Dim wClass As New StringBuilder(StringBufferLength)Now we can get the class name of the enum handle passed to the callback:GetClassName(ChildhWnd, wClass, StringBufferLength)Once we have the current class name, we can compare it with SFClassName: If wClass.ToString.ToLower = SFClassName.ToLower Then
    End IfIf the class names match, we must then check the index value. To do this, we have a global integer to count the class hits. When we first start the enum, we set this value to 0. Dim SFClassHit As IntegerSince the Index value starts at 1, the first thing we want to do when we get a class hit, is to increment the SFClassHit by 1: SFClassHit += 1Now that the class hit count is set, we can compare to see if it is the correct control and if it is, store the handle and stop the enum: If SFClassHit = SFClassIndex Then
    SFHnd = ChildhWnd
    EnumChildWindowByClassName = 0
    Exit Function
    End IfIf it is not the correct class name or class hit, keep enumerating: EnumChildWindowByClassName = 1That will take care of the EnumChildWindowByClassName function:[attachment=2:1cl6px5q]<!-- ia2 -->int11.PNG<!-- ia2 -->[/attachment:1cl6px5q]Now we must create a function to execute the enum. So we will, by using the control class name in conjunction with the main window class, main window caption, and control index, find the handle for the sub control (in our demo, Edit). Public Function GetSubFormByClassNameWithMWClassWithMWCaption(ByVal MainWindowClass As String, ByVal MainWindowCaption As String, ByVal SubFormClass As String, ByVal SubFormClassIndex As Integer) As Integer
    End FunctionWe will start out this function by setting our defualt values: SFHnd = 0
    SFClassHit = 0
    SFClassIndex = SubFormClassIndex
    SFClassName = SubFormClassWe are now ready to enum the child windows (controls) but that API requires the parent handle that we do not have. So we must use the FindWindow API function that will get the handle of a main window if given the class and caption. We are ready to call the EnumChildWindows in our function and then set the function equal to the returned handle. EnumChildWindows(FindWindow(MainWindowClass, MainWindowCaption), AddressOf EnumChildWindowByClassName, 0)
    GetSubFormByClassNameWithMWClassWithMWCaption = SFHndSo now our code looks like the following:[attachment=1:1cl6px5q]<!-- ia1 -->int12.PNG<!-- ia1 -->[/attachment:1cl6px5q]Now lets add a third button to the form to find the handle using the enum method:[attachment=0:1cl6px5q]<!-- ia0 -->int13.PNG<!-- ia0 -->[/attachment:1cl6px5q]In the code for the button3 click event, we will use the GetSubFormByClassNameWithMWClassWithMWCaption function to return the edit control handle and then display it in a msgbox just like before: Dim EditHndl As Integer = GetSubFormByClassNameWithMWClassWithMWCaption(“Notepad”, “Untitled – Notepad”, “Edit”, 1)
    MsgBox(EditHndl.ToString)`Now you really should play with the enum method to learn it well. For by using it, we do not need to update our project with each and every update put out by another company.

    #190365
    Chike
    Member

    @autopilot wrote:

    So to insure that the index is reset before we enum the child controls, we must use a global variable for it.

    Global variables are only to be used as last resort, or where there is no elegant way to avoid them. That’s exactly what the LPARAM in EnumChildWindows and the callback are for.
    Note that LPARAM type guaranteed to be large enough for passing a pointer (and thus declaring it as an integer may not be appropriate for 64 bits OS for example.)

    @autopilot wrote:

    We also will use a global variable to set the class name we are looking for. A Global variable for passing back the found handle is also needed

    Now that you have more than one parameter that’s need to be cahnged you can declare a structure that holds all of them and pass it’s address to the callback.

    #190364
    autopilot
    Member

    @Chike wrote:

    Global variables are only to be used as last resort, or where there is no elegant way to avoid them. That’s exactly what the LPARAM in EnumChildWindows and the callback are for.
    Note that LPARAM type guaranteed to be large enough for passing a pointer (and thus declaring it as an integer may not be appropriate for 64 bits OS for example.)
    Now that you have more than one parameter that’s need to be cahnged you can declare a structure that holds all of them and pass it’s address to the callback.

    Good to know… I am a self taught coder and so while I can get things done, they may not always be the best way to do it. So now lets turn it into a module and implement some better practices. Please show us in code examples how to implement your suggestions.

    Thanks

    #190363
    Chike
    Member

    @autopilot wrote:

    Please show us in code examples how to implement your suggestions.

    The following code based on your code and logic and written in C.
    The call to GetSubFormByClassNameWithMWClassWithMWCaption in C look just the same as it is in your code. And with no global variables it is safe to use in multi threaded environment.

    #include 
    struct EnumByClassNameParam {
    const char *class_name;
    int class_index;
    int class_hit;
    HWND hwnd;
    };
    typedef struct EnumByClassNameParam EnumByClassNameParam; // not needed in C++
    
    static BOOL __stdcall EnumChildWindowByClassName(HWND ChildhWnd, LPARAM lParam); // forward decalaration
    
    HWND GetSubFormByClassNameWithMWClassWithMWCaption(
    const char *MainWindowClass,
    const char *MainWindowCaption,
    const char *SubFormClass,
    int SubFormClassIndex)
    {
    EnumByClassNameParam param;
    
    param.class_name = SubFormClass;
    param.class_index = SubFormClassIndex;
    param.class_hit = 0;
    param.hwnd = NULL;
    EnumChildWindows(FindWindow(MainWindowClass, MainWindowCaption),
    EnumChildWindowByClassName, (LPARAM) &param);
    return param.hwnd;
    }
    
    static BOOL __stdcall EnumChildWindowByClassName(HWND ChildhWnd, LPARAM lParam)
    {
    char wClass[255];
    EnumByClassNameParam *pparam = (EnumByClassNameParam*) lParam;
    
    GetClassName(ChildhWnd, wClass, sizeof(wClass));
    if (lstrcmpi(wClass, pparam->class_name) == 0) {
    //pparam->class_hit += 1; will do the same with C increment style next line
    ++pparam->class_hit;
    if (pparam->class_hit == pparam->class_index) {
    pparam->hwnd = ChildhWnd;
    return FALSE;
    }
    }
    return TRUE; // Continue enumeration
    }

    for non C programmers:
    *: a variable that is a pointer to that type (or array), strings in C are char array of characters with a NUL (”) character termination.
    const: that must not be changed (or read only.)
    &
    : the address (pointer) to the variable.
    () : cast from one type to another.
    static: that is inaccessible from other files.

Viewing 4 posts - 1 through 4 (of 4 total)
  • You must be logged in to reply to this topic.