Building PowerBuilder Extensions

About this chapter

This chapter describes how to build a PowerBuilder extension. It begins with a sample application that uses a simple nonvisual extension.

Nonvisual extension example

To illustrate the principles involved in building and using an extension, this chapter starts with a sample application that uses a PowerBuilder extension to perform a simple arithmetic operation. Ordinarily, this is not a task that needs PBNI, but it is used here to make the basic process clear. The rest of this chapter describes building extensions in more detail.

PBX file suffix

PowerBuilder extensions are DLL files but typically use the file extension .pbx instead of .dll. Your extension is compiled into a PBX file by default if you use the wizard described in Using the Visual Studio Wizards

For more realistic examples, see the PowerBuilder Code Samples Web site at https://www.appeon.com/developers/library/code-samples-for-pb.

The following sample application has two main steps:

Building the pbadd PowerBuilder extension

In this example, the C++ code is in three files:

  • The class declaration is in a header file, pbadd.h

  • The standard functions that every PowerBuilder extension must expose are in main.cpp

  • The implementation of the class is in pbadd.cpp.

To implement the pbadd extension:

  1. Create the pbadd.h header file.

    The pbadd.h header file declares the pbadd class. The file includes pbext.h, which must be included in all PowerBuilder extensions because it declares the ancestor classes for native classes and the standard functions that the extension must expose. Here is the code for pbadd.h:

    #include "pbext.h"
    class pbadd: public IPBX_NonVisualObject
    {
    public:
       pbadd();
       virtual ~pbadd();
       PBXRESULT Invoke(
          IPB_Session   *session,
          pbobject      obj,
          pbmethodID    mid,
          PBCallInfo    *ci);
    
       int f_add(IPB_Session*, pbint, pbint);
    // Enum used to provide entry points for each
    // method in the class - the only one in this case
    // is mAdd   enum MethodIDs
       {
          mAdd = 0 
       };
    
    private:
       virtual void Destroy();
    };
  2. Create the main.cpp file, which includes pbadd.h and implements the standard functions, PBX_GetDescription and PBX_CreateNonvisualObject:

    • PBX_GetDescription is used to pass the descriptions of classes in the extension to PowerBuilder.

    • The PBX_CreateNonVisualObject method creates the object instance. The PowerScript CREATE statement maps to this PBNI method.

    The following is the code for main.cpp:

    #include "pbadd.h"
    // initialize the PBX
    BOOL APIENTRY DllMain(HANDLE hModule,
                    DWORD ul_reason_for_all,
                     LPVOID lpReserved
                  )
    {
       switch(ul_reason_for_all)
       {
          case DLL_PROCESS_ATTACH:
          case DLL_THREAD_ATTACH:
          case DLL_THREAD_DETACH:
          case DLL_PROCESS_DETACH:
             break;
       }
       return TRUE;
    }
    
    
    // describe the pbadd class
    PBXEXPORT LPCTSTR PBXCALL PBX_GetDescription()
    {
       static const TCHAR desc[]={
          "class pbadd from nonvisualobject \n" \
          "function int f_add(int a,int b)\n" \
          "end class \n"
       };
    return desc;
    }
    
    
    // export the required PBX_CreateNonVisualObject
    // function so that the PBVM can
    // create an instance of the class
    PBXEXPORT PBXRESULT PBXCALL PBX_CreateNonVisualObject
    (
       IPB_Session*    pbSession,
       pbobject        pbobj,
       LPCSTR          xtraName,
       IPBX_NonVisualObject   **obj
    )
    {
       // if the calling function requests the pbadd
       // class, create an instance
       if (strcmp(xtraName,"pbadd")==0)
          {
             *obj=new pbadd;
       }
       return 0;
    };
  3. Create the pbadd.cpp file, which includes pbadd.h and contains the implementation of the pbadd class and its single method, f_add.

    #include "pbadd.h"
    
    // Implement the required Invoke method
    PBXRESULT pbadd:: Invoke(IPB_Session *Session,
       pbobject obj, pbmethodID mid, PBCallInfo *ci)
    {
       // if the method to call is f_add
       if (mid == mAdd)
       {
          int sum = f_add(Session, ci->pArgs->GetAt(0)->
             GetInt(), ci->pArgs->GetAt(1)->GetInt());
          ci->returnValue->SetInt(sum);
       }
       return PBX_OK;
    }
    
    // constructor and destructor
    pbadd:: pbadd()
    {
    }
    pbadd:: ~pbadd()
    {
    }
    
    // implement the class's f_add method
    int pbadd:: f_add(IPB_Session* session, pbint arg1,
       pbint arg2)
    {
       return arg1+arg2;
    }
    
    // Implement the required Destroy method
    void pbadd::Destroy()
    {
       delete this;
    }

To compile and link the PBX:

  • In your C++ development tool or on the command line, compile and link the PBX.

    Make sure the include directory in %AppeonInstallPath%\PowerBuilder [version]\SDK\PBNI is in your include path. For this example, the generated DLL is called pbadd.pbx.

Using the extension in PowerBuilder

To use the PowerBuilder native class in a PowerBuilder application, import the object descriptions in the PBX file into a library in your application.

