Visual Basic Secrets

Copyright© 2002 by Kevin Wilson


  • Introduction
  • Using Pointers In Visual Basic
  • VarPtr, StrPtr, and ObjPtr
  • ByRef  /  ByVal
  • AddressOf and Callbacks
  • Accessing "Hidden" API's


Introduction:

Visual Basic is called a "Rapid Application Development (RAD) Development Tool" because it was designed to take care of the Windows "ground work" for you, thus allowing you to concentrate on the important stuff like the program's functionality and documentation.

For example, when you open VB and add a standard "Form" to your project, there's A LOT that goes into putting that form on the screen when you hit "F5" to execute the program and simply display the form.  You have to call the "CreateWindow" to actually create the Form and give it it's properties that make up it's interface.  You then have to modify it's text font, forecolor, backcolor, device context, etc. by calling various Win32 API's.   Lastly, you have to hook into the Windows messages that are being sent to the newly created form by subclassing it and then catching and processing each Windows messages properly via a "WindowProc" callback function.  More complex interfaces require more complex object creation and handling functionality to be programmed into the form.  C and C++ programmers actually have to create all that object creation, message handling, and object destruction code by hand (or have a template of it generated).

Visual Basic's ability to do the "basics" for you like this is a powerful thing to programmers who know how to correctly use VB as a development tool, but also puts a lot of power into the hands of people that don't know much about programming.  Visual Basic is mocked by C/C++ programmers because of this.   They say, "Anyone can develop with VB, but it takes a real programmer to develop with C/C++."  I say that the SMART programmer chooses Visual Basic because VB eliminates potential bugs in your object creation, message handling, and object destruction routines, VB offers easier and quicker handling of Windows events, VB gives you a more robust interface capabilities, VB gives you easier access to COM objects and third party controls, VB is easier to read because it is very close to reading English where C/C++ is VERY cryptic, VB allows you easy access to the Win32 API (which gives the programmer the ability to tap into the power of Windows), and on top of ALL THAT... Visual Basic has the ability to hook into the power and speed of C/C++ via components, libraries, and other code written in C/C++.  Heh... where's the bragging rights now?   :)

Here's the thing though... even VB programmers that have been in the industry for years don't realize the real power of VB because they don't grasp (or realize) a few key concepts and functionalities that VB offers.  These concepts aren't taught, or at least are not emphasized the way they should, so I call them "VB SECRETS".

^ TOP ^

Using Pointers In Visual Basic:

I was once asked in a job interview a question that I now realize was a TRICK QUESTION.  The question was, "Does Visual Basic have or use 'pointers' ?"  The obvious answer to anyone that uses Visual Basic is "NO".  You don't see pointer declarations and macros in VB like you do in C/C++... and that's what I think the interviewer was getting at.  She accepted my answer with that reasoning.   However, the correct answer should have been "YES".

Visual Basic (like just about every other programming language) does use pointers... EXTENSIVELY.  The difference is, Visual Basic hides them from you whenever possible, or calls them something different so as to not burden you with the formalities and protocols required when using them.

I will proceed to explain how you can use pointers to access information held in variables directly (VarPtr / StrPtr / ObjPtr), pass information to functions by pointers (ByRef / ByVal), and retrieve and pass pointers to functions (AddressOf).

^ TOP ^

VarPtr, StrPtr, and ObjPtr:

The VB Functions "VarPtr" (Variable Pointer), "StrPtr" (String Pointer), and "ObjPtr" (Object Pointer) are UNDOCUMENTED, UNSUPPORTED functions that Microsoft has made available in Visual Basic 5.0, and 6.0.  These functions (along with many others) are no longer available in VB.net.  These functions allow you to get the address in memory where VB variables (pointers) are, as well as the address in memory where the actual data that the variables point to are.   The reason these functions are so useful is because if you know the memory address of data, you can manipulate it, copy it, or pass it around directly instead of relying on VB to do it for you.  This is MUCH faster and (in some cases) gives you the ability to do things that VB on it's own can't do.

