Tips and Tricks for Visual Basic Programming:
This file contains gems I discovered while learning Visual Basic 5.0.
In just three months I went from zero to writing a number of ActiveX
controls for the project I was working on. The QA team was convinced
that the application was written in C++ -- the app had dynamically
resizable dialog boxes, a tabbed main window, a pair of Explorer like
tree controls and even an animated easter egg in the about box!
Syntax: =================================================================
Alternative way of using object methods and properties:
Usually you use object.property but you can use
[object].property if for example you named object the
same as a keyword i.e. Loop
Control Arrays:
Referencing an item in a control array.
Controls("List1") Controls!List1 Controls(3)
For Each Control In Form1.Controls()
Control.Top = 200
Next Control
Adding to a Control Array:
Load cmdArray(5) Add a new control to the array
Unload cmdArray(5) Remove a control from the array
cmdArray.Count returns the total number of controls in the array
For i = LBound(cmdArray,1) to UBound(cmdArray,1)
cmdArray(i).Top = 200
Next i
Control Containers:
Form Holds all controls on a form
Frame Use to group some controls together with a label
PictureBox Use to group controls together
ToolBar Use to group toolbar controls together
Moving a control to a different container:
cmdButton.Container = picContainer
Predefined Collections:
Forms An array of all loaded forms
Controls An array of all controls on a form
Printers An array of the printers in the system
Predefined Globals:
App Used to get info about the Application
Screen Used to get info about the Windows Desktop
Err Used to hold info about the last error
Generic Objects:
as Form Use to point to any type of form in the project
as Control Use to point to any type of control
as MDIForm Use to point to any type of MDI form
as Object Use to point to any type of object
Default Indexing:
Control Arrays Base 0
Strings Base 1
Point(x, y) Base 0, 0
ListBox Base 0
Choose(idx,...) Base 1
ImageList Base 1
SSTab Base 0
StatusBar Panels Base 1
Collections Base 1
MSFlexGrid Row,Col Base 0
Default Properties:
TextBox .Text
You may use the object name to assign to a default property
txt = "Hello" instead of txt.Text = "Hello"
To set a default property: Tools/Procedure Attributes/Advanced
Remove the current default - find the Name and set Procedure ID
to None. Then set a new one.
Can also use F2 - the object browser and clear/set the default
with context menu
Using New:
1. Dim x as New Object
2. Dim x as Object
Set x = New Object
2. is faster, prefer it
When finished with an object: Set x = Nothing to free the memory
When finished with a Form, Unload it and free the Hidden Global Reference
Unload Form1: Set Form1 = Nothing
Debug.Assert (Not obj Is Nothing) ' verify that an object points to something
Create and show a new form:
Dim frm as Form1
Set frm = New Form1
frm.Show
...
Unload frm ' Unload the form
Set frm = Nothing ' Frees the form's memory
To click a button in code:
cmd.Value = 1 not cmd_Click()
Form Lifetime:
1. Created but not loaded
2. Loaded, but not shown
3. Shown
4. Unloaded and unreferenced while a control is still referenced
5. Memory and resources completely reclaimed
' Stage 1 in lifetime
' This is the first form event that gets executed
' Form exists as an object but has no window.
' No controls exist in the form either.
' All forms pass through this state.
' You can call Me.Show here to load and show the form
Form_Initialize()
' Create but don't Load or Show a Form:
Dim frm As Form1
Set frm = New Form1
' You can call any custom Sub, Function and Property Procedures
' without forcing a load of the form. If you access any built in
' properties of the form or any of the form's controls, it will
' be loaded.
' Create, Load and Show a Form
Form1.Show
' Stage 2 in lifetime
' The form now has a window but hasn't been shown
' Controls have been created and loaded
' Forms pass through this function only once
' no matter how many times they are shown and hidden
' But if the form is Unloaded and Loaded multiple times
' then this will be called multiple times for the same form.
Form_Load()
' Form is about to unload.
' We can prevent unloading by setting the Cancel argument
' We can determine how we're being unloaded:
' User clicked Close
' Unload called
' App closed
' Windows shutdown
Form_QueryUnload(Cancel)
' Stage 4 in lifetime
Dim frm As New Form1
Dim obj As Object
frm.Show() vbModal
' When the modal form is dismissed, save a reference to
' one of its controls
Set obj = frm.Command1
Unload frm
Set frm = Nothing
' The form is now unloaded and all references to it released
' but we still refer to a control on the form so it's still
' in memory
obj.Caption = "Back to life"
' Form_Load() was just called again by the above statement
' Unload the form. Note, after unload,
' The form still exists in memory until all references
' to it and it's controls are set to Nothing
Form_Unload()
Static Properties of a Class:
Simulating Static Class Member Variables:
class.bas
' or make it a property get
Public Property Get Static() as Integer
return ClassGetSetStatic(false)
End Property
' or make it a property let
Public Property Let Static(ByVal iValue as Integer)
ClassGetSetStatic(True, iValue)
End Property
class_stat.bas
' You should never call this function, only class.bas
' needs to call this.
Public Function ClassGetSetStatic(bSet as boolean, optional value as integer) as integer
Static iValue as Integer
if bSet then iValue = value
return iValue
end Function
Variant Properties of a Class:
Private mvntAnything As Variant
Public Property Get Anything() As Variant
If IsObject(mvntAnything) Then
Set Anything = mvntAnything
Else
Let Anything = mvntAnything
End If
End Property
Public Property Let Anything(ByVal NewValue as Variant)
Let mvntAnything = NewValue
End Property
Public Property Set Anything(ByVal NewValue as Variant)
Set mvntAnything = NewValue
End Property
Choosing whether to make something a Property or a Method:
The Syntax Argument:
Do you want users to be able to say
Set Widgets.Item(4) = wdgWidget
If yes then a property is the way to go
The Property Window Argument:
Can you imagine the member showing up in the Property
window or on a property page? If that doesn't make sense
don't implement it as a property
The Perception Argument:
Do you want the member to be percieved as a method or
as a property? If retrieving the member is slow, it
might be best to make it a member. Properties are
assumed to be fast.
The Sensible Error Argument:
If a user forgets that a property is read only and trys
to assign to it, a property gives a more intuitive
error message than a method.
The Argument of Last Resort:
Flip a coin. If none of the other arguments seem
compelling, it probably doesn't make much difference
Implementing Class Events (Callbacks):
Widget.cls
Option Explicit
Public Event PercentDone(ByVal Percent As Single, ByRef Cancel as Boolean)
Public Sub LongTask(ByVal Duration as Single, ByVal MinimumInterval as Single)
Dim sngThreshold as Single
Dim sngStart as Single
Dim blnCancel as Boolean
sngStart = Timer
sngThreshold = MinimumInterval
RaiseEvent PercentDone(0, blnCancel)
Do While Timer < (sngStart + Duration)
' Do some work
If Timer > (sngStart + sngThreshold) Then
RaiseEvent PercentDone(sngThreshold / Duration, blnCancel)
if blnCancel Then Exit Sub
sngThreshold = sngThreshold + MinimumInterval
End If
Loop
RaiseEvent PercentDone(100, blnCancel)
End Sub
Form1.bas
Option Explicit
Private WithEvents mWidget as Widget
Private mblnCancel as Boolean
Private Sub Form_Load()
Set mWidget = New Widget
End Sub
Private Sub mWidget_PercentDone(ByVal Percent as Single, Cancel as Boolean)
Static bMutex as Boolean ' Prevent re-entry with a mutex boolean
if (Not bMutex) then
iMutex = True
lblPercentDone.Caption = CInt(100 * Percent) & "%"
lblPercentDone.Refresh
DoEvents
If mblnCancel Then Cancel = True
iMutex = False
end if
End Sub
Private Sub Command1_Click()
mblnCancel = False
Call mWidget.LongTask(14.4, 0.66)
End Sub
Private Sub Command2_Click()
mblnCancel = True
End Sub
Implementing Custom Form Events:
Class1.cls
Private WithEvents mForm1 as Form1
Public Property Get Form1() as Form1
Set Form1 = mForm1
End Property
Public Property Set Form1(ByVal NewForm1 as Form1)
Set mForm1 = NewForm1
End Property
Private Sub mForm1_Gong()
MsgBox "Gong!"
End Sub
Form1.bas
Public Event Gong
Private mc1 as Class1
Private Sub Form_Load()
Set mc1 = New Class1
Set mc1.Form1 = Me
End Sub
Private Sub Text1_Change()
RaiseEvent Gong
End Sub
Abstract/Concrete Classes:
Animal.cls (Abstract class)
Public Sub Move(ByVal Distance as Double)
End Sub
Public Sub Bite(ByVal What as Object)
End Sub
Flea.cls (Concrete class implementing Animal)
Option Explicit
Implements Animal
Private Sub Animal_Move(ByVal Distance as Double)
Debug.Print "Flea Moved"
End Sub
Private Sub Animal_Bite(ByVal What as Object)
Debug.Print "Flea bit a " & TypeName(What)
End Sub
Debugging Object Creation/Termination
Keep track of all objects created in a global collection.
Then as you destroy objects you can see which ones are left
by iterating over the global collection.
globcol.bas
Public gcolDebug as New Collection
Public Function DebugSerial() as Long
Static lngSerial As Long
lngSerial = lngSerial + 1
DebugSerial = lngSerial
End Function
Add this code to all your class modules:
Public mlngDebugID as Long
Property Get DebugID() As Long
DebugID - mlngDebugID
End Property
Private Sub Class_Initialize()
' Add string to global collection for this instance of the class
mlngDebugID = DebugSerial
gcolDebug.Add "MyClassName Initialize; DebugID=" & DebugID, CStr(DebugID)
End Sub
Private Sub Class_Terminate()
' Remove string from global collection for this instance of the class
gcolDebug.Remove CStr(DebugID)
End Sub
Implementing a Collection Class:
Employees.cls - the collection of employees
Option Explicit
Private mcolEmployees As New Collection
Public Function Add(ByVal Name as String, ByVal Salary as Double) As Employee
Dim empNew as New Employee
Static intEmpNum as Integer
With empNew
intEmpNum = intEmpNum + 1
.ID = "E" & Format$(intEmpNum, "00000")
.Name = Name
.Salary = Salary
mcolEmployees.Add empNew, .ID
End With
Set Add = empNew
End Function
Public Function Count() As Long
Count = mcolEmployees.Count
End Function
Public Sub Delete(ByVal Index as Variant)
mcolEmployees.Remove Index
End Sub
' Make this the default method of the class:
' Tools/Procedure Attributes/(Name=NewEnum)/Advanced/Procedure ID/(Default)
' This allows users to write Employees("E00001") instead of
' Employees.Item("E00001")
Public Function Item(ByVal Index as Variant) As Employee
Set Item = colEmployees.Item(Index)
End Function
' This allows users to use For Each on the collection
' You need to hide this function in the type library:
' Tools/Procedure Attributes/(Name=NewEnum)/Advanced/Hide This Member/Procedure ID/-4
Public Function NewEnum() as IUnknown
Set NewEnum = mcolEmployees.[_NewEnum]
End Function
Employee.cls - The class to put in the employee collection
Option Explicit
Public Name as String
Public Salary as Long
Private mstrID as String
Property Get ID() As String
ID = mstrID
End Property
Property Let ID(strNew As String)
Static blnAlreadySet as Boolean
If Not blnAlreadySet Then
blnAlreadySet = True
mstrID = strNew
End If
End Property
Quick Tricks: ===========================================================
Load an entire file into a string quickly:
Dim iFILE as Integer
iFILE = FreeFile
Open szFile For Input As #iFILE
szString = Input(iFILE, LOF(iFILE)) ' LOF=Length of File
Close #iFILE
C++ Style Exception handling approximation:
On Error GoTo catch ' try {
do_something
do_something_else ' }
If (Err) Then ' catch (...) {
catch:
' Be careful of errors here - app will terminate
recover_error
End If ' }
continue_other_stuff
Resuming error handling in the catch section
On Error GoTo Catch
do_something
do_something_else
If (Err) Then
Catch:
Resume Here
Here:
On Error GoTo Catch22
recover_error
End If
If (Err) Then
Catch22:
' Handling an error from Here: block
recover_recover_error
End If
continue_other_stuff
Optimizing Code: ========================================================
Checklist for Optimizing Code for Speed:
Avoid using Variant.
Use Option Explicit to help avoiding variant.
Be careful declaring multiple variables on the same line.
Dim x,y, z as Long -- x,y are variants not long
Use Long Integer variables and integer math particularly in loops.
If ScaleHeight is always set to twips or pixels then you can use
long integers for all size and position calculations and graphics
Use integer division \ if you don't need a decimal result.
Long=Fastest, Integer, Byte, Single, Double, Currency=Slowest
Cache frequently used properties in variables.
Variables are 10-20 times faster than properties. Never get
properties more than once in a procedure unless you know it
has changed. Store a property in a variable when used in a loop
Cache function returns if used more than once.
Replace procedure calls with inline code.
Although this produces code bloat and maintenance trouble
it may be necessary to speed up critical code.
Use constants whenever possible.
Use intrinsic constants from Object Browser when possible.
Unused constants are removed from the final .exe
Pass arguments ByVal instead of ByRef.
Use typed optional arguments.
Variants are 16 bytes -- slow to push and fill up stack space
quickly.
Take advantage of collections.
Use For Each Next instead of For Next. For Each may be optimized
for a given collection.
Avoid using the Before and After arguments with Add.
Use keyed collections rather than arrays for groups of objects
of the same type. If you can't key the objects and have to
traverse the array to find then a straight array is faster.
Arrays are faster for small collections of less than about
100 objects.
Set ClipControls property of containers to False.
Unless you are using graphics methods (Line, PSet, Circle, Print)
set ClipControls on the form, frame and picture box controls
Use AutoRedraw appropriately.
Set to true if you draw complex graphics which don't change
very frequently. Use False if you draw graphics that change
frequently. You must then perform the drawing in the Paint
event.
Use image controls instead of picture box controls.
Use image when you are only displaying pictures and reacting
to click events and mouse actions on them.
Hide controls when setting properties to avoid multiple repaints.
Place a number of controls on a picturebox and hide it prior
to updating the contained controls. Also, use Move to move
a control instead of setting Left and Top separately. This
causes two repaints instead of one.
Use Line instead of a series of PSet methods.
Keep forms hidden but loaded.
This reduces memory but makes showing forms much faster.
Preload data.
Load multiple files at the same time instead of making the user
wait later.
Use timers to work in the background.
Use a static or module variable to keep track of progress and
do a little bit each time through the timer.
Use progress indicators.
Use the standard Windows 95 ProgressBar and DoEvents to keep
it updated. Display the wait cursor with the MousePointer
property (vbHourglass)
Speed the start of your application.
Use Show in the Form_Load event.
Place show at the start of Form_Load so it is visible
while the rest of the startup code executes
Me.Show: DoEvents: Load MainForm: Unload Me: MainForm.Show
Simplify your startup form.
Use a simple splash form to let the user know that the
application has started. Preload the most commonly used
forms while the splash screen is up. Use a progress bar.
Don't load modules you don't need.
Avoid calling procedures from other modules in your startup
form.
Run a small Visual Basic application at startup to preload
the run-time DLLs.
Create a small but useful application like a calendar and
install that to the startup folder. Then the VB DLLs will
already be loaded.
Use early binding.
Dim X as New MyObject instead of Dim X as New Object. Only
declare as Object if you don't have the type library available
or you need to be able to pass any kind of object as an argument.
Minimize the dots.
Especially when referring to other applications' objects.
Use the default property where possible and keep a reference
to objects deep in a chain instead of always referring to it
with the full reference. Take advantage of other methods available
from the object to accomplish the same things
i.e. Application.Workbooks.Item(1).Worksheets.Item("Sheet1").Cells.Item(1,1)
becomes Application.Workbooks(1).Worksheets("Sheet1").Range("A1")
Use Set and With ... End With.
Set xlrange = Application.ActiveSheet.Cells(1,1)
then do stuff with xlrange instead of repeating the long
expression.
or With Application.ActiveSheet.Cells(1,1)
...
End With
Minimize cross-process calls.
Especially in a loop. If possible build a macro loop in the
object and call that. Design your ActiveX components with
loops so users can do the same. Provide methods which take
a number of parameters and set properties from the parameters
so users don't have to call several property methods in a row.
Checklist for Optimizing Code for Size:
Reduce the number of loaded forms.
Set form = Nothing after you've done an Unload and only load
the form when needed.
Reduce the number of controls on each form.
Use control arrays instead of a large number of controls of
the same type.
Use labels instead of text boxes.
Keep data in disk files or resources and load only when needed.
This is particularly useful for large bitmaps and strings.
Organize your modules.
Modules are loaded on demand. Group common functions
together in small modules to reduce the amount of loading done.
Consider alternatives to variant data types.
Only use variants when you need a variable to change types
and you can't do it easily with other data types.
Use Dynamic Arrays and Erase to reclaim memory.
Use Erase or ReDim Preserve to discard unneeded data in a
dynamic array.
Reclaim space used by strings or object variables.
Reclaim string space from global and module level strings
when you're through with them. Also set Object references
to nothing to reclaim memory.
Eliminate dead code and unused variables.
Simple Debug.Print statements are omitted from the final .exe
but if any functions are called in the Debug.Print statement
they are still executed. Remove these dead calls in the final
product or use an #if.
Finding unused global/module variables. Use Option Explicit
in all modules and then comment out the variable declaration.
If no errors then the variable is unused.
Use the image control to display bitmaps.
Load bitmaps from files as needed and share pictures.
Store pictures in a resource file and use LoadResPicture to load
them. Share the same picture between controls if possible.
Use the PaintPicture method.
Use PaintPicture to display a bitmap on a form instead of
an image control. Especially if you want to tile the bitmap.
Free the memory used by graphics.
Set Picture properties to Nothing when they are not in use.
If using AutoRedraw you can reclaim the memory used by the Image
buffer: pic.AutoRedraw = True: pic.Cls: pic.AutoRedraw = False
Use RLE-Format Bitmaps or Metafiles.
Save images as RLE encoded so they will load faster and take
up less memory. Metafiles can save even more in some situations.
Avoid scaling metafiles to a larger size. JPG and GIF are also
good compression sizes.
               (
geocities.com/gurucoder)