Gw Temp

Menu

Tutorial - 'Visual Basic: Subclassing and Advanced Timers' by AzureFenrir

An item about Programming Languages posted on

Blurb

AzureFenrir's first Visual Basic tutorial, which shows you how to use Subclassing and API Timers to increase the level of control and power of your program.

Body

Visual Basic: Subclassing and Advanced Timers


You know who I am, and if you don't, my name is of no importance to you. Just understand that I am a part of an world organization known as Gaming World, and my mission is to educate. So...tutorial start...

* Switches to a monotonous voice

If you are reading this tutorial, then you probably already know the basics of Visual Basic, as well as understand the concept of using the Windows API. If you don't, then continue. If you do, then proceed to the "Subclassing" section.



Using the Windows API in Visual Basic in a Nutshell

The Windows API are a set of functions that are included in various DLLs and libraries. These DLLs allow for detailed manipulation of windows and can thus do anything that windows allows you to do. This includes controlling and terminating programs, gathering windows messages, ownerdrawing controls, etc. - all things that VB's limitations prevents you from doing.

Before you use windows DLL functions, you must "include" it, or tell VB that your program will use it. To "include" a windows DLL function in Visual Basic, you must use the following syntax:

Declare Function FunctionName Lib "LibraryName" (ByVal Parameter1 As Type, ...) As ReturnType

FunctionName is the name of the function. It must be the exact same as the function's name in the DLL.

LibraryName specifies the library that the function comes from, such as "shell32.dll". The most commonly used libraries are "GDI32", "kernel32", and "user32".



Subclassing

Are you familiar with the concept of windows messages? Well, every time something happens to your program or certain important things happen to windows, windows will send your program a "message" informing your program of this change. Visual Basic's Events (like Button_Click and ComboBox_Change) are automatically called by the Visual Basic-assigned Window Procedure when it receives the proper messages. For example, whenever the WM_CLICK message is received for your form, the event "Form_Click" is automatically generated.

A window procedure is the function that is responsible for detecting these windows messages and responding to them correctly.

Befure you start subclassing, please realize that you cannot use the STOP button to stop your program. You must also not have any breakpoints within your code, and should not pause you program. Otherwise, your program AND Visual Basic will crash, taking any of your unsaved data hostage.

Subclassing requires the following API function (and variable) declarations:

Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Public Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long

Public Const GWL_WNDPROC = (-4)

Public ProcOld As Long


SetWindowLong is an API Function that allows you to set certain properties of a window, including its style, its parent window, and most importantly (in this tutorial), its Window Procedure. We will be using this function to change the Window Procedure of our form so we can intercept our messages before VB gets its nasty little hands on it.

To do this, we need to create a new callback function. If you didn't put the API functions in a module, then add a new module and put the API functions in there (and not in the form that you want to subclass). Then, add the following function to your module:

Public Funciton WindowProc(ByVal hwnd As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
    WindowProc = CallWindowProc(ProcOld, hwnd, Msg, wParam, lParam)
End Function


This will be your form's new window procedure. Obviously, because we didn't detect any messages, this does nothing as of now, just like me (just kidding!), but you can easily add messages for the function to detect.

In the window Procedure, the hwnd parameter will contain the handle of the form that you are trying to subclass (you should know what a handle is if you know about the windows API, but if not, it is Window's way of identifying a form). The Msg parameter shows you the message that was sent, and the other two parameters contain extra information. If you use any messages that require you to physicall modify any of these parameters, you should make them ByRef.

The CallWindowProc just sends the message back to Visual Basic's default window Procedure for handling. Obviously, your WindowProc won't handle ALL of the messages, and it's definitely wise to pass every unused message to Visual Basic. ProcOld simply points (or refers to) to the old Visual Basic's Window Procedure.

Now, in your form, you need to add the following code:

Private Sub Form_Load()
    ProcOld = SetWindowLong(Me.hwnd, GWL_WNDPROC, AddressOf WindowProc)
End Sub

Private Sub Form_Unload()
    SetWindowLong Me.hwnd, GWL_WNDPROC, ProcOld
End Sub


As we can see, whenever the form loads, we call SetWindowLong to set the window procedure to our own Window Procedure. Because SetWindowLong returns the value of the previous long (in our case, the previous Visual Basic-assigned window procedure), we can just set ProcOld (which, if you can recall, holds the previous procedure) to SetWindowLong's return value.

Obviously, Visual Basic will crash if you don't set the window procedure back to the old one when your program exits. Thus, we must set the window procedure back to ProcOld during Form_Unload. This is why using the "Stop" button or trying to debug your program will result in a crash - these bypass the Form_Unload code, which eans that the window procedure is still set to our custom one. And since VB didn't retain control of the form, it will cry like a child and throw the programmer a crash to annoy him/her.

But can't we just use the simple Visual Basic events that doesn't crash if we try to debug it? What benefits could subclassing offer us? Plenty, my dear tutorial reader. Let's use a frequent subclassing tutorial example:

Let's assume that you want to assign a maximum size to a form. One easy way to do this is to change the form's size in the Form_Resize event, but try it and run the code. Oh no! What's with the horrible flickering? What did Claw do to your program?

Well...the size event offurs after your for has been resized. This means that your form will enlarge for a while, and then shrink because of your code. This, and not the AOL-using Claw, caused this flickering.

So...how do we fix it? Simple enough. There's a Windows Message, WM_GETMINMAXINFO, that is called before the form resizes. In addition, this message allows you to easily set BOTH the max and min sizes for a form AND eliminate flickering. In fact, it even allows you to set the maximized size of the form! How conveinent!

To use this, let's add a bunch of code to the module's General_Declarations, after the API code:

Public Const WM_GETMINMAXINFO = &H24

Public Type POINTAPI
x As Long
y As Long
End Type

Public Type MINMAXINFO
ptReserved As POINTAPI
ptMaxSize As POINTAPI
ptMaxPosition As POINTAPI
ptMinTrackSize As POINTAPI
ptMaxTrackSize As POINTAPI
End Type

Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)


So, how do we detect this? Elementary, my dear reader:

Public Funciton WindowProc(ByVal hwnd As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long

    MINMAXINFO WinSize

    Select Case Msg
        Case WM_GETMINMAXINFO
            CopyMemory WinSize, ByVal lParam, Len(Winsize)

            WinSize.ptMaxTrackSize.x = 400
            WinSize.ptMaxTrackSize.y = 300
            WinSize.ptMaxSize.x = 400
            WinSize.ptMaxSize.y = 300

            CopyMemory ByVal lParam, WinSize, Len(Winsize)

            WindowProc = False
            Exit Function

    End Select

    WindowProc = CallWindowProc(ProcOld, hwnd, Msg, wParam, lParam)
End Function


The Select Case simply detects if the message really is the WM_GETMINMAXINFO that we are looking for. Simple enough.

The message returns a MINMAXINFO stucture as its lParam, which allows you to set the new minimum and maximum form sizes for the form. However, because it's only a pointer to this info (lParam is a long) and visual basic cannot resolve pointers, we must use the CopyMemory function to copy the info to a new structure first. Then, we can change what we need, and copy the info back to the reserved memory location.

Obviously, as shown above, we changed the maximum form size (as well as the size of the form when maximized) to 400x300.

The WindowProc = False indicates that we've handled the message. Since WM_GETMINMAXINFO doesn't need to return a value, we must return False (0) and exit the function so that VB doesn't get its hands on it.



Advanced API Timers

API Timers carry the same burden. Since (as you will see) we need to use a API callback function for timers, you cannot debug or end your program with the End button.

So...why would you use API timers instead of regular timers? Elementary...OK, I'll stop, the joke is old.

Well, API timers are MUCH faster than regular VB tiers. The VB Timer is an ActiveX control contained in a massive DLL, and is often slow (it can't go lower than about 55 miliseconds). However, API timers have no such restriction, and thus could run at speeds as high as 1 milisecond...not to mention that it's much more efficient memory-wise.

API Timers require a different set of API functions than Subclassing:

Declare Function SetTimer Lib "user32" (ByVal hWnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
Declare Function KillTimer Lib "user32" (ByVal hWnd As Long, ByVal nIDEvent As Long) As Long


The SetTimer function allows you to create and set a new timer, while KillTimer allows you to destroy (stop) a timer. hWnd, as you may have guessed, is a handle to the window that the timer "belongs" to. This window will receive a WM_TIMER message every time the timer fires. Obviously, this is not a good idea, as windows with too many messages running might never get to the timer code.

nIDEvent specifies a number that uniquely identifies the timer. Quite simple - this number will be passed down to the callback function and the WM_TIMER message, allowing you to identify which timer was activated and act appropriately. It also allows you to easily stop the right timer.

uElapse (SetTimer) is like the interval property - it allows you to set how long the timer waits before executing. lpTimerFunc (SetTimer) is the pointer to a Timer Callback function. Think about a Window Procedure, except for timers :).

Obviously, API Timers will require a different callback function, with different parameters:

Public Sub TimerProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal id As Long, ByVal dwTime As Long)

End Sub


hWnd is obviusly the hWnd of the for that the tier in question belongs to, and id correlates to "nIDEvent" - it specifies the ID of the timer that called the event. uMsg always contain WM_TIMER (so it's somewhat useless), and dwTime just specifies how long windows has been running (similar to the GetTickCount event). "id" is really the only thing that you really need in this event.

So, here's an API timer example:

// FORM CODE
Private Sub Form_Load()
    SetTimer hWnd, 2, 1, AddressOf TimerProc
End Sub

Private Sub Form_Unload()
    KillTimer hWnd, 2
End Sub

// MODULE CODE
Public Sub TimerProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal id As Long, ByVal dwTime As Long)
    // This function will set the millisecond timer with a precision of 1 ms
    frmTimer.TimerUpdate
End Sub



* Returns to monotonous voice

This tutorial has ended. You shall stop reading now. There is no more content, so you will end your reading. Thank you for your cooperation and we hope to see you again :P!