This is what Microsoft's MSDN says about "VarPtr":

This function can be used to get the address of a variable or an array element.   It takes the variable name or the array element as the parameter and returns the address. However, you should be aware that unlocked Dynamic Arrays may be reallocated by Visual Basic, so you must be very careful when you use VarPtr to get the address of an array element.

The following example gets the address of a variable:

Dim lngVariableAddress As Long
Dim dblMyVariable As Double
lngVariableAddress = VarPtr(dblMyVariable)

This example gets the address of the fourth element of an array:

Dim lngElementAddress As Long
Dim lngArrayOfLongs(9) As Long
' The following will get the address of the 4th element in the array
lngElementAddress = VarPtr(lngArrayOfLongs(3))

Limitations: The VarPtr function cannot be used to get the address of an array...

This is what Microsoft's MSDN says about "StrPtr":

Strings in Visual Basic are stored as BSTR's. If you use the VarPtr on a variable of type String, you will get the address of the BSTR, which is a pointer to a pointer of the string. To get the address of the string buffer itself, you need to use the StrPtr function. This function returns the address of the first character of the string. Take into account that Strings are stored as UNICODE in Visual Basic.

To get the address of the first character of a String, pass the String variable to the StrPtr function.

Example:

Dim lngCharAddress As Long
Dim strMyVariable As String
strMyVariable = "Some String"
lngCharAddress = StrPtr(strMyVariable)

You can use this function when you need to pass a pointer to a UNIOCODE string to an API call.

This is what Microsoft's MSDN says about "ObjPtr":

ObjPtr takes an object variable name as a parameter and obtains the address of the interface referenced by this object variable.

One scenario of using this function is when you need to do a collection of objects. By indexing the object using its address as the key, you can get faster access to the object than walking the collection and using the Is operator. In many cases, the address of an object is the only reliable thing to use as a key.

Example:

objCollection.Add MyObj1, CStr(ObjPtr(MyObj1))
...
objCollection.Remove CStr(ObjPtr(MyObj1))

Note that the "Limitations" at the end of the information about "VarPtr", it said that you can't use VarPtr to get the address of an array.  That's true... to a point.  You can't pass the variable "MyArray" to it (because VB stores arrays in an OLE object called a "SafeArray"), but if you get the address of the first element of the array "MyArray(0)", you have the address of the whole array because arrays elements are stored in memory contiguously (in numeric order from the first to the last).   So if a Win32 API, or a C/C++ function asks for a pointer to a byte array, like this:

Option Explicit
Private Type POINTAPI
  
X As Long
  
Y As Long
End Type


'BOOL Polyline(
'  HDC          hDC,    // handle of device context
'  CONST POINT *lpPT,   // address of array containing endpoints
'  int          cPoints // number of points in the array
');

Private Declare Function Polyline Lib "GDI32.DLL" (ByVal hDC As Long, _

        ByRef
lpPT As Any
, ByVal cPoints As Long) As Long

You could call it like this:

Private Sub Form_Load()
  Dim ThePoints() As POINTAPI
  Me
.AutoRedraw = True
  Me
.Visible = True
  Me
.Move 0, 0, Me.Width, Me.Height
  ReDim ThePoints(1 To 5) As POINTAPI
  ThePoints
(1).X = 0:   ThePoints(1).Y = 0
  ThePoints
(2).X = 100: ThePoints(2).Y = 0
  ThePoints
(3).X = 100: ThePoints(3).Y = 100
  ThePoints
(4).X = 0:   ThePoints(4).Y = 100
  ThePoints
(5).X = 0:   ThePoints(5).Y = 0
 
If Polyline(Me.hDC, ByVal VarPtr(ThePoints(1)), 5) = 0 Then Debug.Print "FAILED!"
  Me
.Refresh
 
Erase ThePoints
End Sub

NOTE: Be careful about storing pointers to dynamic arrays because when arrays are reallocated, resized, or redim'ed... it is very possible that you'll have a completely new memory address for the actual data.

