Advanced Visual Basic - Project 9

Using an ActiveX application as an OLE Server

What’s so Important about OLE Servers and ActiveX controls?

You mean besides the fact that they’re Cool? Suppose you write a program that contains complex routines that you want to share among several applications. Rather than rewriting the routines in each program, you can expose sections of your code to OLE client-enabled applications. This not only reduces the amount of code you need to write, but also gives you a single, centralized location to make changes when needed. If you need to make modifications to the routines, you only need to make them in one place.

Here’s another powerful feature of OLE Servers applications. Suppose you wrote an order entry program for a company that calculated the minimum profit margin per invoice before a new order could be entered. In a traditional client-server environment, the code for this calculation would be programmed into the front-end application which is deployed on the user’s desktops. This is a good division of labor between the client and server computers, but it makes it very difficult to update your profit margin calculations if they need changing. If the minimum profit margin per invoice changes, you may need to reprogram the client application and re-deploy it onto many desktops. If you were to insert all of the profit margin code into an OLE Server application, it would be far easier to update a single executable (or DLL) file and then distribute only 1 OLE server EXE or DLL to the Server on the network rather than many client applications to the Workstations.

ActiveX and creating Object Classes

Object Classes are Visual Basic’s way of letting you create your own Objects. So far you’ve become adept at using Objects that Visual Basic has provided. These include Textboxes, Command Buttons, Listboxes, etc. Now, by defining your own Object Classes within an ActiveX application, you can create Objects that have both Methods, Properties, and even Event procedures, just like other Visual Basic Objects. (Remember, the difference between methods and propertiesMethods are functions that are built into an object. For example, a Customer object might have a Delete method to remove the customer. A property usually effects the way an Object appears or behaves. For example the Customer object may have an Enabled property which determines whether the User can interact with it or not).

Planning Your OLE Server

Proper planning is probably the most important part of building an OLE server or ActiveX project. Always keep in mind that the Objects in your OLE Server application will be exposed to other applications (and other programmers), so make sure the names of your Objects make sense.

The Object Hierarchy

In this project you are going to create 3 new Objects (with Class Modules) that have one or more properties each:

Object Name Properties What it does
OS Name Returns Operating System name
  Build Returns Operating System build/version
CHIP CPU Returns the type of CPU
MEMORY TotPhys Returns the Total Physical Memory
  AvailPhys Returns the Available Physical Memory
  TotVirt Returns the Total Virtual Memory
  AvailVirt Returns the Available Virtual Memory
Computer OS A Get property that provides access to the Name and Build properties of the OS Object
  CHIP A Get property that provides access to the CPU property of the CHIP Object
  MEMORY A Get property that provides access to the TotPhys, AvailPhys, totVirt, and AvailVirt properties of the MEMORY Object

As an OLE Server, these Objects will be usable by other applications. To simplify access to them you need to create an Object Hierarchy through which they can be accessed:

In the illustration above, access to the OS, CHIP, and MEMORY Objects are made through a single Object—Computer. The group of Objects below the Computer Object are called a Collection. Access to a Collection of Objects via 1 top level Object is a classic construct of OLE Servers. While the Properties of the OS, CHIP, and MEMORY Objects provide the information and data that make this OLE Server useful, it’s the Get Properties of the Computer Object that allow OLE-Client applications to access the properties of the Collection Objects.

Well, I think I’ve said enough to confuse you thoroughly. As you begin to create this project, I’m sure a lot of it will start to make sense. Let’s begin.

Load Visual Basic and select New Project under the File menu. Choose ActiveX DLL as the project type and click the OK button. Instead of a Form, and ActiveX DLL (or EXE of that matter) starts off with a Class Module named by default Class1. A Class Module is similar to a standard Module, except that a Class Module is what you need to define your own custom Objects.

This is how you tell Visual Basic that this application will be an OLE server (DLL) instead of a standalone program:

  1. Open the Properties dialog by selecting Project1 Properties under the Project drop-down menu. Make sure the General tab on the Properties dialog is selected.
  2. In the Startup Object combobox choose Sub Main (we’ll actually be adding the Sub Main procedure later). In the Project Name textbox type Sysinfo. In the Project Description textbox type the following: An OLE Server that provides System Information.
  3. Click on the Component tab and you should see this:

Notice how the ActiveX Component option under Start Mode is selected by default. Because this is an ActiveX DLL project, the Standalone item is not selectable (it must be an ActiveX EXE project for that). In version 4 of Visual Basic, there was actually an OLE Server option button that you could select to make your project an OLE server. In version 5 and up, the ActiveX Component option is synonymous with OLE server. Now click the OK button.