To import the extension into an application:

  1. Copy the PBX (or DLL) file to a directory on your application's path.

  2. In PowerBuilder, create a new workspace.

  3. On the Target page of the New dialog box, select the Application icon to create a new target, library, and application object.

  4. In the System Tree, expand the new target, right-click the library, and select Import PB Extension from the pop-up menu.

  5. Navigate to the location of the pbadd.pbx file and click Open.

To invoke the f_add function in PowerBuilder:

  1. Create a new window called w_add, and add three single-line edit boxes and a command button to it.

  2. Declare an instance variable called mypbadd for the pbadd native class, and then add this script to the button's Clicked event:

    TRY
       mypbadd = CREATE pbadd
       sle_3.text = string (mypbadd.f_add( &
          integer(sle_1.text), integer(sle_2.text)))
    CATCH (runtimeerror re)
       MessageBox("Error", &
          "pbadd native class could not be created: " + &
          re.getmessage() )
    END TRY

    The pbadd class displays in the System Tree. As shown in the following screen shot, you can expand its function list:

  3. Add open(w_add)to the application's Open event.

  4. Run the application.

The application runs just as it would if you had created a custom class user object in PowerBuilder with an f_add function. If PowerBuilder cannot find pbadd.pbx, the runtime error in the Clicked event script will be triggered and caught. Put pbadd.pbx in the same directory as the executable or the PowerBuilder runtime DLLs to make sure it can be found.

Creating a PowerBuilder extension

To build a PowerBuilder extension, follow these steps:

Step 1: Decide on a feature to implement.

Step 2: Define the classes and functions in the extension .

Step 3: Declare native classes and global functions.

Step 4: Implement native classes and global functions.

Step 5: Export methods to create class instances.

Step 6: Build a PBX .

These steps apply whether you are building a nonvisual or a visual extension. The differences between building nonvisual and visual extensions are described in Creating and using a visual extension. This section focuses primarily on nonvisual extensions.

Required methods

All PowerBuilder nonvisual extensions must export two methods: PBX_GetDescription and PBX_CreateNonVisualObject. The use of these methods is described in Step 2: Define the classes and functions in the extension and Step 5: Export methods to create class instances.

PowerBuilder visual extensions must export PBX_GetDescription and PBX_CreateVisualObject. See Creating and using a visual extension.

If the extension declares global functions, it must also export the PBX_InvokeGlobalFunction method.

For every native class, you must implement two PBNI methods, Invoke and Destroy, in addition to the methods the class will provide. The use of these PBNI methods is described in Step 4: Implement native classes and global functions.

Step 1: Decide on a feature to implement

The first step in building a PowerBuilder extension is to identify a problem that an extension can solve. This might be a feature that can be coded more efficiently and easily in C++ than in PowerScript, or that requires the use of callback functions or nonstandard datatypes. You might also have access to existing C++ classes that perform the tasks you want to add to a PowerBuilder application, or you might want to create a wrapper for existing standard utilities written in C++.

For possible uses of PowerBuilder extensions, see Understanding PowerBuilder extensions.

For examples of PowerBuilder extensions, see the PowerBuilder Code Samples website at https://www.appeon.com/developers/library/code-samples-for-pb.

Step 2: Define the classes and functions in the extension

Your C++ code must expose two standard methods that enable PowerBuilder to recognize each native class and create instances of the class. One of these methods is PBX_GetDescription.

Use PBX_GetDescription to pass the descriptions of classes and global functions in the PowerBuilder extension to PowerBuilder. Every extension must export this method. Importing the PBX or DLL file into a PBL converts the description of the extension into PowerScript and adds it to the PBL as source code. The keyword native in the source code indicates that the PowerBuilder type was defined in an extension.

All the classes or global functions in an extension module are passed in a single description. The examples that follow illustrate how you define classes and functions in a description. For the full syntax, see PBX_GetDescription.

Describing nonvisual classes

Nonvisual classes can inherit from the NonVisualObject PowerBuilder system class or any of its descendants. While a native class can inherit from a user-defined user object, Appeon recommends that you use only system classes. Each native class can provide several functions, subroutines, and events.

The following example shows how you use the PBX_GetDescription method in the C++ code for an extension that includes three nonvisual classes. ClassName1 inherits from NonVisualObject, ClassName2 inherits from Exception, and ClassName3 inherits from Transaction. All three classes must be in a single description passed by PBX_GetDescription:

PBXEXPORT LPCTSTR PBXCALL PBX_GetDescription()
{
  static const TCHAR desc[] = {
   // Description begins here
   "class ClassName1 from NonVisualObject\n"
    "function integer objectFunction(integer a[])\n"
    "subroutine objectSubroutine(integer ai_ref)\n"
    "event integer eventName(integer b)\n"
    "end class\n"

    "class ClassName2 from Exception\n"
    "function integer objectFunction(readonly 
       integer ai)\n"
    "subroutine objectSubroutine(integer arg)\n"
    "event integer eventName(integer arg)\n"
    "end class\n"

    "class ClassName3 from Transaction\n"
    "function integer objectFunction(integer arg)\n"
    "subroutine objectSubroutine(integer arg)\n"
    "event integer eventName(integer arg)\n"
    "end class\n"
    // Description ends here
  };
  return desc;
}