For more information on VarPtr, StrPtr, and ObjPtr, see the following:

http://support.microsoft.com/default.aspx?scid=kb;en-us;Q199824
http://msdn.microsoft.com/library/en-us/dnw32dev/html/ora_apiprog6_topic1.asp
http://msdn.microsoft.com/library/en-us/dnovba00/html/LightningStrings.asp
http://msdn.microsoft.com/library/en-us/dnovba01/html/Lightweight.asp

^ TOP ^

ByRef  /  ByVal:

By far the biggest problem VB programmers run into while working with Win32 API's (or any exported C/C++ function for that matter) is correctly passing the required parameters to the function.  Inserting a "ByRef" where a "ByVal" should've been (or visa versa), or passing a value or variable when the function was expecting a pointer can be the one difference between the function being called working perfectly or causing Windows to crash and burn.   Understanding how to pass parameters correctly takes an understanding of how Windows programs work with "calling stacks" and memory allocation between the calling program and the function being called.

First of all, lets discuss what a "call stack" is and how it relates to memory allocation when passing parameters to a function.  The "call stack" is simply a spot in memory where the variables and values being passed to and from a function are stored.  It's called a "stack" because parameter values follow one after the other in memory and are accessed with that assumption.  Because of this, parameters are in a way "stacked on top of eachother" to make up all the information being given to the function.  When a parameter is added to a function's call stack, it is said to be "pushed" onto the call stack.  When a parameter is removed (or cleaned up) from a function's call stack, it is said to be "popped" off the call stack.  The terms "stack", "push", and "pop" are assembler terms (yes, we are that low-level at this point) and if you were to decompile a program or DLL into assembly, you'd see lines with the words "push", "pop", etc.

When Visual Basic calls a Win32 API (or any exported C/C++ function), it expects the function to use the "Standard Calling Convention" ( __stdcall ) as apposed to the default C/C++ calling convention ( __cdecl ).  This means that when the function is called, parameters are passed into memory (or pushed onto the stack) from right to left, and the function being called is responsible for cleaning up the memory passed to it (or popping the memory passed off of the stack).  If you try to call an exported function that is declared with any other calling convention but __stdcall, Visual Basic will not know how to handle the stack and parameters being passed back and forth so you will get a message from VB saying "Bad DLL Calling Convention".

For a more in-depth and advanced explanation of how memory is allocated and deallocated when calling parameters, and what call stacks are and how they work in Windows, I strongly recommend reading an EXCELLENT book by Dan Appleman (Desaware) called "Dan Appleman's Win32 API Puzzle Book and Tutorial for Visual Basic Programmers".

Now lets back up a little and get out of the inner workings of Windows memory and get back to working with VB.  When you call a function, you can pass parameters to it in one of two ways.  You can pass it an explicit value that you want the function to take and work with, or you can pass it a pointer to information already present in memory.  When you're passing simple information like numbers, sizes, flags, etc. you want to pass the information in ByVal (meaning By Value) because you want it to take the value of what you are passing, not the memory address of where that value is currently being held.  Now when you want to pass more complex data to a function like a data type, an array of values, or an object reference, you need to pass a reference (or pointer) to the function telling it where in memory the data is.  This is done by specifying the ByRef (meaning By Reference) keyword.  The function will then go to that point in memory and read the data that applies.  The exception to this is when you pass strings to Win32 API calls (or any C/C++ exported function).  Always pass strings ByVal to API's (unless you're passing a string array... in which case you'd use ByRef, or just pass the first element of the array ByVal).

So at this point you're saying, "I already know about passing parameters ByRef/ByVal".  Yes, but did you realize that what you're doing when you pass "ByRef" is passing a pointer?  If you take that concept a step further, you can do things like make function interfaces more generic (thus opening them up for more broad application) by altering the "ByRef" to "ByVal" and passing an explicit pointer to the data you want to pass.  So instead of declaring your function like this:

Option Explicit