Now you need to create the new Classes for the OS, CHIP and MEMORY Objects. These will be slightly different from the Computer Object:

  1. Add another Class Module to the project by choosing Add Class Module from the Project menu. Choose the Class Module item from the Add Class Module dialog and click the Open button.
  2. Because this Object will be Private and not visible to outside applications, the name should be prefixed with a p (for Private) to avoid confusion later. Set the Name property of this new Class Module to pOS.
  3. Set the Instancing property to PublicNotCreatable. Recall the Object Hierarchy explained above. The properties of the objects that are in the Collection are accessed via the top level Computer object. So these objects do not have their Instancing property set to MultiUse—which would allow other applications to create instances of them with the CreateObject command.

Repeat the above 3 steps twice more to create 2 more Class Modules with the names pCHIP and pMEMORY. Make sure both their Instancing properties are set to PublicNotCreatable.

Choose Save Project from the File menu. Note that Class Modules have CLS extensions. Save these files with these names:

Sysinfo.vbp          (Project)
Computer.cls       (Computer Class Module)
pOS.cls                  (pOS Class Module)
pCHIP.cls              (pCHIP Class Module)
pMEMORY.cls      (pMEMORY Class Module)

Previously you set the Startup Object to Sub Main in the Project Properties dialog box. Even though this application doesn’t need any startup code (it’s an ActiveX DLL and an OLE Server which means it cannot be a Standalone application), you will still need the Sub Main procedure so that later you can test this project. The Sub Main procedure will actually reside in a Module that you will add now.

  1. Pull down the Project menu and choose Add Module. Choose the Module item in the Add Module dialog and click the Open button.
  2. Set the Name property of the module to modSysinfo
  3. In the new Module’s Code Window (below any code that’s already in the General Declarations section, i.e. Option Explicit) type Sub Main and press the Enter key. This creates a Sub Main procedure (we’ll be adding code to it later).
  4. Select Save modSysinfo.bas As from the File menu. Save the Module as Sysinfo.bas (Modules have BAS extensions)

Go to the General Declarations section of the modSysinfo Module (above the Sub Main procedure). It is here that you need to declare the 3 API Functions that will get you information about the Operating System, Build/Version, and Memory status of the computer--so our OS, CHIP and MEMORY objects will work. Remember, API Declarations made in a Module can have project-global (Public) scope--form based API declarations must always be Private.

API Text Viewer

Adding API Function Declarations is usually a tedious affair—Their complex structures are very typo-prone. Visual Basic provides a tool to make the process a little easier. Launch the API Text Viewer program (you’ll find it in the Visual Basic group off of the Windows 95/98/NT/2000 Start Button). Once the API Viewer is launched, select Load Text File from its File menu. Load the Win32api.txt file. Scroll through the Available Items list. Click on each item below and click the Add button to add their declarations to the Selected Items list:

Once the declarations for all 3 API functions are added to the Selected Items list, click the Copy button to the right of the list. Now exit from the API Text Viewer program. Right click in the General Declarations section of your modSysinfo Module (above the Sub Main procedure), and select Paste from the context menu. The complete declarations for the GetSystemInfo, GetVersionEX, and GlobalMemoryStatus API functions should appear. Whoa! Typing those would have been a pain.

The complexity of API declarations cannot be understated. Even Microsoft has made typos in several of the declarations in the Win32api.txt file. Watch out for this typo in your declaration of GetVersionEX (Note: This was fixed in VB version 6 and higher):

Declare Function GetVersionEx Lib "kernel32" Alias "GetVersionExA" (ByVal lpVersionInformation As OSVERSIONINFO) As Long

The ByVal keyword in the declaration above should NOT be part of the delcaration of GetVersionEX. Delete the ByVal keyword (shown in bold above) if you find it in your GetVersionEX declaration. The declarations of the two other API functions are correct.

Now you’ve got some typing to do. Before these API functions will work, you need to create 3 User Defined Data Types—one for each API function. A User defined Data Type is a way of grouping several variables into one, like the following example—Remember, the following code is just an example of a User Defined Data TypeDon't type it into your program!

Type EmployeeRecord    'Create user-defined type.
      ID As Integer                 'Define its Elements.
      Name As String * 20
      Address As String * 30
      Phone As Long
      HireDate As Date
End Type

The following line of code is how you would dimension a variable of the above User Defined EmployeeRecord type:

Dim Employee As EmployeeRecord

And this is how you can assign values to the Elements of the Employee variable created by the above dimension statement::