Describing visual classes

Visual native classes can inherit only from the UserObject PowerBuilder system class. The PowerBuilder VM considers any class that inherits from UserObject to be a visual class. All other native classes are considered to be nonvisual classes. For more information about how to describe visual classes, see Creating and using a visual extension.

Describing global functions

An extension can include global functions as well as classes. This example shows a description for two global functions:

"globalfunctions \n" \
"function int g_1(int a, int b)\n" \
"function long g_2(long a, long b)\n" \
"end globalfunctions\n"

The syntax and usage of global functions defined in an extension are the same as for global functions defined in the Function painter in PowerBuilder.

Global functions cannot be overloaded

Like global functions in PowerScript, global functions in a PowerBuilder extension cannot be overloaded.

Using forward declarations

PowerBuilder extensions can provide multiple classes. A class can reference any class that is defined earlier in the description, but if it references a class defined later in the description, you must provide a forward declaration. This example shows a description that includes forward declarations for two classes, nativeclass_1 and nativeclass_2, that reference each other. This example also demonstrates that a single description can include global functions as well as classes:

"forward\n" \
"class nativeclass_1 from nonvisualobject\n"\
"class nativeclass_2 from nonvisualobject\n"\
"end forward\n" \

"class nativeclass_1 from nonvisualobject \n" \
"function int add(nativeclass_2 a, int b)\n" \
"function int sub(int a, int b)\n" \
"end class \n" \

"class nativeclass_2 from nonvisualobject \n" \
"function int add(nativeclass_1 a, int b)\n" \
"function int sub(int a, int b)\n" \
"end class \n"

"globalfunctions \n" \
"function int g_1(int a, int b)\n" \
"function long g_2(long a, long b)\n" \
"end globalfunctions\n"

Step 3: Declare native classes and global functions

For each native class that the nonvisual extension supports, declare an ANSI C++ class that inherits from IPBX_NonVisualObject, which is the ancestor class for all nonvisual PowerBuilder native classes.

The declaration of the class can be placed in a header file, and it must include Invoke and Destroy methods. This is a simple prototype for a nonvisual class:

#include "pbext.h"

class CMyClass : public IPBX_NonVisualObject
{
enum MethodIDs
{
   mFunca = 0,
   mFuncb = 1
};
public:
   // constructor, destructor
   CMyClass()
   virtual ~CMyClass()
   // member methods   PBXRESULT Invoke(
      IPB_Session   *session, 
      pbobject   obj, 
      pbmethodID   mid,
      PBCallInfo   *ci
   );
   void Destroy();

private:
   void funcA(IPB_Session* session, pbobject obj,
       PBCallInfo* ci);
   void funcB(IPB_Session* session, pbobject obj,
       PBCallInfo* ci);
};

If you declare global functions in your extension, the extension must export the PBX_InvokeGlobalFunction method. The following PBX_GetDescription call declares three global functions: bitAnd, bitOr, and bitXor:

PBXEXPORT LPCTSTR PBXCALL PBX_GetDescription()
{
   static const TCHAR desc[] = {
      "globalfunctions\n"
      "function int bitAnd(int a, int b)\n"
      "function int bitOr(int a, int b)\n"
      "function int bitXor(int a, int b)\n"
      "end globalfunctions\n"
   };

  return desc;
}

Step 4: Implement native classes and global functions

The implementation of each class must include the implementation of the Invoke and Destroy methods, as well as all the methods declared for the class. Invoke and Destroy are methods of the IPBX_UserObject interface.

When the PowerBuilder application calls a method on the native class, the PBVM calls the Invoke method, which dispatches the call based on the method ID or method name. The method name is used when the method is called dynamically.

The Invoke method must be coded to invoke each method in the class. The example that follows shows a switch-case statement that invokes either funcA or funcB, depending on the value of the method ID. When the PowerBuilder application has finished using an instance of a native class, the PBVM calls the Destroy method.

This example does not show the implementation of the methods of the class itself:

PBXRESULT MyClass::Invoke(IPB_Session *session, pbobject obj, pbmethodID mid, PBCallInfo *ci)
{
   PBXRESULT result = PBX_OK;

   switch (mid)
   {
   case mFunca:
      result = funcA(session, obj, ci);
      break;

   case mFuncb:
      result = funcB(session, obj, ci);
      break;

   default:
      result = PBX_E_INVOKE_FAILURE;
      break;
   }

   return result;
}
// Implementation of funcA and funcB not shown
void Destroy()
   {
      delete this;
}

The following PBX_InvokeGlobalFunction contains the implementation of the three global functions included in the description shown in Step 3: Declare native classes and global functions:

PBXEXPORT PBXRESULT PBXCALL PBX_InvokeGlobalFunction
(
   IPB_Session*   pbsession,
   LPCTSTR        functionName,
   PBCallInfo*    ci   )
{

  PBXRESULT pbrResult = PBX_OK;

  int arg1 = ci->pArgs->GetAt(0)->GetInt();
  int arg2 = ci->pArgs->GetAt(1)->GetInt();

  if (stricmp(functionName, "bitand") == 0)
  {
     ci->returnValue->SetInt(arg1 & arg2);
  }else if (strcmp(functionName, "bitor") == 0)
  {
     ci->returnValue->SetInt(arg1 | arg2);
  }else if (strcmp(functionName, "bitxor") == 0)
  {
     ci->returnValue->SetInt(arg1 ^ arg2);
  }else
  {
     return PBX_FAIL;
  }

  return pbrResult;
}

Step 5: Export methods to create class instances

PowerBuilder creates nonvisual and visual class instances differently:

  • For visual classes, the instance is created when the window or visual control in which the class is used is opened. See Creating visual class instances.

  • For nonvisual classes, the instance is created when the PowerBuilder CREATE statement is used. This is described next.

When the PowerBuilder application creates an instance of a nonvisual class using the PowerScript CREATE statement, the PBVM calls the PBX_CreateNonVisualObject method in the extension. Every extension that contains nonvisual native classes must export this method.

In the same way that multiple classes are included in a single description passed by PBX_GetDescription, PBX_CreateNonVisualObject can be used to create multiple classes.

In this example, the extension has three classes. An IF statement compares the name of the class passed in from the PowerBuilder CREATE statement to the name of each of the classes in the extension in turn and creates an instance of the first class with a matching name. You could also use a CASE statement. The class name in the string comparison must be all lowercase:

PBXEXPORT PBXRESULT PBXCALL PBX_CreateNonVisualObject(
   IPB_Session * session, 
   pbobject obj, 
   LPCSTR className, 
   IPBX_NonVisualObject **nvobj
)
{
   PBXRESULT result = PBX_OK;
   // The class name must not contain uppercase
   if ( strcmp( className, "classone" ) == 0 )
      *nvobj = new ClassOne;
   else if ( strcmp( className, "classtwo" ) == 0 )
      *nvobj = new ClassTwo( session );
   else if ( strcmp( className, "classthree" ) == 0 )
      *nvobj = new ClassThree;
   else   {
      *nvobj = NULL;
      result = PBX_E_NO_SUCH_CLASS;
   }
   return PBX_OK;
};

Step 6: Build a PBX

Using your C++ development tool or the command line, build a PBX from your C++ classes.

When you compile and link the C++ code, verify the following:

  • The include directory for the PBNI SDK, typically PowerBuilder 19.0\SDK\PBNI\include, must be in your include path.

  • If you use any helper classes, make sure the source file that contains them is added to your project. For a list of classes, see the table in The PBNI SDK.

Now you are ready to use the extension in a PowerBuilder application.

Adding an extension to a PowerBuilder target

The simplest way to add a PowerBuilder native class to a PowerBuilder target is to import the object descriptions in the PBX file into a library in the PowerBuilder System Tree. You can also use a command-line tool to create a PBD file from a PBX file and add it to the target's library search path. See pbx2pbd190.

To import the descriptions in an extension into a library:

  1. Copy the PBX file into a directory on the application's path.

  2. In the System Tree, expand the target in which you want to use the extension, right-click a library, and select Import PB Extension from the pop-up menu.

  3. Navigate to the location of the PBX file and click Open.

Each class in the PBX displays in the System Tree so that you can expand it, view its properties, events, and methods, and drag and drop to add them to your scripts.

Using the extension

Using nonvisual classes

In PowerScript, use the classes in a nonvisual extension just as you would a custom class user object: Declare an instance of the object, use the CREATE statement to create the instance, invoke the object's functions, and destroy the instance when you have finished with it. You can inherit from the native classes if you want to add functions or events to the class.

At runtime, instances of the native class are created as normal PowerBuilder objects.

In this example, the extension module contains two nonvisual native classes: fontcallback and fontenumerator. A PowerBuilder custom class user object, nvo_font, inherits from the fontcallback class. These statements create instances of both classes:

fontenumerator fe
nvo_font uf
fe = create fontenumerator
uf = create nvo_font

After an instance of a native class has been created, the PowerBuilder application can call methods on the object. Each native class must implement an Invoke method that the PowerBuilder VM calls when the PowerBuilder application calls one of the native class's methods. Then, the Invoke method dispatches the method call based on the method ID or method name. The method name is used when a native method is called dynamically.

Using the previous example, this statement invokes the enumprinterfonts method of the instance of the fontenumerator class:

fe.enumprinterfonts(uf)

Destroying the PBNI object instance

When the PowerBuilder application no longer needs an instance of a nonvisual class and a DESTROY statement is issued, by either the user or the garbage collector, or when the window or visual control that contains a visual class is closed, the PowerBuilder VM destroys the instance by calling the native class's Destroy method.

Creating and using a visual extension

In general, you follow the same steps to create and use a visual extension that you do to create a nonvisual extension:

Step 1: Decide on a feature to implement.

Step 2: Define the classes and functions in the extension.

Step 3: Declare visual classes and global functions.

Step 4: Implement native classes.

Step 5: Export methods to create class instances.

Step 6: Build and use a PBX.