Private Type RECT
  Left   As Long
  Top   As Long
  Right   As Long
  Bottom   As Long
End Type

'int FillRect(
'  HDC         hDC,  // handle to device context
'  CONST RECT *lpRC, // pointer to structure with rectangle
'  HBRUSH      hBR   // handle to brush
');

Private Declare Function FillRect Lib "USER32.DLL" (ByVal hDC As Long, _
        ByVal lpRC As Long, ByVal hBR As Long) As Long
Private Declare Function CreateSolidBrush Lib "GDI32.DLL" (ByVal crColor As Long) As Long
Private Declare Function DeleteObject Lib "GDI32.DLL" (ByVal hObject As Long) As Long

Private Sub Form_Load()
  Dim hBrush As Long
  Dim MyRect As RECT
 
  Me.AutoRedraw = True
  Me.Visible = True
  Me.Move 0, 0, Me.Width, Me.Height
  With MyRect
    .Top = 0: .Left = 0: .Right = 100: .Bottom = 100
  End With
 
  hBrush = CreateSolidBrush(vbRed)
  If hBrush = 0 Then Exit Sub
  If FillRect(Me.hDC, VarPtr(MyRect), hBrush) = 0 Then Debug.Print "FAILED!"
  Me.Refresh
  DeleteObject hBrush
End Sub

If you think about it, this gives you all kinds of options when declaring functions and parameters.  You're not restricted anymore to the exact variable type.  You could make EVERYTHING "Long" variable types and pass pointers around to everything (as long as you were careful about how you did it).   So you're having trouble passing that monster custom type around, FORGET ABOUT IT... pass it with a pointer.  So you're having problems passing that object around, FORGET ABOUT IT... pass it with a pointer.  VB 5.0 won't let you return variable arrays (or funky types and objects) as the return type for a function, FORGET ABOUT IT... pass back a long value representing where the array is in memory and use the CopyMemory API to copy it down into a local array.  See where I'm going with this?   :)

If you just said, "NO"... using VarPtr, StrPtr, and ObjPtr in conjunction with ByRef and Byval allows you to pass around data in ANY format if you know what you're doing.

^ TOP ^

AddressOf and Callbacks:

The "AddressOf" operator is all about callbacks.  "But what is a call back?" you ask.  A callback is the Windows equivalent of a VB "Event".  In fact, VB events (on a very low level) are just about always triggered by callback functions that catch the original event in the form of a Windows Message.  Callbacks are most often seen within the Win32 API (and other C/C++ code) where notification of user and/or Windows activity is required or desired within your application.  You don't see callbacks within VB much because VB handles messaging and notification via "Events", which are much easier and safer to deal with compared to callbacks and all that goes into them.

Lets say that you wanted to receive notification of EVERY message that Windows is sending to a Form within your project (even ones that you'd never use), along with a few custom messages that may be sent to your Form via some other API call(s).  What you would do is setup a callback function that is recognized by Windows ("WindowProc") and then tell Windows (via the "SetWindowLong" API) to send all of it's messages meant for your Form to your callback function instead, so you can inspect them and/or react to them... and then send them on their way (via the "CallWindowProc" API).  Doing this is called "Sub-Classing" and is a very powerful (but at the same time very dangerous) technique that you can use to redraw your Form, it's menus, and/or it's contents in a custom manner (or whatever you want to do with your Form).

There two draw-backs to using "AddressOf":

1) You can only retrieve the address to a function or sub (public or private) contained within a Standard VB Module.  There's no way around this.

2) It can only be called as part of a parameter list to a function or sub.  The way to get around it is like this:

Option Explicit

Public Sub Main()
  Dim lngProcAddress As Long
  lngProcAddress = GetProcAddress(AddressOf WindowProc)
End Sub

Public Function GetProcAddress(ByVal lngAddressOf As Long) As Long
  GetProcAddress = lngAddressOf
End Function

Public Function WindowProc(ByVal hWnd As Long, ByVal uMsg As Long, _
                           ByVal wParam As Long, ByVal lParam As Long) As Long
  ' < YOUR CODE GOES HERE >