Employee.ID = 405
Employee.Name = "Fred Mertz"
Employee.Address = "1943 East Lester Avenue"
Employee.Phone = 5021993
Employee.HireData = "Jan 15, 1994"

This example demonstrates another good reason to follow proper naming conventions when naming Controls and Variables in your programs. Employee is a Variable, not a Control, but looking at the code above, it almost appears as if the Properties of a Control named Employee are being set. Fortunately, proper naming conventions dictate that all Controls you add to your program have a 3 letter, lower case prefix, which describes what type of control it is (i.e. txtEmployee would indicate a Textbox, lblEmployee would indicate a Label). You can tell here that Employee is a Variable not a Control because it does not have a Control type prefix.

User Defined data types (or Structures as C programmers call them) are an excellent way for you to create groups of related variables. As your programs become more complex, User Defined data types will become indispensable to you. User Defined data types allow you to create and use structures of different, yet related, variables.  It's also a good tongue-twister, try saying User Defined data types three times fast. 

Well, so much for the lecture on User Defined Data Types (hereafter abbreviated UDDT). Enter the following code to create three User Defined data types in the General Declarations section of the modSysinfo module (above the API declarations), and check your spelling!:

 

Type SYSTEM_INFO 'Structure for GetSystemInfo API
      dwOemID As Long
      dwPageSize As Long
      lpMinimumApplicationAddress As Long
      lpMaximumApplicationAddress As Long
      dwActiveProcessors As Long
      dwNumberOfProcessors As Long
      dwProcessorType As Long
      dwAllocationGranularity As Long
      dwReserved As Long
End Type

Type OSVERSIONINFO 'Structure for GetVersionEx API
      dwOSVersionInfoSize As Long
      dwMajorVersion As Long
      dwMinorVersion As Long
      dwBuildNumber As Long
      dwPlatformId As Long
      szCSDVersion As String * 128
End Type

Type MEMORYSTATUS 'Structure for GlobalMemoryStatus API
      dwLength As Long
      dwMemoryLoad As Long
      dwTotalPhys As Long
      dwAvailPhys As Long
      dwTotalPageFile As Long
      dwAvailPageFile As Long
      dwTotalVirtual As Long
      dwAvailVirtual As Long
End Type

These User Defined data types are designed to be passed to the API functions we declared earlier. When we pass a variable of each of these types to an API function, the elements of each type will have no value.  But the API function will assign value to each element when it returns.  Add the following code after the API declarations in the modSysinfo module:

Public Const PROCESSOR_INTEL_386 = 386
Public Const PROCESSOR_INTEL_486 = 486
Public Const PROCESSOR_INTEL_PENTIUM = 586
Public Const PROCESSOR_MIPS_R4000 = 4000
Public Const PROCESSOR_ALPHA_21064 = 21064

These constants will be used in your code to make it more readable (as if it doesn’t already look like Greek!) That’s it for the code in the modSysinfo module. Now is a good time to save.

So far you have created 4 new Objects (via Class Modules). Now is the time to add Properties to those Objects so that they can do something. Let’s begin with the pOS object. Click on the pOS Class module in the Project Explorer window and click on the View Code button (make sure you’re viewing the code window of the pOS object.). pOS needs a Name property to return the name of the Operating System, and a Build property to return the build or version of the Operating System.

Type this code into the General Declarations section of the pOS Module to create the Name property:

'Get the Operating System Name
Property Get Name() As String
      
‘Create a UDDT variable for the GetVersionEX API call
      Dim VerInfo As OSVERSIONINFO

      Dim ReturnCode As Long
      Set dwOSVersionInfoSize to the size of the UDDT
      '     variable so that the API function knows where the 

      '     individual elements of the UDDT are located 
      VerInfo.dwOSVersionInfoSize = Len(VerInfo)
      ReturnCode = GetVersionEx(VerInfo)
      'Many API functions return 0 when unsuccessful 
      If ReturnCode = 0 Then
            MsgBox "Error Getting Version Information", vbOKOnly  _
                  + vbCritical + vbApplicationModal, "Error!"
            Unload Me
      End If
      'The dwPlatformId element of the UDDT was set to a 
      '     number by GetVersionEX that indicates the OS
      Select Case VerInfo.dwPlatformId
            Case 0
                  Name = "Windows 32s"
            Case 1
                  Name = "Windows 95"
            Case 2
                  Name = "Windows NT"
      End Select
End Property

Now type the following code (below what you’ve already typed) in the General Declarations section of the pOS Module to create the Build property:

'Get the Operating System Version
Property Get Build() As String
      