Step 7: Use the visual extension in an application.

Using PowerBuilder visual objects in C++

For information about using PowerBuilder visual objects from a C++ application, see Processing PowerBuilder messages in C++.

Step 1: Decide on a feature to implement

You can choose to use visual extensions to implement controls with a specific purpose or that use a custom look and feel. For some examples of visual extensions, see the PowerBuilder Code Samples website at https://www.appeon.com/developers/library/code-samples-for-pb.

Step 2: Define the classes and functions in the extension

The description for a visual class follows the same rules as for a nonvisual class, but it must inherit from the UserObject system class:

PBXEXPORT LPCTSTR PBXCALL PBX_GetDescription()
{
   static const TCHAR desc[] = {
      "class myvisualext from userobject\n"
      "subroutine func_1(int arg1, int arg2)\n"
      "subroutine func_2(string arga)\n"
      "end class\n" 
   };
return desc;
}

There are no events in the preceding example, but a typical visual extension makes use of events such as mouse clicks. There are two ways to declare and handle events. See Event processing in visual extensions.

Step 3: Declare visual classes and global functions

You declare native visual classes in the same way as nonvisual classes, except that you declare an ANSI C++ class that inherits from IPBX_VisualObject, which is the ancestor class for all nonvisual PowerBuilder native classes, instead of from IPBX_NonVisualObject. You can also declare global functions in a visual extension. See Step 3: Declare native classes and global functions in the section on nonvisual extensions.

Step 4: Implement native classes

You implement Invoke and Destroy methods and any class or global functions the same way for visual extensions as for nonvisual extensions. See Step 4: Implement native classes and global functions.

Step 5: Export methods to create class instances

The major difference between visual and nonvisual extensions is in how instances of the class are created. See Creating visual class instances.

Step 6: Build and use a PBX

As for nonvisual extensions, you must build a PBX, import it into the application, and put the PBX in the execution path. See Step 6: Build a PBX and Adding an extension to a PowerBuilder target in the section on nonvisual extensions.

Step 7: Use the visual extension in an application

You do not need to declare an instance of a visual class or use the CREATE statement to create an instance. The PBVM creates an instance when the window or visual control in which the visual class resides is opened, as described in Creating visual class instances. You can invoke the object's functions the same way that you invoke a nonvisual object's functions.

To use a visual extension:

  1. Select File>Inherit from the PowerBuilder menu and select the PBD in the Libraries list in the Inherit from Object dialog box.

  2. Select the visual class and click OK.

  3. In the User Object painter, size the visual object and make any other changes you need.

  4. Save the object.

You can now drag the new user object from the System Tree directly onto a window or onto another visual control, such as a tab control, and use it like any other visual user object.

Code samples

The code fragments in the rest of this section are based on complete sample applications that you can find on the PowerBuilder Code Samples website at https://www.appeon.com/developers/library/code-samples-for-pb.

Creating visual class instances

When the window or other visual control in which a visual native class resides is created in a PowerBuilder application, the PBVM calls the PBX_CreateVisualObject method in the extension automatically -- the PowerBuilder application developer does not need to write a CREATE statement in a script. The PBVM also calls the IPBX_VisualObject's CreateControl method. Every extension that contains visual native classes must export PBX_CreateVisualObject and implement CreateControl.

The following is sample code for PBX_CreateVisualObject:

PBXEXPORT PBXRESULT PBXCALL PBX_CreateVisualObject
(
   IPB_Session*         pbsession, 
   pbobject            pbobj,
   LPCTSTR               className,
         IPBX_VisualObject   **obj
)
{
   PBXRESULT result = PBX_OK;

   string cn(className);
   if (cn.compare("visualext") == 0)
   {
      *obj = new CVisualExt(pbsession, pbobj);
   }
   else
   {
      *obj = NULL;
      result = PBX_FAIL;
   }

   return PBX_OK;
};

Registering the window class

Before CreateControl can be called, the window class must be registered. This code uses the Windows RegisterClass method to register the window class with the class name s_className:

void CVisualExt::RegisterClass()
{
   WNDCLASS wndclass;

   wndclass.style = CS_GLOBALCLASS | CS_DBLCLKS;
   wndclass.lpfnWndProc = WindowProc;
   wndclass.cbClsExtra = 0;
   wndclass.cbWndExtra = 0;
   wndclass.hInstance = g_dll_hModule;
   wndclass.hIcon = NULL;
   wndclass.hCursor = LoadCursor (NULL, IDC_ARROW);
   wndclass.hbrBackground =(HBRUSH) (COLOR_WINDOW + 1);
   wndclass.lpszMenuName = NULL;
   wndclass.lpszClassName = s_className;

   ::RegisterClass (&wndclass);
}

You must also implement the Windows UnregisterClass method to unregister the class when the window is closed:

void CVisualExt::UnregisterClass()
{
   ::UnregisterClass(s_className, g_dll_hModule);
}

The RegisterClass and UnregisterClass methods are called in the initialization code for the PBX. This is the Visual C++ DllMain method:

BOOL APIENTRY DllMain( HANDLE hModule,
                       DWORD  reasonForCall,
                       LPVOID lpReserved
                     )
{
   g_dll_hModule = (HMODULE)hModule;

   switch (reasonForCall)
   {
      case DLL_PROCESS_ATTACH:
         CVisualExt::RegisterClass();
          break;

      case DLL_THREAD_ATTACH:
      case DLL_THREAD_DETACH:
         break;

      case DLL_PROCESS_DETACH:
         CVisualExt::UnregisterClass();
         break;
   }
   return TRUE;
}

Implementing CreateControl

Every visual native class must implement the IPBX_VisualObject CreateControl method. After getting the class name with the IPBX_VisualObject GetClassName method, CreateControl passes the class name to the Windows CreateWindowEx method to create the window, then returns the window handle to the PBVM:

TCHAR CVisualExt::s_className[] = "PBVisualExt";

LPCTSTR CVisualExt::GetWindowClassName()
{
   return s_className;
}

HWND CVisualExt::CreateControl
(
   DWORD dwExStyle,      // extended window style
   LPCTSTR lpWindowName, // window name
   DWORD dwStyle,        // window style
   int x,             // horizontal position of window
   int y,                // vertical position of window
   int nWidth,           // window width
   int nHeight,          // window height
   HWND hWndParent,      // handle to parent or
                         // owner window
   HINSTANCE hInstance   // handle to application
                         // instance
)
{
   d_hwnd = CreateWindowEx(dwExStyle, s_className,
      lpWindowName, dwStyle, x, y, nWidth, nHeight,
      hWndParent, NULL, hInstance, NULL);

   ::SetWindowLong(d_hwnd, GWL_USERDATA, (LONG)this);
   return d_hwnd;
}

Event processing in visual extensions

A visual extension can have a window procedure that can process any Windows message or user-defined message. The PBVM passes all such messages to the visual extension, which can intercept messages and either process or ignore them.

WindowProc is an application-defined callback function that processes messages sent to a window. In the example in this section, a WM_PAINT message is sent to the extension when an action in the PowerBuilder application causes the window to be redrawn. When the extension receives the message, it repaints an area in the window using the current values for text and color set by the user of the application.

The following example also captures mouse clicks and double clicks, and triggers the Onclick and Ondoubleclick event scripts in the PowerBuilder application. You can use two different syntaxes for describing events:

event returnType eventName(args_desc) newline
event eventName pbevent_token newline

Using an event name with return type and arguments

The following description uses the first syntax. The class has two events, onclick and ondoubleclick:

PBXEXPORT LPCTSTR PBXCALL PBX_GetDescription()
{
   static const TCHAR desc[] = {
      "class visualext from userobject\n"
      "event int onclick()\n"
      "event int ondoubleclick()\n"
      "subroutine setcolor(int r, int g, int b)\n"
      "subroutine settext(string txt)\n"
      "end class\n" 
   };
return desc;
}

Capturing messages and mouse clicks

The code in the extension captures the Windows messages that cause the window to be drawn, as well as mouse clicks and double clicks:

LRESULT CALLBACK CVisualExt::WindowProc(
                                   HWND hwnd,
                                   UINT uMsg,
                                   WPARAM wParam,
                                   LPARAM lParam
                                   )
{
   CVisualExt* ext = (CVisualExt*)::GetWindowLong(hwnd,
      GWL_USERDATA);
   switch(uMsg) {

   case WM_CREATE:
      return 0;

   case WM_SIZE:
      return 0;

   case WM_COMMAND:
      return 0;

   case WM_PAINT: {
      PAINTSTRUCT ps;
      HDC hdc = BeginPaint(hwnd, &ps);
      RECT rc;
      GetClientRect(hwnd, &rc);
      LOGBRUSH lb;
      lb.lbStyle = BS_SOLID;
 
// Get color using the visual class's GetColor method
      lb.lbColor = ext->GetColor();
      HBRUSH hbrush = CreateBrushIndirect(&lb);
      HBRUSH hbrOld = (HBRUSH)SelectObject(hdc,
         hbrush);
      Rectangle(hdc, rc.left, rc.top, rc.right-rc.left,
         rc.bottom-rc.top);
      SelectObject(hdc, hbrOld);
      DeleteObject(hbrush);


// Get text using the visual class's GetText method
      DrawText(hdc, ext->GetText(), 
         ext->GetTextLength(), &rc, 
      DT_CENTER|DT_VCENTER|DT_SINGLELINE);
      EndPaint(hwnd, &ps);
      }
      return 0;

// Trigger event scripts in the PowerBuilder application
   case WM_LBUTTONUP:
      ext->TriggerEvent("onclick");
      break;

   case WM_LBUTTONDBLCLK:
      ext->TriggerEvent("ondoubleclick");
      break;
   }
   return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Triggering click events

The following is the TriggerEvent method that triggers the Onclick and Ondoubleclick events:

void CVisualExt::TriggerEvent(LPCTSTR eventName)
{
   pbclass clz = d_session->GetClass(d_pbobj);
   pbmethodID mid = d_session->GetMethodID(clz,
      eventName, PBRT_EVENT, "I");

   PBCallInfo ci;
   d_session->InitCallInfo(clz, mid, &ci);
   d_session->TriggerEvent(d_pbobj, mid, &ci);
   d_session->FreeCallInfo(&ci);
}

Using an event name with a PowerBuilder event ID

A simpler way to trigger events in a visual extension uses direct mapping of Windows messages to PowerBuilder events. The following class description contains the same two events, but in this case they use the alternative syntax that maps the event name to a PowerBuilder token name:

PBXEXPORT LPCTSTR PBXCALL PBX_GetDescription()
{
   static const TCHAR desc[] = {
      "class visualext from userobject\n"
      "event onclick pbm_lbuttonup\n"
      "event ondoubleclick pbm_lbuttondblclk\n"
      "subroutine setcolor(int r, int g, int b)\n"
      "subroutine settext(string txt)\n"
      "end class\n" 
   };
return desc;
}

Generating event syntax automatically

Importing the extension generates the Onclick and Ondoubleclick events with the appropriate arguments automatically, and at runtime, the PBVM fires the events. You do not need to capture the Windows messages WM_LBUTTONUP and WM_LBUTTONDBLCLK in the extension.

In the following description, onclick is the event name and pbm_lbuttonup is the event token name. Notice that the event name is not followed by empty parentheses as it is when you use the return type and arguments technique for describing the event:

 "event onclick pbm_lbuttonup\n"

About the token name

The token name is a string that maps to an internal PowerBuilder event ID defined in the header file pbevtid.h. The first ID in this file is PB_NULL. For all other IDs in the file, there is a fixed relationship between the name that you use in the description and the event ID in pbevtid.h. The name is the same as the ID with the letter m appended to the pb prefix. You must use lowercase in the description.

For example, the event ID PB_ACTIVATE in pbevtid.h maps to the token name pbm_activate. In the description provided with PBX_GetDescription, you must use the name pbm_activate. If the event name you provide does not exist, importing the extension generates an error message. See the pbevtid.h file for a complete list of mapped IDs.

Processing events sent to the parent of the window

Some Windows messages, such as WM_COMMAND and WM_NOTIFY, are sent to the parent of an object and not to the object itself. Such messages cannot be caught in the visual extension's window procedure. The PBVM calls the GetEventID method to process these messages, as follows:

  • If the message is mapped to a PowerBuilder event, GetEventID returns the event's identifier, for example PB_BNCLICKED, and the event is fired automatically.

  • If the message is not mapped to an event, GetEventID returns the value PB_NULL and the message is discarded.

In the following example, the GetEventID function returns the identifier PB_BNCLICKED if a WM_COMMAND message with the notification code BN_CLICKED was sent. It returns the identifier PB_ENCHANGE if a WM_NOTIFY message was sent. Otherwise, it returns PB_NULL.

TCHAR CVisualExt::s_className[] = "PBVisualExt";

LPCTSTR CVisualExt::GetWindowClassName()
{
  return s_className;
}

HWND CVisualExt::CreateControl
(
  DWORD dwExStyle,      // extended window style
  LPCTSTR lpWindowName, // window name
  DWORD dwStyle,        // window style
  int x,               // horizontal position of window
  int y,            // vertical position of window
  int nWidth,       // window width
  int nHeight,      // window height
  HWND hWndParent,  // handle of parent or owner window
  HINSTANCE hInstance // handle of application instance
)
{
  d_hwnd = CreateWindowEx(dwExStyle, s_className,
    lpWindowName, dwStyle, x, y, nWidth, nHeight,
    hWndParent, NULL, hInstance, NULL);

  ::SetWindowLong(d_hwnd, GWL_USERDATA, (LONG)this);

  return d_hwnd;
}

int CVisualExt::GetEventID(
   HWND    hWnd,     /* Handle of parent window */
   UINT    iMsg,     /* Message sent to parent window*/
   WPARAM  wParam,   /* Word parameter of message*/
   LPARAM  lParam    /* Long parameter of message*/
  )
{
  if (iMsg == WM_COMMAND)
  {
    if ((HWND)lParam == d_hwnd)
    {
      switch(HIWORD(wParam))
      {
      case BN_CLICKED:
        return PB_BNCLICKED;
        break;
      }
    }
  }

  if (iMsg == WM_NOTIFY)
  {
    return PB_ENCHANGE;
  }
  return PB_NULL;
}

Calling PowerScript from an extension

You can call PowerBuilder system functions through IPB_Session. The InitCallInfo method simplifies the process of setting up the call information. You need to provide the arguments to the InitCallInfo method, including an identifier for the PowerBuilder function you want to call.

The identifier can be returned from the GetMethodID or FindMatchingFunction method.

Using GetMethodID

To get the function's ID using the GetMethodID method, you need the function's signature:

PbmethodID GetMethodID(pbclass cls, LPCTSTR
   methodName, PBRoutineType rt, LPCTSTR signature);

The signature argument in this method call is a string representing the method's return type and arguments. You can obtain this string in the Browser.

For example, to obtain the signature of a system function, select systemfunctions from the left pane of the System page, right-click the function in the right pane, and select Properties from its pop-up menu:

For methods in your application, you can expand the object that contains it in the System Tree, select the function or event, and select Properties from its pop-up menu:

Consider this function:

of_get_trans ( ref transaction atr_trans ) returns (none)

The signature for this function is QRCtransaction. Q indicates that the function does not return a value, R that the argument is passed by reference, and Ctransaction that the argument is a PowerBuilder system object of type transaction.

You can use the pbsig190 command-line tool to obtain a function's signature. However, the pbsig190 tool does not report the signature of functions that are inherited from an ancestor object unless they are extended in the descendant, and it does not report event signatures.

For more information about using pbsig190, and an explanation of all the formats used in the signature, see pbsig190.

Using FindMatchingFunction

Instead of the string that GetMethodID uses, the FindMatchingFunction function provides another way to get the method ID. Some short signatures can be difficult to parse, and signatures that include PowerBuilder system objects or Java classes can be much longer.

FindMatchingFunction uses a "readable signature" instead of the string used by GetMethodID:

FindMatchingFunction(pbclass cls, LPCTSTR methodName, PBRoutineType rt, LPCTSTR readableSignature) 

The readableSignature argument is a comma-separated list of the arguments of the function. Unlike the string used by GetMethodID, it does not include the return type. For example, for a function called uf_test that takes two arguments, an int by value and a double by reference, the call to FindMatchingFunction looks like this:

mid = Session -> FindMatchingFunction(cls, "uf_test",
   PBRT_FUNCTION, "int, double");

Invoking PowerBuilder functions

The following methods are those you use most frequently to invoke PowerBuilder functions. For descriptions of each method, see IPB_Session interface.

PbmethodID GetMethodID(pbclass cls, LPCTSTR methodName,
    PBRoutineType rt, LPCTSTR signature, pbboolean publiconly)
PBXRESULT InitCallInfo(pbclass cls, pbmethodID mid, PBCallInfo *ci)
void FreeCallInfo(PBCallInfo *ci)
PBXRESULT Add<Type>Argument(PBCallInfo *ci, PBType v);
PBXRESULT InvokeClassFunction(pbclass cls, pbmethodID mid, PBCallInfo *ci)
PBXRESULT InvokeObjectFunction(pbobject obj, pbmethodID mid, PBCallInfo *ci)
PBXRESULT TriggerEvent(pbobject obj, pbmethodID mid, PBCallInfo *ci)

Example: Calling PowerBuilder functions

In this code fragment, the class and method ID returned by calls to the IPB_Session GetClass and GetMethodID methods are used to initialize a PBCallInfo structure, called ci, using the IPB_Session InitCallInfo method.

After a new pbstring variable is created, the value of that string is set to the value of the first argument in the PBCallInfo structure.

BOOL CALLBACK CFontEnumerator::EnumFontProc
(
   LPLOGFONT lplf, 
   LPNEWTEXTMETRIC lpntm, 
   DWORD FontType, 
   LPVOID userData
)
{
   UserData* ud = (UserData*)userData;
   pbclass clz = ud->session->GetClass(ud->object);
   pbmethodID mid = ud->session->GetMethodID
      (clz, "onnewfont", PBRT_EVENT, "IS");

   PBCallInfo ci;
   ud->session->InitCallInfo(clz, mid, &ci);

// create a new string variable and set its value
// to the value in the first argument in the 
// PBCallInfo structure
   pbstring str = ud->session->NewString
      (lplf->lfFaceName);
   ci.pArgs->GetAt(0)->SetString(str);

   ud->session->TriggerEvent(ud->object, mid, &ci);
   pbint ret = ci.returnValue->GetInt();

   ud->session->FreeCallInfo(&ci);
   return ret == 1 ? TRUE : FALSE;
}

Exception handling and debugging

To handle errors, you use the error codes returned from PBNI methods. Some functions of the IPB_Session interface return detailed error codes that make debugging easier.

Native methods, such as the IPBX_UserObject Invoke method, return either PBX_OK or PBX_FAIL if the extension encounters a serious problem from which it cannot recover.

Whenever the PowerBuilder VM gets PBX_FAIL from a native method, it throws a PBXRuntimeError in the PowerBuilder application. PBXRuntimeError inherits from the PowerBuilder RuntimeError system object and can be caught and handled in a script in the same way as any exception in PowerBuilder.

To catch these errors, wrap your PowerScript call in a try-catch block as follows:

TRY
   n_cpp_pbniobj    obj
   obj = CREATE n_cpp_pbniobj
   obj.of_test( arg1 )
CATCH ( PBXRuntimeError re )
   MessageBox( "Caught error", re.getMessage() )
END TRY

The IPB_Session interface provides a set of methods to handle exceptions that occur in native code. Use HasExceptionThrown to determine whether an exception occurred. If it did, use GetException to get the current exception object so that it can be handled. If necessary, you can throw exceptions to PowerBuilder with ThrowException. When an exception has been handled, use ClearException to clear it.

Debugging

You cannot edit a native class in the PowerBuilder development environment, and you cannot enter native methods in the PowerBuilder debugger because the methods are C++ methods. You must use a C/C++ debugger to debug an extension module.