End Function

You'll notice that we pass the "AddressOf" with the name of the function we want to get the address (memory pointer) of to the function "GetProcAddress" which simply returns back that value.  Simple concept and is very effective.  The addresses of functions and subs doesn't change so you could store the address of the functions and subs you want to reference this way so you don't have to repeatedly call AddressOf, etc.

"So lets see it in action!" you say...   OK!

Here's an example of "sub-classing" as mentioned above:

Option Explicit

Private Const GWL_WNDPROC = (-4)
Private lngPrevProc As Long
Private lngHWND As Long

Private Declare Function SetWindowLong Lib "USER32.DLL" Alias "SetWindowLongA" _

        (ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Private Declare Function CallWindowProc Lib "USER32.DLL" Alias "CallWindowProcA" _

        (ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal uMsg As Long, _
        ByVal wParam As Long, ByVal lParam As Long) As Long

' This is the CALLBACK function that receives the messages for the specified hWnd
Private Function WindowProc(ByVal hWnd As Long, _

                            ByVal uMsg As Long, _
                            ByVal wParam As Long, _
                            ByVal lParam As Long) As Long
  ' Display the messages and their information in the IMEDIATE window
  ' NOTE: You can find out what messages are being passed by comparing the value of

  ' "uMsg" to Windows Messages (WM_*) constant values defined in the WINUSER.H file
  Debug.Print _

  "hWnd=" & hWnd & ", uMsg=" & uMsg & ", wParam=" & wParam & ", lParam=" & lParam
 
  ' Forward on the messages to where they were originally supposed to go. This MUST
  ' here or the window will become unresponsive because it will stop recieving messages
  WindowProc = CallWindowProc(lngPrevProc, hWnd, uMsg, wParam, lParam)
End Function

' This function starts a new sub-classing
Public Function Subclass_Start(ByVal hWnd As Long) As Boolean
  ' Stop any previous sub-class
  If Subclass_Stop = False Then Exit Function
  ' Attempt to start a new sub-class
  lngPrevProc = SetWindowLong(hWnd, GWL_WNDPROC,
AddressOf WindowProc)
  If lngPrevProc <> 0 Then
    lngHWND = hWnd
    Subclass_Start = True
  End If
End Function

' This function stops any existing sub-classing
Public Function Subclass_Stop() As Boolean
  ' If no previous sub-class was started, just exit
  If lngPrevProc = 0 Or lngHWND = 0 Then
    Subclass_Stop = True
  Else
    ' Set the message handling procedure back to what it originally was
    If SetWindowLong(lngHWND, GWL_WNDPROC, lngPrevProc) <> 0 Then
      Subclass_Stop = True
    End If
  End If
  ' Clear the variables used
  lngPrevProc = 0
  lngHWND = 0
End Function

Option Explicit

Private Sub Form_Load()
  Subclass_Start Me.hWnd
End Sub

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
  Subclass_Stop
End Sub

Here's an example of "enumeration", which is a VERY popular way for Windows to give you back information about "information lists" (like a list of all windows, a list of all the objects on a window, a list of all the installed fonts, etc):

Option Explicit

Private lngWinHandle() As Long
Private lngWinHandleCount As Long

Private Declare Function EnumWindows Lib "USER32.DLL" (ByVal lpEnumFunc As Long, _

        ByVal lParam As Long) As Long

' This is the CALLBACK that enumerates through all windows in the current desktop
Private Function EnumWindowsProc(ByVal hWnd As Long, ByVal lParam As Long) As Long
  ' Incrament the array of window handles
  lngWinHandleCount = lngWinHandleCount + 1
  ReDim Preserve lngWinHandle(1 To lngWinHandleCount) As Long
  ' Add the information to the array
  lngWinHandle(lngWinHandleCount) = hWnd
  ' Tell the function to keep going
  EnumWindowsProc = 1
End Function

Public Function GetAllWindows(ByRef Return_Handles() As Long, _

       Optional ByRef Return_WinCount As Long) As Boolean
  ' Clear any previous information
  Erase lngWinHandle
  lngWinHandleCount = 0
  ' Start enumerating through the windows
  If EnumWindows(
AddressOf EnumWindowsProc, 0) <> 0 Then
    Return_Handles = lngWinHandle
    Return_WinCount = lngWinHandleCount
    GetAllWindows = True
  End If
  Erase lngWinHandle
  lngWinHandleCount = 0
End Function

Option Explicit
Private Sub Form_Load()
  Dim lngWindows() As Long
  Dim lngWindowsCount As Long
  Dim lngCounter As Long
  Dim strWindows As String
  If GetAllWindows(lngWindows, lngWindowsCount) = True Then
    If lngWindowsCount > 0 Then
      For lngCounter = 1 To lngWindowsCount
        strWindows = strWindows & " " & lngWindows(lngCounter) & Chr(13)
      Next
    End If
    Me.AutoRedraw = True
    Me.Print strWindows
  End If
  Erase lngWindows
End Sub

^ TOP ^

Accessing "Hidden" API's:

This part is definately the most "secret" of all the secrets decribed on this page.  There are indeed MANY hidden Win32 API calls in Windows... the trick is to find them and find out how to call them because Microsoft sure won't tell you.

"But why hide them?" you ask?  Because Microsoft adds extra functionality to the API that only they can gain access to.   This gives their products an advantage when (running under Windows) over everyone else's because only they have access to more powerfull functionality, faster functionality, or extra functionality via these hidden API calls when everyone else has to make do with the regular, documented functionality exposed by Windows and the documented in the MSDN.  "Unfair advantage" you say?  DEFINATELY!  But who ever said that Microsoft does business fairly... or ethically for that matter!  These kinds of business practices are what are constantly keeping Microsoft in court and on newspaper headlines.

"What kind of hidden API's are out there, and how do I find out what they are and how to use them?" you ask?  EXCELLENT question, and that's why I've included this here.  There are many web pages out on the internet dedicated to finding these hidden API's and exposing their functionality to "level the playing field" and give the more cool functionality to developers like you and me!  Here's a few good web pages on this:

http://www.geocities.com/SiliconValley/4942/index.html
http://www.users.qwest.net/~eballen1/nt.sekrits.html
http://www.mvps.org/vbnet/code/shell/undocshelldlgs.htm
http://www.mvps.org/vbnet/code/shell/undocformatdlg.htm
http://www.mvps.org/vbnet/code/shell/undocchangeicondlg.htm
http://www.mvps.org/vbnet/code/shell/undocshpaths.htm
http://www.ercb.com/ddj/1992/ddj.9211.html

You can find a few of these "hidden API's" in the modCOMDLG32.bas module under the "VB Standard Modules".  They look like this:

Public Declare Function DLG_FindFile Lib "shell32.dll" Alias "#90" _
       (ByVal pidlRoot As Long, ByVal pidlSavedSearch As Long) As Long
Public Declare Function DLG_FindComputer Lib "shell32.dll" Alias "#91" _
       (ByVal pidlRoot As Long, ByVal pidlSavedSearch As Long) As Long

You'll notice that they are aliased by a number "#90", "#91", etc.  These are called "Ordinal Numbers" and they are a way for you to expose API's through a DLL without exposing it's name.   So if you wrote a function, and you wanted to use it but didn't want anyone else to, you could expose it by a number.  This doesn't give anything away as to what it does or why it's there, but at the same time gives you access to it.

Sneaky, huh?!    :)

 

WELL!  That's all folks.  If I think of any other "secrets" or neat "hidden" or "obscure" functionality within VB (or remember any that I meant to put here), I'll add them.  Happy coding!     =)

 


MAIN   |  DOWNLOADS  |  SAMPLE CODE  |  STEP BY STEP  |   DISCUSSION BOARD  |  LINKS  |  AUTHOR
E-MAIL