‘Create a UDDT variable for the GetVersionEX API call
      Dim VerInfo As OSVERSIONINFO
      Dim sVersion As String
      Dim Ver_Major As String, Ver_Minor As String
      Dim ReturnCode As Long
      Dim sStr As String
      
‘Set dwOSVersionInfoSize to the size of the UDDT
      '     variable so that the API function knows where the 

      '     individual elements of the UDDT are located 
      VerInfo.dwOSVersionInfoSize = Len(VerInfo)
      
‘Here’s the API call that gets the info
      ReturnCode = GetVersionEx(VerInfo)

      If ReturnCode = 0 Then
            MsgBox "Error Getting Version Information", vbOKOnly _
                  + vbCritical + vbApplicationModal, "Error!"
            Unload Me
      End If
      
Several elements of the UDDT are used to
      
    to give us the OS Build and Version info 
      Ver_Major = VerInfo.dwMajorVersion
      Ver_Minor = VerInfo.dwMinorVersion
      sVersion = VerInfo.dwBuildNumber
      sStr = Ver_Major & "." & Ver_Minor
      sStr = sStr & " (Build " & sVersion & ")"
      Build = sStr
End Property

Both of these new Properties are Read Only.  That is, the programmer can get value form them, but cannot assign value to them.  If we wanted to create properties that a programmer using our objects could assign value to, we would also have to create a Property Let for each one.  More on Property Let in a later project. 

Now let’s move on to the pCHIP Object. Click on the pCHIP module in the Project Window and click on the View Code button. pCHIP needs just one property called CPU that will return the CPU type that is installed in the computer. Type the following code into the General Declarations section of the pCHIP Module:

'Get CPU Type
Property Get CPU() As String
      Dim SysInfo As SYSTEM_INFO
      
‘Here’s the API call that gets the info
      GetSystemInfo SysInfo

      Select Case SysInfo.dwProcessorType
            Case PROCESSOR_INTEL_386
                  CPU = "Intel 386"
            Case PROCESSOR_INTEL_486
                  CPU = "Intel 486"
            Case PROCESSOR_INTEL_PENTIUM
                  CPU = "Intel Pentium"
            Case PROCESSOR_MIPS_R4000
                  CPU = "MIPS R4000"
            Case PROCESSOR_ALPHA_21064
                  CPU = "DEC Alpha 21064"
            Case Else
                  CPU = "(unknown)"
      End Select
End Property

That takes care of the CPU Property of the pCHIP object.

Now let’s move on to the pMEMORY Object. Click on the pMEMORY module in the Project Window and click on the View Code button. pMEMORY needs four properties called TotPhys, AvailPhys, TotVirt, and AvailVirt. Each will return information about the computer’s memory. Type the following code into the General Declarations section of the pMEMORY Module:

Property Get TotPhys() As String
      Dim MemStatus As MEMORYSTATUS
      Dim Memory As Long
      
‘Here’s the API call that gets the info
      GlobalMemoryStatus MemStatus

      Memory = MemStatus.dwTotalPhys
      TotPhys = Format$(Memory / 1024, "###,###,###") & "k"
End Property

Property Get AvailPhys() As String
      Dim MemStatus As MEMORYSTATUS
      Dim Memory As Long
      
‘Here’s the API call that gets the info
      GlobalMemoryStatus MemStatus

      Memory = MemStatus.dwAvailPhys
      AvailPhys = Format$(Memory / 1024, "###,###,###") & "k"
End Property

Property Get TotVirt() As String
      Dim MemStatus As MEMORYSTATUS
      Dim Memory As Long
      
‘Here’s the API call that gets the info
      GlobalMemoryStatus MemStatus

      Memory = MemStatus.dwTotalVirtual
      TotVirt = Format$(Memory / 1024, "###,###,###") & "k"
End Property

Property Get AvailVirt() As String
      Dim MemStatus As MEMORYSTATUS
      Dim Memory As Long
      
‘Here’s the API call that gets the info
      GlobalMemoryStatus MemStatus

      Memory = MemStatus.dwAvailVirtual
      AvailVirt = Format$(Memory / 1024, "###,###,###") & "k"
End Property

That takes care of the Properties of the pMEMORY object.

Now your pOS, pCHIP, and pMEMORY Objects have the Properties they need. But how do you access them from other applications? Through the Computer Object. Select the Computer Class module in the Project Explorer window and click the View Code button. In your Object hierarchy the Computer Object is at the highest level. Access to the lower level objects (pOS, pCHIP, and pMEMORY) must go through the Computer Object. Add these Private declarations to the General Declarations section of the Computer object:

Private objOS As New pOS
Private objCHIP As New pCHIP
Private objMEMORY As New pMEMORY
Public Parts As New Collection

The Public variable Parts is a Collection data type. Now you need to initialize the collection of Parts (which will include pOS, pCHIP, and pMEMORY). To do this, you will use the Add method of a Collection variable. This needs to be done as soon as the class is loaded. All Class Modules come with a Class_Initialize event procedure (which is similar to a Form’s Load event procedure). Enter the code below into the Computer object’s Class_Initialize event procedure:

Parts.Add Item:=objOS
Parts.Add Item:=objCHIP
Parts.Add Item:=objMEMORY

There is a space between the words Add and Item. These three commands add the objOS, objCHIP, and objMEMORY objects to the Parts collection.

The final step to providing access to the Properties of pOS, pCHIP, and pMEMORY requires you add 3 Property Get procedures to the General Declarations section of the Computer class module:

Property Get OS() As Object
      Set OS = objOS
End Property

Property Get CHIP() As Object
      Set CHIP = objCHIP
End Property

Property Get Memory() As Object
      Set Memory = objMEMORY
End Property

These three properties will allow a programmer using our objects to set references to each of them through the Computer object.  Be sure to save the project now. This project is complete, except we really don’t have any way of testing it. If you try to run it now, it will appear to do nothing (which is exactly what it should do if it’s working correctly). In order for us to test an OLE server or ActiveX DLL application, you’ll need to add a Form to it. You can use a Form to display the output of the properties of the pOS, pCHIP, and pMEMORY objects to see if they work.

Testing our OLE Server application

Add a Form to the project by select Add Form from the Project menu. Choose Form from the Add Form dialog and click the Open button. Set the Name property of the new form to frmTest. Set its Caption property to System Information. Select Save Form1 As from the File menu and save the form as frmTest.frm.

Use the following illustration as a guide and place 7 Labels on the Form (one for each property of your new objects):

Leave the Caption properties blank, and set each Label's Name properties like so:

Object Property Setting
Label Name lblName
Label Name lblBuild
Label Name lblCPU
Label Name lblTotPhysMem
Label Name lblAvailPhysMem
Label Name lblTotVirtMem
Label Name lblAvailVirtMem

Add this code to the Form_Load event procedure of the frmTest Form (don’t type the line numbers):

1.           Dim objComputer As Object
2.           Set objComputer = New Computer
3.           lblName.Caption = "OS: " & objComputer.OS.Name
              lblBuild.Caption = "Build: " & objComputer.OS.Build
              lblCPU.Caption = "CPU: " & objComputer.CHIP.CPU
              lblTotPhysMem.Caption = "Total Physical Memory: " & _
                        objComputer.Memory.TotPhys
              lblAvailPhysMem.Caption = "Available Physical Memory: " & _
                        objComputer.Memory.AvailPhys
              lblTotVirtMem.Caption = "Total Virtual Memory: " & _
                        objComputer.Memory.TotVirt
              lblAvailVirtMem.Caption = "Available Virtual Memory: " & _
                        objComputer.Memory.AvailVirt

On line 1 you dimension a variable called objComputer as an Object type.

Line 2 is where you use the objComputer variable to create a new Computer object. Computer is your custom object which is the top member of your Object Hierarchy. Through it, you can access the other custom objects (OS, CHIP, and Memory), and their properties.

The code on line 3 shows how objComputer can be used to access the Name property of your custom OS object.

The remain lines of code copy all the other Properties of your custom objects to the Caption properties of the remaining Labels.

This project is ultimately going to create an ActiveX DLL file (OLE Server), which is not a standalone application. And to that end, the Startup Object for this project has been set to Sub Main (The Sub Main procedure in the modSysinfo module file that you added in a previous step). With Sub Main as the Startup Object, if you run the program now, you won’t see the frmTest form. But you need to see frmTest to test the application, so add the following temporary code to Sub Main, and make the following temporary changes to your program:

  1. Add this line of code the Sub Main procedure in the modSysinfo code window:

frmTest.Show

This displays the frmTest form when the program starts.

  1. Choose the Sysinfo Properties item on the Project dropdown menu, and with the General tab selected, change the Project Type to ActiveX EXE. Click on the Components tab and enable the Standalone option under Start Mode.

These changes make the application run in a Standalone mode. If everything is working well, when you run the program, the frmTest form will be displayed with similar output to the example above.

But remember, the changes in steps 1 and 2 above are just temporary. After testing the program, you will need to comment out the frmTest.Show line, and set the Project Type back to ActiveX DLL before compiling this application into its final form—which you will do in the next project.

Save the project. Go ahead and test it, and fix any problems. Do Not add any enhancements to this project!